loadometer
v0.2.8
Published
Profile how long your app spends loading modules (require and import, Node and Bun) as folded stacks you can render into a flame graph.
Maintainers
Readme
loadometer
See where your app spends time loading modules, as a flame graph.
loadometer times every module your app loads — require() and import,
JavaScript and TypeScript — and writes the result as
folded stacks
(import;chain milliseconds), the format flame-graph tools understand.
Install
npm install --save-dev loadometerUsage
Run your app with loadometer as a preload — no code changes:
node --import loadometer/register app.js # Node
bun --preload loadometer/register app.js # BunThis is the recommended way everywhere. The preload hooks the module loader
and times each module's load and evaluation, covering require() and
import, in both JavaScript and TypeScript.
Run it: print to the console
By default the folded stacks are printed to stdout:
node --import loadometer/register app.js./app.js 42
./app.js;express 31
./app.js;express;body-parser 8Each line is an import chain and how many milliseconds that load took.
Run it: write to a file
Set LOADOMETER_OUT_FILE to write the folded stacks to a file instead:
LOADOMETER_OUT_FILE=imports.folded node --import loadometer/register app.jsVisualize it on the web
Open speedscope.app and drag your
imports.folded file onto the page — it renders an interactive flame graph,
no install required.
Prefer an SVG? Any folded-stack renderer works:
npx inferno imports.folded > imports.svg
# or Brendan Gregg's script:
flamegraph.pl imports.folded > imports.svgHow it works
loadometer measures the wall-clock time each module takes to load, then
writes it as folded stacks. It uses two mechanisms, depending on how a module is
loaded:
CommonJS (require). It wraps Module.prototype.require and measures with
performance.now() around the original call. Because require() synchronously
reads, compiles, and executes the module — and everything that module requires
in turn — the time is inclusive: a module's number includes its children.
ESM (import), via the preload. Native imports never call require, and a
module's body runs after its dependencies are loaded, so there's no single
call to wrap. Instead the loader hooks do two things per module:
Time the load step — reading the file and transpiling it (e.g. stripping TypeScript types).
Rewrite the module's source to insert timestamps around its body, timing its evaluation (the top-level code actually running). The
resolvestep records each module's importer, which is how thea;b;cimport chain is rebuilt.The reported number is
load + evaluationfor that module.
What a line means. Output is one line per module in folded-stack form:
express;body-parser;bytes 3reads as "loading bytes, imported by body-parser, imported by express,
took 3 ms." Flame-graph tools size each box by summing its children, turning
these lines into the familiar width-is-time picture.
It's wall-clock time, not CPU time, so it includes disk I/O and any waiting
(such as top-level await). A module that's already cached loads in ~0 ms — the
work only happens the first time it's pulled in.
TypeScript
Works out of the box. The preload instruments TypeScript source directly, so it
covers Node's built-in type stripping, tsx / ts-node, and Bun's native TS —
no separate build step.
Coverage, and the require() workaround
| Runtime | --import / --preload …/register (recommended) | require('loadometer') |
|---------|:-------------------------------------------------:|:-----------------------:|
| Node | CommonJS + ESM | CommonJS |
| Bun | ESM | CommonJS |
The preload is the way to go everywhere — with one exception: on Bun,
require()'d CommonJS dependencies bypass Bun's loader plugin, so the preload
sees only ESM there.
For a CommonJS app on Bun, use the in-file import as a workaround — drop this
at the very top of your entry, before anything else (Bun routes require
through Module.prototype.require, so this captures the loads below it):
require('loadometer'); // must be first
require('./app');Examples
Runnable examples live in examples/ — quick scripts, self-contained
repos for every Node / Bun × JS / TS × CommonJS / ESM combination, and a
long-running demo server. Run any of them with npm:
npm run example:1 # console output
npm run example:2 # writes imports.folded
npm run example:node-js-esm # Node + JS + native ESM (preload)
npm run example:bun-ts-esm # Bun + TS + native ESM (preload)
npm run example:node-ts-cjs # Node + TS + CommonJS
# …every runtime × language × module system — 8 in total
npm run example:server # long-running server: hit /load?pkg=…, then Ctrl+C
npm run examples # run the whole matrix at onceSee examples/README.md for the full matrix. (Bun scripts
need bun on your PATH.)
Notes
- What the numbers mean. The preload times each module's load + its own
evaluation; the
require('loadometer')workaround times each load inclusively (the module and everything it pulls in). Both render fine as a flame graph.
