使用 grunt-replace 注入 Web Vitals 监控代码

解读

在国内前端面试中,面试官提出“用 grunt-replace 注入 Web Vitals 监控代码”,并不是单纯考察“会不会用插件”,而是验证候选人能否在遗留 Grunt 体系内优雅地引入现代化性能监控能力
核心考点有三层:

  1. 对 Grunt 任务流与插件机制的熟悉度——能否在不破坏原有构建顺序的前提下插入新任务;
  2. 对 Web Vitals 官方脚本注入最佳实践的理解——既要保证采集代码轻量、无阻塞、可配置,又要避免与业务代码冲突;
  3. 对国内网络与合规场景的落地经验——例如如何把数据上报到自研网关或阿里云 SLS、腾讯云 RUM,并做采样率、域名白名单、用户隐私脱敏等处理。

如果候选人只回答“装 grunt-replace → 写字符串替换”,会被认为“能跑但不够生产级”;只有给出**“任务分级、占位符设计、sourcemap 友好、灰度采样、一键回滚”** 的完整思路,才能拿到高分。

知识点

  1. grunt-replace 的三种模式:
    • plain 字符串直接替换,最快但易误伤;
    • regexp 正则捕获,适合精准定位;
    • function 动态函数,可运行时读取版本号、Git commit、灰度配置等。
  2. Web Vitals 库加载方式:
    • 官方 web-vitals 模块仅 1.7 kB(gz),需 type="module"
    • 若需兼容旧浏览器,可改用 web-vitals-base.js 并加 nomodule 兜底;
    • 采集后通过 navigator.sendBeacon 发送到 /metrics 接口,失败时降级为 1×1 gif 以绕过企业网关拦截。
  3. Grunt 任务编排:
    • grunt.registerTask('build', ['clean', 'replace:injectVitals', 'uglify', 'rev']); 必须保证注入在压缩与加 hash 之前,否则替换标记会被破坏;
    • 使用 grunt-contrib-watch 开启 livereload 时,需排除监控插入后的临时文件,防止无限循环。
  4. 国内合规细节:
    • 采集指标只能包含性能条目,严禁携带用户身份信息;
    • 上报域名需加入工信部备案号隐私政策链接,否则企业安全审计会被 block;
    • 支持采样率配置化,如 window.VITALS_SAMPLE = Math.random() < 0.1,减少带宽费用。

答案

  1. 安装与初始化
npm i -D grunt-replace web-vitals@3
  1. Gruntfile.js 关键片段
module.exports = function(grunt) {
  grunt.initConfig({
    replace: {
      injectVitals: {
        options: {
          patterns: [{
            match: /<!--\s*vitals\s*-->/,          // 1. 占位符必须唯一且与业务注释区分
            replacement: function() {
              const fs = require('fs');
              const code = fs.readFileSync('./build-inject/vitals.js', 'utf-8')
                         .replace('{{SAMPLING}}', grunt.option('sampling') || 0.1)
                         .replace('{{ENDPOINT}}',  process.env.VITALS_ENDPOINT || '/metrics');
              return `<script>${code}</script>`;
            }
          }]
        },
        files: [{
          expand: true,
          cwd: 'dist/',
          src: '**/*.html',
          dest: 'dist/'
        }]
      }
    }
  });

  grunt.loadNpmTasks('grunt-replace');
  grunt.registerTask('build', ['clean', 'replace:injectVitals', 'uglify', 'rev']);
};
  1. build-inject/vitals.js(精简后约 1 kB)
(function(s){
  if(Math.random()>s)return;
  import('https://cdn.jsdelivr.net/npm/web-vitals@3/dist/web-vitals.base.iife.js').then(function(m){
    function send({name,value,id}){
      var data={n:name,v:value,i:id,url:location.href,ts:Date.now()};
      navigator.sendBeacon('{{ENDPOINT}}', JSON.stringify(data)) ||
        new Image().src='{{ENDPOINT}}?gif=1&d='+encodeURIComponent(JSON.stringify(data));
    }
    m.onCLS(send);m.onFID(send);m.onLCP(send);m.onFCP(send);m.onTTFB(send);
  });
})({{SAMPLING}});
  1. 灰度与回滚
# 全量
grunt build --sampling=1
# 10% 灰度
grunt build --sampling=0.1
# 紧急回滚:重新打包,replace 任务把占位符替换为空串即可
  1. 验证
    本地起 nginx,访问页面,DevTools Network 面板应看到 /metrics 请求带 gif=1 兜底,且响应 204;Lighthouse 中 Performance 分数不应因注入下降超过 3 分。

拓展思考

  1. 如果公司已将构建迁移到 Vite,但老项目仍需 Grunt 维护,可共用同一套 vitals.js 注入脚本,通过 vite-plugin-htmlgrunt-replace 同时引用,保证指标口径一致。
  2. 对于小程序内嵌 WebView 场景,Web Vitals 部分指标(如 CLS)会失真,需在注入脚本里加 userAgent 判断,关闭不适用的指标,避免误导运营。
  3. 当业务采用多页应用 + 服务端拼接模板(如 Java Velocity)时,占位符应改为服务器注释 #*vitals*#,Grunt 任务仅负责预编译静态片段,最终注入由后端灰度开关控制,实现“前后端联合灰度”。