Performance
Recording costs time in every test, and the plugin costs a little in every run. The numbers come from the react-browser and react-jsdom suites.
In short #
The time goes to three places:
-
The plugin, in every run. Its config hook, the transform that registers
the exports of your modules, and the setup file. This is the cost of a run with
exclude: '**', which records nothing. - The capture, once per frame. rrweb serializes the DOM and its stylesheets. The cost grows with the size of the DOM.
- The reporter, at the end. It reads props with TypeScript, writes the snapshot files and stylesheets, and copies assets. Reading props costs about 0.1 to 0.2 s per run on the examples.
In the jsdom example, recording adds 310 ms to the sum of test durations and 728 ms to the wall clock of the run. In browser mode it adds 101 ms and 717 ms. The plugin alone adds 309 ms to the jsdom run and 91 ms to the browser-mode run.
Example suites #
In each example directory,
node ../../scripts/bench-macro.mjs 9 <variants> runs
vitest run 9 times per variant. The variants take turns, after one warm-up
run that does not count, and the tables show medians. Every variant reports through the
same JSON reporter, so the console reporter never differs between them. Measured on an
Apple M2 with Node 20.19.5.
off- No plugin.
idle- The plugin with
exclude: '**', so it records nothing. on- Recording.
The plugin cost is idle − off, the recording cost on − idle. The
examples switch variants with an environment variable, see
DESCRIBE_ME.
jsdom
examples/react-jsdom: 28 tests, 23 of them in the manifest, 33 frames.
off |
idle |
on |
plugin cost | recording cost | |
|---|---|---|---|---|---|
| sum of test durations | 543 ms | 536 ms | 846 ms | -8 ms (-1 %) | +310 ms (+58 %) |
wall clock of vitest run |
1956 ms | 2265 ms | 2993 ms | +309 ms (+16 %) | +728 ms (+32 %) |
The wall clock of the jsdom run without recording, with one part of the plugin switched
off, against idle. These are the variants idle-no-css,
idle-no-styled and idle-no-exports:
| wall clock | change | |
|---|---|---|
test.css: false |
2272 ms | +7 ms (+0 %) |
styledComponentsBrowserBuild: false |
2235 ms | -30 ms (-1 %) |
registerExports: false |
2207 ms | -58 ms (-3 %) |
Browser mode
examples/react-browser in headless Chromium: 25 tests, 44 frames.
off |
idle |
on |
plugin cost | recording cost | |
|---|---|---|---|---|---|
| sum of test durations | 1429 ms | 1493 ms | 1594 ms | +65 ms (+5 %) | +101 ms (+7 %) |
wall clock of vitest run |
2525 ms | 2617 ms | 3334 ms | +91 ms (+4 %) | +717 ms (+27 %) |
The wall clock also covers the reporter: component docs, snapshot files, stylesheets and assets.
Capture cost #
One step() capture by DOM size, the median of pnpm bench:micro
in each example:
| DOM size | Chromium | jsdom |
|---|---|---|
| about 40 nodes | 0.4 ms (48 nodes) | 1.8 ms (35 nodes) |
| about 300 nodes | 1.3 ms (318 nodes) | 3.7 ms (305 nodes) |
| about 3 000 nodes | 10.7 ms (3 018 nodes) | 26.6 ms (3 005 nodes) |
| about 15 000 nodes | 57.4 ms (15 018 nodes) | 136.8 ms (15 005 nodes) |
In jsdom, the 1 ms setTimeout(0) of the settle is part of every capture. Most
of the rest is rrweb's serialization. JSON.stringify of the result takes
13–14 % of a capture from 3 000 nodes up in Chromium, and 7–8 % in jsdom. Comparing that
text with the previous frame's takes at most 0.3 ms in Chromium and 0.52 ms in jsdom, at
15 000 nodes.
Naming a test after the first registered component inside a provider takes 0.14 ms at 3
008 nodes in jsdom (median, pnpm bench:micro in
examples/react-jsdom).
There is no requestAnimationFrame wait before a capture. The first version
had one, which cost a full vsync (about 16 ms) per frame and made tests 1.8× slower. A
snapshot is DOM and stylesheets, not layout, so one macrotask is enough for React to
commit.
Output size #
Every snapshot file is named after a hash of its content, so identical DOM is stored once,
however many frames, tests and runs produce it. Long stylesheets move into
styles/, split into chunks that snapshots share. See
How it works.
pnpm measure-output finds:
| Example | Snapshot files | Distinct DOMs | styles/ |
|---|---|---|---|
examples/react-browser |
37 | 37 | 4 files, 3.8 kB |
examples/react-jsdom |
26 | 26 | 10 files, 5.2 kB |
Web fonts add weight to a static site. describe-me build downloads every
unicode-range subset of a font, so Inter in three weights adds about 220 kB
to the site. See Vendoring.
Live in the static site
describe-me build --live adds the
static live preview to the site as
__live/. In each example directory,
describe-me build --no-vendor-fonts ran 9 times with --live and
9 times without, taking turns after one warm-up run of each. The times are medians of the
whole command. Measured on an Apple M2 with Node 20.19.5 and Vite 8.3.0.
| Example | build |
build --live |
__live/ |
|---|---|---|---|
examples/react-browser |
0.17 s | 0.35 s | 23 files, 1.1 MB |
examples/react-jsdom |
0.17 s | 0.32 s | 25 files, 1.7 MB |
The build is not minified. Most of __live/ is one chunk that every test
shares, with React's development build: 1.0 MB in react-browser, and 1.3 MB in
react-jsdom, where Testing Library joins it. The runtime of the live page adds about 70
kB, and each test file a chunk of its own.
Measure your project #
Compare three variants of the same run:
- Off
enabled: false, the baseline. -
Idle
exclude: '**', the plugin without recording. PointoutDirelsewhere, or its cleanup empties your.describe-me/. - On recording, as you run it for the docs.
vitest.config.ts
import { defineConfig } from 'vitest/config'
import { describeMe } from '@describe-me/vitest/plugin'
const variant = process.env.VARIANT ?? 'on'
const idle = variant === 'idle'
export default defineConfig({
plugins: [
describeMe({
enabled: variant !== 'off',
exclude: idle ? '**' : undefined,
outDir: idle ? 'node_modules/.describe-me-idle' : undefined,
}),
],
test: {
// The same console reporter in every variant.
reporters: ['default'],
},
})
Name the reporters in the config, as above. The plugin then adds only its own, so all
three variants print through the same ones. Do not pass --reporter: it
replaces the whole list, describe-me's reporter included. A reporter difference distorts
the comparison. An AI agent session, for one, picks minimal, which prints
less than default.
Run the variants in turn (off, idle, on, off, idle, on…) rather than one after the other,
so warm caches and machine load hit them alike. Skip the first round, and take the median
of several. scripts/bench-macro.mjs in the repository does this for the
examples. pnpm bench:macro does 5 runs of off,
idle and on in examples/react-browser, and of all
six variants in examples/react-jsdom.
Making it cheaper #
-
Record fewer files. Point
includeandexcludeat the tests that show components. See Which tests record. - Record in a docs run of its own. Keep your everyday run without the plugin and record in a second config. See A config of its own.
- Keep the DOM small. Capture cost grows with the number of nodes. Render the component, not the whole app.
- Do not call font loaders in tests. The link they add lands in every snapshot, and in jsdom, where it never loads, every capture sets a timer for it.
-
Keep
describe-me buildoff the network.--no-vendor-fontsskips the font downloads. See--no-vendor-fonts.
test.css: false, styledComponentsBrowserBuild: false and
registerExports: false save little, as the
jsdom table shows, and cost output quality.