perfy
v2.1.0
Published
A tiny, zero-dependency utility for measuring code execution time in high-resolution real time. Works in Node.js, browsers, Deno and Bun.
Maintainers
Readme
perfy
This module is ESM 🔆. Please read this.
A tiny, zero-dependency utility for measuring code execution time in high-resolution real time — named timers, one-shot exec() wrappers, and a rich elapsed-time result. Runs in Node.js, browsers, Deno and Bun.
import { perfy } from 'perfy';
perfy.start('loop');
// ...heavy work...
console.log(perfy.end('loop').time); // -> 1.235 (sec.)[!NOTE] Perfy picks the most precise clock available:
process.hrtime.bigint()on Node.js (exact integer nanoseconds), falling back toperformance.now()elsewhere. The elapsed time is computed with integer-nanosecond math, so it never accumulates floating-point drift. Wall-clockstartTime/endTimestamps come fromDate.now().
Installation
npm install perfyUsage
Call perfy.start('name') to create a timer and mark its start, then perfy.end('name') to get the elapsed-time result. By default the instance is destroyed once ended.
import { perfy } from 'perfy';
perfy.start('loop-stuff');
// ...some heavy stuff here...
const result = perfy.end('loop-stuff');
console.log(result.time); // -> 1.459 (sec.)...or wrap the work in exec() and let Perfy time it for you:
perfy.exec('async-stuff', (done) => {
// ...some heavy async stuff here...
const result = done();
console.log(result.time); // -> 1.459 (sec.)
});The exported perfy is a shared singleton — the simplest way to use the library. When you want an isolated registry (or a custom clock, e.g. in tests), construct your own:
import { Perfy } from 'perfy';
const perfy = new Perfy();Exports
Everything is a named export (there is no default export):
import {
perfy, // shared Perfy singleton — the simplest entry point
Perfy, // class — construct an isolated registry: new Perfy(clock?)
PerfyItem, // a single timing instance (advanced / typing)
PerfyError, // Error subclass thrown on failure, with a `.code`
createNanoClock, // build a NanoClock from given host objects
defaultNanoClock // the NanoClock selected for this environment
} from 'perfy';
import type {
IPerfyResult, // the elapsed-time result object
IPerfyMeasurement, // the disposable handle returned by measure()
NanoClock, // () => bigint monotonic nanosecond clock
PerfyErrorCode, // 'NAME_REQUIRED' | 'NO_INSTANCE' | 'NOT_STARTED' | 'INVALID_CALLBACK' | 'NO_CLOCK'
DoneFn, // the `done` callback passed to a callback-style exec task
SyncTask,
AsyncTask,
PromiseTask,
PerfyTask // SyncTask | AsyncTask | PromiseTask
} from 'perfy';API
Every method that takes a name throws a PerfyError with code NAME_REQUIRED when it is empty.
| Method | Returns | Description |
| ------ | ------- | ----------- |
| start(name, autoDestroy?) | Perfy | Creates a new instance under name and marks its start time. Reusing a name overwrites it. autoDestroy (default true) drops the instance when end() is called. Chainable. |
| end(name) | IPerfyResult | Ends the instance and returns the elapsed-time result. If autoDestroy was left on, the instance is removed right after. Calling end() again on a kept instance returns the same cached result. Throws NO_INSTANCE if no such instance exists. |
| lap(name) | IPerfyResult | Records a split — the elapsed time since the previous lap (or since start for the first) — then advances the lap marker. The instance keeps running; end() still reports the total from start. Throws NO_INSTANCE if no such instance exists. |
| measure(name, onEnd?) | IPerfyMeasurement | Starts a kept instance and returns a disposable whose [Symbol.dispose] ends it — so a using declaration times its enclosing scope. The result stays retrievable via result(name); pass onEnd to receive it immediately. Disposing twice is a no-op. |
| exec([name,] fn) | IPerfyResult | Promise<IPerfyResult> | Perfy | Times the execution of fn, picking the mode from the task itself. Synchronous (fn returns a non-thenable) → ended automatically, result returned. Promise-returning (fn returns a promise) → awaited, resolves to the result (a rejection is propagated). Callback-style (fn(done) declares a done argument) → must call done() to end; returns the Perfy instance immediately. Pass a name to keep the instance. Throws INVALID_CALLBACK if fn is not a function. |
| result(name) | IPerfyResult | null | The stored result of a kept, ended instance — or null if it does not exist or has not ended yet. |
| exists(name) | boolean | Whether an instance currently exists under name. false once an auto-destroyed instance has ended. |
| names() | string[] | Names of all existing instances. |
| count() | number | Number of existing instances. |
| destroy(name) | Perfy | Destroys the instance under name, if any. Chainable. |
| destroyAll() | Perfy | Destroys all existing instances. Chainable. |
The Result Object
end() (and exec() / result()) return an IPerfyResult — every field is a plain number/string, so the object is safe to JSON.stringify.
| Property | Type | Description |
| -------- | ---- | ----------- |
| name | string | Name of the instance ('' for an unnamed exec()). |
| time | number | Full elapsed time in seconds (float, 3 decimals). e.g. 1.235 |
| milliseconds | number | Full elapsed time in milliseconds (float). e.g. 1235.125 |
| nanoseconds | number | Full elapsed time in nanoseconds. e.g. 1235125283 |
| summary | string | Human-readable shorthand. e.g. 'loop: 1.235 sec.' |
| startTime | number | UTC wall-clock time (ms) at start, via Date.now(). e.g. 1533302465251 |
| endTime | number | UTC wall-clock time (ms) at end, via Date.now(). e.g. 1533302466486 |
Examples
Reading the elapsed time in different units:
perfy.start('metric');
// ...
const r = perfy.end('metric');
console.log(`${r.time} sec.`); // -> 1.234 sec.
console.log(`${r.milliseconds} ms.`); // -> 1234.567 ms.
console.log(r.summary); // -> metric: 1.234 sec.Auto-destroy (default):
perfy.start('metric').count(); // -> 1
perfy.end('metric');
perfy.count(); // -> 0 (destroyed on end)Keep the instance (disable autoDestroy):
perfy.start('metric', false);
perfy.end('metric').time; // -> 0.123
perfy.exists('metric'); // -> true (kept)
perfy.result('metric'); // -> the same result objectTiming a synchronous function — exec() returns the result directly:
const result = perfy.exec(() => {
// sync work
});
console.log(result.time);Timing a promise / async function — exec awaits it and resolves to the result:
const result = await perfy.exec('fetch', async () => {
await fetch('https://example.com');
});
console.log(result.time);Timing a callback-style async function — call done() when finished:
perfy.exec((done) => {
setTimeout(() => {
const result = done();
console.log(result.time);
}, 1000);
});Laps — record splits within one running timer:
perfy.start('pipeline');
loadData();
console.log('load:', perfy.lap('pipeline').time);
transform();
console.log('transform:', perfy.lap('pipeline').time);
console.log('total:', perfy.end('pipeline').time);Scope timing with using — the timer ends automatically when the block exits:
{
using _ = perfy.measure('block', (r) => console.log(r.time));
// ...work...
} // ended hereNamed exec() keeps the instance for later retrieval:
perfy.exec('async-op', (done) => {
done();
});
perfy.exists('async-op'); // -> true
perfy.result('async-op'); // -> the result objectDestroy everything:
perfy.destroyAll().count(); // -> 0Errors
Every failure throws a PerfyError — an Error subclass carrying a stable, machine-readable code (NAME_REQUIRED, NO_INSTANCE, NOT_STARTED, INVALID_CALLBACK, NO_CLOCK):
import { perfy, PerfyError } from 'perfy';
try {
perfy.end('never-started');
} catch (err) {
if (err instanceof PerfyError && err.code === 'NO_INSTANCE') {
// handle it
}
}Tests & Quality
100% test coverage (statements, branches, functions, lines) and a 100% Stryker mutation score, run across Node.js 22 & 24 in CI.
Changelog
See CHANGELOG.md. v2 is a breaking release (ESM-only, universal clock, streamlined result object) — the migration notes live there.
Related
- tasktimer — An accurate timer utility for running periodic tasks on the given interval ticks or dates.
License
© 2026, Onur Yıldırım. MIT License.
