如何对第三方脚本延迟加载并统计实际执行时间

解读

在国内前端面试里,这道题表面问“延迟加载”和“性能统计”,实则考察三点:

  1. 是否理解 Grunt 构建阶段与运行时阶段的职责边界;
  2. 能否用 Grunt 插件体系提前生成延迟加载骨架代码埋点钩子,而不是在构建期直接做“延迟”或“统计”;
  3. 是否熟悉国内网络环境(CDN 不稳定、域名备案、HTTPS 混合内容限制)并给出可落地的灰度方案
    回答时务必先区分“构建时”与“运行时”,再给出可复制的 Gruntfile 片段,否则会被面试官认为“只会嘴上说,不会写配置”。

知识点

  1. Grunt 构建期职责:文件预处理、代码注入、版本哈希、雪碧图合并,不负责运行时调度
  2. 延迟加载运行时方案
    • 动态 import()(ESM)
    • IntersectionObserver + preload/prefetch
    • requestIdleCallback 空闲调度
  3. 性能统计 API
    • PerformanceObserver 监听 resource timing
    • PerformanceResourceTiming.duration 获取脚本网络+编译耗时
    • mark/measure 自定义打点
  4. Grunt 插件
    • grunt-string-replace:在 html 里注入埋点 snippet
    • grunt-contrib-uglify:生成 loader 骨架并压缩
    • grunt-filerev:给第三方脚本加哈希,避免缓存干扰统计
  5. 国内合规细节
    • 统计域名需备案且接入工信部 CDN 白名单
    • 避免使用国外 Google Analytics 域名,否则会被防火墙重置连接
    • 灰度发布需兼容微信 X5 内核UC 内核,二者对 type="module" 支持度不同

答案

  1. 在 Gruntfile 中新建任务 delayInject
    grunt.registerTask('delayInject', function() {
      const loader = `
    

window.__delayLog = []; function load3rd(src, name) { const start = performance.now(); const obs = new PerformanceObserver((list) => { for(const entry of list.getEntries()) { if(entry.name.includes(src)) { window.__delayLog.push({ name, duration: Math.round(entry.duration), dns: Math.round(entry.domainLookupEnd - entry.domainLookupStart), tcp: Math.round(entry.connectEnd - entry.connectStart) }); obs.disconnect(); } } }); obs.observe({type: 'resource', buffered: true}); import(src).catch(() => { const script = document.createElement('script'); script.src = src; script.async = true; document.head.appendChild(script); }); }`; grunt.file.write('build/js/loader.js', loader); });

2. 用 `grunt-string-replace` 把 loader 注入到 html 底部:  
   ```javascript
   stringReplace: {
     dist: {
       files: {'build/index.html': 'src/index.html'},
       options: {
         replacements: [{
           pattern: '</body>',
           replacement: '<script src="js/loader.js"></script></body>'
         }]
       }
     }
   }
  1. 在业务代码里调用:
    load3rd('https://cdn.example.com/lib/stat.js', 'stat');
    load3rd('https://cdn.example.com/lib/ad.js', 'ad');
    
4. 上报阶段:  
   - 页面 `onload` 后延迟 3 秒,把 `window.__delayLog` 通过**navigator.sendBeacon**发送到**同域已备案**的 `/m.gif` 接口,避免被广告拦截插件屏蔽。  
5. Grunt 打包时同步生成 source-map 并上传至**阿里 SLS**或**腾讯 RUM**,方便线上回捞真实用户耗时。  

这样,**构建期只负责代码注入与版本管理**,**运行时由浏览器完成延迟加载与性能采集**,职责清晰,面试官一听就知道你“**懂 Grunt 也懂国内环境**”。

## 拓展思考
1. 如果第三方脚本**不支持 ESM**,Grunt 可在构建期用 **grunt-wrap** 把库包成 UMD,再动态插入 `script.async`,避免重复请求。  
2. 对于**微信小程序 WebView**,需把统计域名加入业务域名校验名单,否则 `sendBeacon` 会被拦截;此时可降级为**图片打点**并限制大小 1×1 px。  
3. 当页面需要**秒开率**考核时,可把第三方脚本拆成**关键**与**非关键**两级:关键脚本用 `rel=preload` 提升优先级,非关键脚本用 `requestIdleCallback` 延迟到**首次渲染**之后,Grunt 通过 `grunt-critical` 自动提取关键脚本列表并内联。  
4. 若团队已迁移到 Vite/Rollup,可用 Grunt 作为**遗留任务兜底**:把新构建产物拷贝到 Grunt 目录,继续用原有 CI 流程发布,实现**渐进式迁移**,降低面试官对你“技术栈老旧”的顾虑。