使用 grunt-benchmark 对比两种算法在浏览器端的耗时

解读

面试官并非想听你背诵“npm install grunt-benchmark”就完事,而是考察三件事:

  1. 能否把浏览器端这一限定条件落地——Grunt 默认跑在 Node,必须把测试代码搬到 Chrome/Edge 等真实环境;
  2. 能否用 Grunt 的“任务链”思想把编译、 Serve、Benchmark、采集、报告串成一键流程;
  3. 能否给出可信的耗时对比方案(多次采样、置信区间、异常值过滤),而不是“跑一次谁快就信谁”。
    在国内面试场景里,这道题常作为“构建+性能”综合题出现,答得浅会被追问“如何排除 JIT 扰动”,答得深可直接拉高评级。

知识点

  1. grunt-benchmark 本质:把 benchmark.js 包装成 Grunt 多任务,每个子任务即一个采样组
  2. 浏览器环境落地:
    • 用 grunt-contrib-connect 起本地 HTTPS 服务(自签证书需信任,否则最新 Chrome 把高性能 API 禁掉);
    • 用 grunt-contrib-watch + grunt-contrib-copy 把算法文件、Benchmark 模板、Source Map 实时同步到 dist;
    • 用 puppeteer 或 playwright 无头脚本驱动真实浏览器打开页面,采集 window.BENCHMARK_RESULT
  3. 统计可信性:
    • 采样次数 ≥ 100,剔除前 5% 与后 5% 异常值;
    • 计算 95% 置信区间,若两区间无重叠才可下结论;
    • 强制垃圾回收:--js-flags='--expose-gc',采样前手动 global.gc()
  4. 工程化细节:
    • 把算法写成 UMD 模块,同一份代码既被 Node 单元测试,也被浏览器 Benchmark;
    • 用 grunt-env 区分 dev|ci 两套配置,CI 环境采样次数翻倍,本地开发减少等待;
    • 报告输出 markdown 表格到 .benchmark/report.md,并上传至 GitLab Pages,MR 阶段强制 diff 报告,防止性能回退;
  5. 国内特殊点:
    • 公司内网 Nexus 源常把 puppeteer 的 Chromium 下载墙掉,需提前上传 .local-chromium 到 CDN,在 .npmrc 里指定 puppeteer_skip_chromium_download=true
    • 若面试国企或金融单位,浏览器版本锁定在 360 或 IE11,需降级到 benchmark.js 2.1.x 并垫片 Promise

答案

  1. 初始化
    npm i -D grunt grunt-benchmark grunt-contrib-connect grunt-contrib-copy grunt-env puppeteer benchmark
    
  2. Gruntfile.js 核心片段
    module.exports = function(grunt) {
      grunt.initConfig({
        env: { ci: { NODE_ENV: 'ci' } },
        copy: { bench: { files: [{ expand:true, cwd:'src', src:'**/*.js', dest:'dist/' }] } },
        connect: {
          bench: { options: { protocol:'https', port:7443, base:'dist', key:grunt.file.read('certs/server.key'), cert:grunt.file.read('certs/server.crt') } }
        },
        benchmark: {
          options: {
            displayResults: true,
            output: '.benchmark/report.json'
          },
          browser: {
            src : ['dist/algo1.js', 'dist/algo2.js'],
            dest: '.benchmark/report.json'
          }
        }
      });
    
      grunt.registerTask('chrome', '驱动浏览器跑采样', async function() {
        const done = this.async();
        const browser = await require('puppeteer').launch({ headless:true, args:['--enable-precise-memory-info','--js-flags=--expose-gc'] });
        const page = await browser.newPage();
        await page.goto('https://localhost:7443/bench.html');
        await page.waitForFunction('window.BENCHMARK_DONE === true', { timeout: 120000 });
        const result = await page.evaluate('window.BENCHMARK_RESULT');
        grunt.file.write('.benchmark/chrome.json', JSON.stringify(result, null, 2));
        await browser.close();
        done();
      });
    
      grunt.registerTask('stat', '计算置信区间', function() {
        const data = grunt.file.readJSON('.benchmark/chrome.json');
        function ci(arr, confidence=0.95) {
          const mean = arr.reduce((a,b)=>a+b)/arr.length;
          const variance = arr.reduce((a,b)=>a+Math.pow(b-mean,2),0)/(arr.length-1);
          const t = 1.96; // 大样本近似
          const margin = t * Math.sqrt(variance/arr.length);
          return [mean - margin, mean + margin];
        }
        const algo1 = ci(data.algo1);
        const algo2 = ci(data.algo2);
        const faster = (algo1[1] < algo2[0]) ? 'algo1' : (algo2[1] < algo1[0]) ? 'algo2' : 'tie';
        grunt.log.ok(`95% 置信区间 algo1: ${algo1[0].toFixed(2)}~${algo1[1].toFixed(2)} ms, algo2: ${algo2[0].toFixed(2)}~${algo2[1].toFixed(2)} ms`);
        grunt.log.ok(`结论:${faster} 显著更快`);
      });
    
      grunt.registerTask('perf', ['copy:bench', 'connect:bench', 'chrome', 'stat']);
      grunt.registerTask('perf:ci', ['env:ci', 'copy:bench', 'connect:bench', 'chrome', 'stat']);
    };
    
  3. bench.html(dist 内)
    引入 benchmark.js、algo1、algo2,各跑 100 次,把结果挂到 window.BENCHMARK_RESULT,并设置 window.BENCHMARK_DONE = true
  4. 运行
    npx grunt perf
    
    终端会打印 95% 置信区间与显著性结论;CI 环境使用 npx grunt perf:ci 提高采样。
  5. 交付物
    • .benchmark/report.json:原始采样数据;
    • .benchmark/chrome.json:浏览器端聚合结果;
    • 终端结论可直接贴到 Pull Request 描述,性能回退超过 5% 拒绝合并

拓展思考

  1. 若算法依赖 WebAssembly,如何排除编译耗时?
    答:把 Instantiate 阶段提前到预热,Benchmark 只测核心 instance.exports.compute();用 performance.mark() 单独记录编译耗时,报告里分开展示。
  2. 如何在同一条流水线里对比 内存占用
    答:在 puppeteer 里调用 performance.memory.usedJSHeapSize,采样前后做差值;注意 --enable-precise-memory-info 必须开启,且页面需 iframe 隔离防止交叉污染。
  3. 如果公司强制要求 低版本 Chrome 45(很多国产定制内核),benchmark.js 的 clock 模块回退到 Date.now(),精度只有 1 ms,此时应:
    • 把单次任务放大到 ≥ 50 ms,通过循环放大再除以循环次数;
    • 使用 performance.now() polyfill(基于 chrome.Interval),但需注入扩展程序,CI 权限申请流程较长,要提前与运维报备。