How to read it
- One output stage for everyone. Every library is timed through to a PNG data URL. Comparing an SVG URL with a PNG would omit rasterization and encoding from one side.
- Cold and repeat are different numbers. First capture builds a new element for every run. Repeat captures the same element again. Compare both columns because reuse can change the timing and ranking.
- Defaults, scale 1. This lab loads SnapDOM from
@latestand uses fixed competitor versions. No library is configured for advantage. SnapDOM runs withburst: falsein every scene except Polling, so its memoization is not compared against everyone else's full pipeline. - The table does not know who should win. Rows are sorted by measured time and the fastest is highlighted, whoever it is. On some scenes that is not SnapDOM.
Absolute milliseconds depend on your CPU, your browser and how busy the machine is. The ratios are the part worth quoting.
The page is held still while a run is in progress. A library that clones the whole document crops at viewport-relative coordinates, so scrolling mid-capture can change the region it captures.
| Library | First capture | Repeat | vs fastest | Output | Differs from SnapDOM |
|---|
The last column is the share of pixels that differ from SnapDOM's capture beyond a per-channel tolerance — not a correctness verdict, since SnapDOM is only the reference because it is the library this page ships. Small differences may come from antialiasing or font rasterization, but can also reveal a real defect. A large number means the two libraries did not render the same thing, and the gallery below shows which: milliseconds cannot tell a fast capture from a fast capture of the wrong thing.
Every row must also produce the same output size, and must actually paint something. A row tagged not comparable rendered at a different resolution, and one tagged blank output returned a correctly-sized image with nothing in it — in both cases the milliseconds are measuring the wrong thing. Treat those rows as bug reports, not results.
What each library actually produced. Click one to open it full size — speed is only half the comparison, and this is the other half.
Capability matrix, verified by pixels
Feature tables are usually copied from READMEs. This one is not: it captures a fixture where each capability paints its own marker colour, then counts those pixels in the output. A library "supports" something here only if the colour survived the round trip.
It starts by proving the oracle can say no — the same fixture is built with every capability removed, and all six must read absent. A checker that cannot fail is a checker that means nothing.
The oracle self-test runs first.
Cells report behaviour with defaults. Some libraries pass more of these once configured — html2canvas, for instance, handles conic gradients with foreignObjectRendering: true. Results also vary by engine: run this page in Safari and Firefox and compare.
What this page cannot tell you
- It is one machine — yours. A run on a busy laptop, in a background tab, or with an extension injecting stylesheets will move every row. Run it twice.
- Competitors load from a CDN. Anything jsDelivr cannot serve you shows up as a row that could not load, not as a win.
- Speed is not fidelity. A library that skips shadow DOM has less work to do. Read the output gallery and the capability matrix alongside the milliseconds, never instead of them.
- These are synthetic scenes. They were chosen because they isolate a cost — per-node work, author-stylesheet scanning, shadow traversal, large-output raster — not because they are your page.
The same scenes run in the repository as __tests__/category.*.benchmark.js. Compare library versions, browser, scene and capture mode when investigating a difference. You can open an issue with the copied results and output.
Read the full comparison
Approach trade-offs, the decision guide, and where each library actually fits.
Back to the comparison View on GitHub