benchmarking

SkillDev tools

How 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.

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-gc so mitata can control garbage collection (Bun exposes GC by default).
  • Use dedicated, quiet hardware; install the optional @mitata/counters package 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(), or lineplot() 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.getElementById is already highly optimized in modern browsers; don't cache DOM references preemptively — measure first.

References

Signals

GitHub stars
26
Forks
1
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
benchmarking-gilsmt
Source
github.com/gilsmt/cache-app