benchik
v1.3.0
Published
A tiny benchmark library with via a simple `console.log` and `console.group`, works nicely in a browser and terminal
Maintainers
Readme
benchik
A tiny benchmark library using console.log and console.group.
Works equally well in the browser and terminal - zero dependencies, ~200 lines of code.
Use this library if you need a quick, fast and reliable peek to benchmark results.
import { bench } from "benchik"
bench("Array.from", () => { Array.from({ length: 1000 }) })
bench("for loop", () => { const a = []; for (let i = 0; i < 1000; i++) a.push(i) })Install
npm i benchikBenchik ships as raw TypeScript, import it directly with any ESM-compatible runtime (Bun, tsx, ts-node, or a bundler like Vite/esbuild).
Usage
Basic benchmark
bench(() => Math.sqrt(Math.random()))
// → [1.40ns] Math.sqrt(Math.random())
bench("sqrt 1000x", () => { for (let i = 0; i < 1000; i++) Math.sqrt(i) })
// → [500.00ns] sqrt 1000xGrouped benchmarks
Groups show which test was fastest and how much slower the others were:
using g = bench.group("Array iteration")
bench("for", () => { for (let i = 0; i < 1e4; i++) })
bench("forEach", () => { Array(1e4).fill(0).forEach(x => x) })
bench("for..of", () => { for (const x of Array(1e4).fill(0)) })Array iteration
[5.00ms] for [fastest]
[8.30ms] forEach [+1.66x]
[5.10ms] for..of [+1.02x]Fresh data per iteration
Prevent state cross-contamination between runs with fresh(), each iteration gets its own copy:
using g = bench.group("Array push")
const f = g.fresh(() => ({ array: [1, 2, 3] }))
bench("push", () => { f.array.push(4) })
bench("spread", () => { const x = [...f.array, 4] })Assertions
Verify that every iteration returns the same value:
using g = bench.group("Sorting")
g.assert = [1, 2, 3, 4, 5]
bench("unsorted", () => [3, 1, 5, 2, 4])
bench("sorted", () => [3, 1, 5, 2, 4].sort())
// → [40.00ns] [✗] unsorted
// → [50.00µs] [✓] sortedDeterministic randomness
Use bench.random() to generate the same pseudorandom sequence across warmup and measurement phases:
bench("rand test", () => {
const seed = bench.random()
// `seed` is deterministic, same in warmup and measurement.
})Wait for JIT compilation
// Declare static variables and compute things.
const d1 = []
const d2 = [2, 3, 4]
// Wait for JIT to make sure the first benchmark doesn't get slowed down.
await bench.untilCompiled()
bench("first test", () => { /* ... */ })Black hole
The JS engines may optimize pure functions that doesn't really do anything. To avoid skipping over iterations or function execution, always return the output of iterations.
For example, if you're benchmarking for loop iteration speed, you should setup some quick calculations like summing many numbers or pushing numbers to array and then returning them. If you don't return anything and just do empty iterations, the engine might skip them.
const MAX = 1_000
using g = bench.group("loops")
g.assert = MAX
let r = 0
bench("for", () => {
r = 0
for (let i = 0; i < MAX; i++) r += 1
return r
})Even though these calculations introduce overhead, it's very often tolerable
Relativeness & Fairness (advice)
When measuring speed, the actual numbers matter, but usually it's more important how an option/approach/solution is better/faster over another one.
So remember to make sure your benchmarks are fair to each other, e.g. init same amount of variables, do the same setups. It should be clear what you're actually measuring and comparing between.
Full Example
import { bench } from "benchik"
class asd { }
const arrD = []
const MAX = 1_000
const finalResult = Array.from({ length: MAX }).map(() => new asd)
await bench.untilCompiled()
{
using _ = bench.group("Array Populate")
bench("dynamic (push)", () => {
const array = []
for (let i = 0; i < MAX; i++) array.push(new asd)
return array
})
bench("dynamic (no push)", () => {
const array = []
for (let i = 0; i < MAX; i++) array[i] = new asd
return array
})
bench("dynamic (no push via length)", () => {
const array = []
for (let i = 0; i < MAX; i++) array[array.length] = new asd
return array
})
bench("pre-allocate", () => {
const array = Array(MAX)
for (let i = 0; i < MAX; i++) array[i] = new asd
return array
})
bench("reusing", () => {
const array = arrD.fill(null, 0, MAX)
for (let i = 0; i < MAX; i++) array[i] = new asd
return array
})
}Then simply run the file with your JS runtime, for example:
bun benchmark/array.jsAnd you will have result like this:
Array Populate
[038.272µs] [+1.46x] dynamic (push)
[034.935µs] [+1.33x] dynamic (no push)
[035.316µs] [+1.34x] dynamic (no push via length)
[029.726µs] [+1.13x] pre-allocate
[026.259µs] [fastest] reusingThis example is also placed at ./examples/array.js.
Browser usage
Benchik works natively in the browser, just import from a CDN:
const { bench } = await import("https://esm.sh/benchik")using _ = bench.group("Array Populate")
bench("dynamic (push)", () => {
const array = []
for (let i = 0; i < MAX; i++) array.push(new asd)
return array
})
bench("dynamic (no push)", () => {
const array = []
for (let i = 0; i < MAX; i++) array[i] = new asd
return array
})Or via module script:
<script type="module">
import { bench } from "https://esm.sh/benchik"
using g = bench.group("DOM tests")
bench("createElement", () => document.createElement("div"))
bench("querySelector", () => document.querySelector("body"))
</script>API
bench(label?, callback)
Runs a 50ms warmup phase, then measures the callback for 50ms and reports the median iteration time.
bench.group(label)
Creates a console group. Returns a Disposable object (works with using declarations).
| Property | Description |
|----------|-------------|
| fresh(factory) | Returns fresh values per iteration |
| assert | Expected return value for all iterations |
bench.random()
Returns a deterministic pseudorandom number (mulberry32). Reseeded to 0xDEADBEEF before both warmup and measurement phases for reproducibility.
bench.untilCompiled()
Returns a promise that resolves after 200ms, giving the runtime time to JIT-compile objects before the first benchmark runs.
Agent Instruction
You can adopt this instruction to allow your agent to run benchmarks from time to time and have better insights for you.
---
description: Asses code speed, do benchmarks.
---
- Read files in `github.com/FrameMuse/benchik/tree/main/examples`, these are benchmarks of a real project, results of which can be used to make considerations, include in the task or show to a user.
- To run the benchmarks, run `file.js`.
- To create a permanent benchmark, create a file (`.js` format, ESNext style) in a desired path.
- To create a temporary benchmark, create a `.js` file in `/tmp/benchiky` directory, run `/tmp/benchiky/file.js` and use the results.
Use installed JS runtime: `bun` or `node` or `deno`.
(Bun only) Note: make sure to import a benchmark suite, use `bun link benchik` once and then import like usual `import { bench } from "benchik"`.Browser & terminal
Benchik uses only universally available APIs:
performance.now(): the high-resolution monotonic clock available in all browsers and Node.js/Bun/Denoconsole.log/console.group/console.groupEnd: native console output everywhere- ANSI escape codes for color
No DOM, no Node-specific APIs, no runtime-specific imports. The same code runs identically in both environments.
Why it's reliable
- Warmup phase: the callback runs for 50ms before measurement begins. This allows JIT compilation to stabilize and CPU caches to warm, so the first measurement isn't artificially slow.
- Deterministic random: the mulberry32 randomizer is reset with the same value before both warmup and measurement, ensuring identical random inputs and eliminating a benchmark variance.
- Fresh data pattern:
g.fresh()creates a new data object for each iteration, preventing state mutations from one run affecting the cost of the next (a common pitfall in JS microbenchmarks). - Median aggregation: default aggregation uses the median, gives a more representative measurement than the mean (average).
- Runs each benchmark either for 50ms or 50_000 times, which works great both for very slow and very fast programs.
- No dependencies: zero third-party code means zero risk of dependency-related performance noise or version conflicts.
Why you may not need it
- Benchmarks are measured in perfect conditions, which works most of the time, but might not be what you need.
- This library doesn't measure time deviations, only final measurement, stable across multiple runs.
- This library doesn't measure memory usage.
