使用 grunt-benchmark 对比两种算法在浏览器端的耗时
解读
面试官并非想听你背诵“npm install grunt-benchmark”就完事,而是考察三件事:
- 能否把浏览器端这一限定条件落地——Grunt 默认跑在 Node,必须把测试代码搬到 Chrome/Edge 等真实环境;
- 能否用 Grunt 的“任务链”思想把编译、 Serve、Benchmark、采集、报告串成一键流程;
- 能否给出可信的耗时对比方案(多次采样、置信区间、异常值过滤),而不是“跑一次谁快就信谁”。
在国内面试场景里,这道题常作为“构建+性能”综合题出现,答得浅会被追问“如何排除 JIT 扰动”,答得深可直接拉高评级。
知识点
- grunt-benchmark 本质:把 benchmark.js 包装成 Grunt 多任务,每个子任务即一个采样组;
- 浏览器环境落地:
- 用 grunt-contrib-connect 起本地 HTTPS 服务(自签证书需信任,否则最新 Chrome 把高性能 API 禁掉);
- 用 grunt-contrib-watch + grunt-contrib-copy 把算法文件、Benchmark 模板、Source Map 实时同步到 dist;
- 用 puppeteer 或 playwright 无头脚本驱动真实浏览器打开页面,采集
window.BENCHMARK_RESULT;
- 统计可信性:
- 采样次数 ≥ 100,剔除前 5% 与后 5% 异常值;
- 计算 95% 置信区间,若两区间无重叠才可下结论;
- 强制垃圾回收:
--js-flags='--expose-gc',采样前手动global.gc();
- 工程化细节:
- 把算法写成 UMD 模块,同一份代码既被 Node 单元测试,也被浏览器 Benchmark;
- 用 grunt-env 区分
dev|ci两套配置,CI 环境采样次数翻倍,本地开发减少等待; - 报告输出 markdown 表格到
.benchmark/report.md,并上传至 GitLab Pages,MR 阶段强制 diff 报告,防止性能回退;
- 国内特殊点:
- 公司内网 Nexus 源常把
puppeteer的 Chromium 下载墙掉,需提前上传.local-chromium到 CDN,在.npmrc里指定puppeteer_skip_chromium_download=true; - 若面试国企或金融单位,浏览器版本锁定在 360 或 IE11,需降级到 benchmark.js 2.1.x 并垫片
Promise。
- 公司内网 Nexus 源常把
答案
- 初始化
npm i -D grunt grunt-benchmark grunt-contrib-connect grunt-contrib-copy grunt-env puppeteer benchmark - 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']); }; - bench.html(dist 内)
引入 benchmark.js、algo1、algo2,各跑 100 次,把结果挂到window.BENCHMARK_RESULT,并设置window.BENCHMARK_DONE = true。 - 运行
终端会打印 95% 置信区间与显著性结论;CI 环境使用npx grunt perfnpx grunt perf:ci提高采样。 - 交付物
.benchmark/report.json:原始采样数据;.benchmark/chrome.json:浏览器端聚合结果;- 终端结论可直接贴到 Pull Request 描述,性能回退超过 5% 拒绝合并。
拓展思考
- 若算法依赖 WebAssembly,如何排除编译耗时?
答:把 Instantiate 阶段提前到预热,Benchmark 只测核心instance.exports.compute();用performance.mark()单独记录编译耗时,报告里分开展示。 - 如何在同一条流水线里对比 内存占用?
答:在 puppeteer 里调用performance.memory.usedJSHeapSize,采样前后做差值;注意--enable-precise-memory-info必须开启,且页面需 iframe 隔离防止交叉污染。 - 如果公司强制要求 低版本 Chrome 45(很多国产定制内核),benchmark.js 的
clock模块回退到Date.now(),精度只有 1 ms,此时应:- 把单次任务放大到 ≥ 50 ms,通过循环放大再除以循环次数;
- 使用
performance.now()polyfill(基于chrome.Interval),但需注入扩展程序,CI 权限申请流程较长,要提前与运维报备。