如何对第三方脚本延迟加载并统计实际执行时间
解读
在国内前端面试里,这道题表面问“延迟加载”和“性能统计”,实则考察三点:
- 是否理解 Grunt 构建阶段与运行时阶段的职责边界;
- 能否用 Grunt 插件体系提前生成延迟加载骨架代码与埋点钩子,而不是在构建期直接做“延迟”或“统计”;
- 是否熟悉国内网络环境(CDN 不稳定、域名备案、HTTPS 混合内容限制)并给出可落地的灰度方案。
回答时务必先区分“构建时”与“运行时”,再给出可复制的 Gruntfile 片段,否则会被面试官认为“只会嘴上说,不会写配置”。
知识点
- Grunt 构建期职责:文件预处理、代码注入、版本哈希、雪碧图合并,不负责运行时调度。
- 延迟加载运行时方案:
- 动态 import()(ESM)
- IntersectionObserver + preload/prefetch
- requestIdleCallback 空闲调度
- 性能统计 API:
- PerformanceObserver 监听 resource timing
- PerformanceResourceTiming.duration 获取脚本网络+编译耗时
- mark/measure 自定义打点
- Grunt 插件:
- grunt-string-replace:在 html 里注入埋点 snippet
- grunt-contrib-uglify:生成 loader 骨架并压缩
- grunt-filerev:给第三方脚本加哈希,避免缓存干扰统计
- 国内合规细节:
- 统计域名需备案且接入工信部 CDN 白名单
- 避免使用国外 Google Analytics 域名,否则会被防火墙重置连接
- 灰度发布需兼容微信 X5 内核与UC 内核,二者对
type="module"支持度不同
答案
- 在 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>'
}]
}
}
}
- 在业务代码里调用:
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 流程发布,实现**渐进式迁移**,降低面试官对你“技术栈老旧”的顾虑。