Image engines
StoryShelf compares screenshots with pixelmatch. Around that comparison, something has to decode the two PNGs, encode the overlay of a failing diff, and shrink screenshots into thumbnails for AI insights. That something is the image engine:
| Engine | What it is | Install | Runs on |
|---|---|---|---|
pngjs (default) | Pure JavaScript | Built in | Everywhere Node runs, including serverless and edge-ish hosts |
sharp | Native (libvips) | npm install sharp | Platforms with a prebuilt sharp binary |
The engine only changes how PNGs are read and written. The comparison is pixelmatch either way, so verdicts and differing-pixel counts are the same on both engines.
Switching to sharp
Section titled “Switching to sharp”createShelfApp({ // ... testing: { images: { engine: "sharp" } },});or set STORYSHELF_IMAGE_ENGINE=sharp (the option wins over the variable). Install sharp next to StoryShelf; it is an optional peer dependency.
If the name is unknown, or sharp is not installed or has no binary for the platform, StoryShelf refuses to start (readiness fails under images) instead of quietly falling back, so a typo never changes your diffs without anyone noticing. The engine in use is logged on each capture attempt’s render pass finished line.
Where the engine applies:
- The server (
createShelfApp) usestesting.images.enginefor diffs, overlays, and AI thumbnails. - A forked capture child inherits it from the server (and
testing.compare.threadstoo), so you set it once. - Every other instance reads its own
testing.imagesoption, orSTORYSHELF_IMAGE_ENGINEfrom its environment, so give all instances behind one database the same value.
With sharp, each diff-pool worker thread limits libvips to one thread, so a busy server is not oversubscribed.
The engine is chosen per deployment, not per project. Baselines are stored PNGs and every build is diffed fresh against them, so you can switch engines at any time without re-approving anything.
Does it change my results?
Section titled “Does it change my results?”Both engines decode the common PNG types to identical pixels (RGB, RGBA, greyscale, palette, interlaced, gamma-tagged), and the test suite checks that diff results match. The one difference is 16-bit PNGs, which round to 8 bits slightly differently (at most one level out of 255, well under the default pixel threshold). Browser screenshots are 8-bit, so you will not meet this in practice. The diff overlay image is pixel-identical; only the PNG file bytes differ.
Performance
Section titled “Performance”Median milliseconds on an Apple M1 Max (Node 24). Screenshots are the StoryShelf UI at 1920 px wide, either a single viewport or a stacked full page. Each engine runs one diff at a time (diffs run on a worker pool, so a busy server does many at once).
Viewport (1920×1050)
| Step | pngjs | sharp | Speed-up |
|---|---|---|---|
| Decode a PNG | 17 | 4 | 4.6x |
| Encode a diff overlay | 110 | 9 | 11.9x |
| Diff: noise only (passes) | 41 | 17 | 2.4x |
| Diff: small change | 176 | 36 | 4.9x |
| Diff: everything changed | 355 | 238 | 1.5x |
Full page (1920×7320)
| Step | pngjs | sharp | Speed-up |
|---|---|---|---|
| Decode a PNG | 105 | 24 | 4.3x |
| Encode a diff overlay | 764 | 64 | 12.0x |
| Diff: noise only (passes) | 247 | 80 | 3.1x |
| Diff: small change | 1153 | 237 | 4.9x |
| Diff: everything changed | 2510 | 1690 | 1.5x |
Where the gain comes from: decoding and encoding PNGs is what sharp accelerates, and a diff that passes spends most of its time decoding. When nearly every pixel differs, the pixelmatch comparison itself dominates and the gain shrinks to about 1.5x. Expect the biggest improvement on large full-page screenshots and on builds with many small changes.
The numbers depend on your hardware and screenshots. Reproduce them on yours:
cd packages/imagenub run bench # the StoryShelf UI screenshotsnub run bench -- ./my-shots # a folder of NAME-light.png / NAME-dark.png pairsWhich should I use?
Section titled “Which should I use?”- Stay on
pngjsif you want zero native dependencies, deploy to a runtime withoutsharpbinaries, or your builds are small. It needs no setup. - Switch to
sharpif capture or re-diff time is dominated by large screenshots, and you run on Linux, macOS, or Windows wheresharpinstalls cleanly.