benchmarking
SkillDev toolsHow to write accurate and meaningful benchmarks for JavaScript/TypeScript code using mitata. Covers JIT pitfalls (dead code elimination, loop invariant code motion), garbage collection noise, and reliable measurement methodology. Use this skill whenever benchmarking, measuring performance, or comparing implementation alternatives.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the benchmarking skill
What this skill tells your AI
The instructions your AI receives, as published by gilsmt/cache-app in .agents/skills/benchmarking/SKILL.md and read by ahel’s review.
Creating accurate and meaningful benchmarks requires careful attention to how modern JavaScript engines optimize code. This covers essential concepts and best practices to ensure your benchmarks measure actual performance characteristics rather than optimization artifacts.
General Principles
- Benchmark before optimizing: always measure code performance with real benchmarks first; never optimize without measuring.
- Profile in realistic environments: engine behavior differs (V8, JSC, SpiderMonkey) and devtools instrumentation changes results. Benchmark in the actual environment the code will run in.
- Focus on bottlenecks: optimize the code sections that consume the most runtime, not just any slow-looking code.
- Prioritize readability: write clear, maintainable code first, and only optimize if benchmarks show a real need.
- Document optimization rationale: when changing code for performance, include comments explaining the benchmark results and reasoning.
- Doubt your results: if a change "runs 100x faster", verify it in production before trusting the benchmark.
Setup with mitata
Use mitata for benchmarking tooling. See its README for the full API; if the context7 MCP server is configured, use it to look up mitata docs.
import { bench, run, do_not_optimize } from "mitata";
bench("fibonacci(30)", () => fibonacci(30));
await run();
- Run Node benchmarks with
node --expose-gcso mitata can control garbage collection (Bun exposes GC by default). - Use dedicated, quiet hardware; install the optional
@mitata/counterspackage for IPC/cache statistics. - Let the JIT warm up — mitata handles warmup internally. Never hand-roll timing loops (e.g., N iterations around
Date.now()); JIT compilation during the measured region invalidates results. - Report distributions, not just averages. Mitata shows min/max, p75/p99 and flags high variance; wrap benchmarks in
summary(),boxplot(), orlineplot()for comparisons. - Use a clean browser profile when measuring in the browser (extensions, especially React devtools, distort results).
Dead Code Elimination
JIT compilers remove code with no observable effects. If a benchmark's result is unused, the engine can optimize the call away entirely; mitata flags this with a ! warning.
// ❌ Bad: the allocation is unused and can be eliminated by the JIT
bench(() => new Array(0));
// ✅ Good: do_not_optimize() emits code with an observable side effect
bench(() => do_not_optimize(new Array(0)));
Garbage Collection Pressure
For benchmarks with significant allocations, GC pauses can dominate timings. Control GC frequency for consistent results:
// ❌ Bad: unpredictable gc pauses (and, as written, this is also DCE-eligible — see above)
bench(() => {
const bigArray = new Array(1000000);
});
// ✅ Good: gc before each (batch-)iteration
bench(() => {
const bigArray = new Array(1000000);
}).gc("inner"); // run gc before each iteration
GC modes: false (never), "once" (after warmup; the default), "inner" (after warmup and before each batch-iteration).
Loop Invariant Code Motion
Engines hoist loop-invariant computations out of measurement loops, distorting results. Use computed parameters so fresh values are produced per iteration and the computation cannot be hoisted or constant-folded:
bench(function* (ctx) {
const str = "abc";
const substr = ctx.get("substr");
// ❌ Likely distorted: the JIT may hoist or fold this call out of the loop
yield () => str.includes(substr);
// ✅ Good: computed parameters prevent JIT hoisting/constant folding
yield {
[0]() {
return str;
},
[1]() {
return substr;
},
bench(str, substr) {
return do_not_optimize(str.includes(substr));
},
};
}).args("substr", ["c"]);
Whether a particular call gets hoisted or folded varies by engine and version; computed parameters make results engine-independent by guaranteeing per-iteration values.
Browser Benchmarks
When benchmarking DOM code, avoid layout thrashing — interleaved reads and writes invalidate measurements:
// ❌ Bad: alternating reads and writes (each read forces layout)
for (const el of elements) {
el.style.height = `${el.offsetHeight * 2}px`;
}
// ✅ Good: batch reads first, then writes
const heights = elements.map((el) => el.offsetHeight);
for (let i = 0; i < elements.length; i++) {
elements[i].style.height = `${heights[i] * 2}px`;
}
document.getElementByIdis already highly optimized in modern browsers; don't cache DOM references preemptively — measure first.
References
- mitata — the benchmarking library used above
- Optimizing JavaScript (romgrk) — engine-level techniques and case against micro-benchmark-driven optimization
- LLVM Benchmarking tips — general micro-benchmark methodology
Signals
- GitHub stars
- 26
- Forks
- 1
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
benchmarking-gilsmt- Source
- github.com/gilsmt/cache-app