perf(browser): reduce matching screenshot overhead#10278
Open
kasperpeulen wants to merge 5 commits intovitest-dev:mainfrom
Open
perf(browser): reduce matching screenshot overhead#10278kasperpeulen wants to merge 5 commits intovitest-dev:mainfrom
kasperpeulen wants to merge 5 commits intovitest-dev:mainfrom
Conversation
Avoid decoding byte-identical PNG screenshots for built-in comparisons and add page-target screenshot support so full-page assertions can use the browser provider directly.
✅ Deploy Preview for vitest-dev ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
Document that byte-level short circuiting only applies to the built-in comparator and make the WebdriverIO page screenshot fallback explicit.
macarie
previously approved these changes
May 7, 2026
Member
macarie
left a comment
There was a problem hiding this comment.
I love the idea behind this change, great work! 🙌🏼
sheremet-va
previously approved these changes
May 7, 2026
Member
sheremet-va
left a comment
There was a problem hiding this comment.
Looks good to me, just a small nitpick 😄
| // element and plain `page.screenshot()` calls keep the existing body fallback. | ||
| const buffer = options.target === 'page' | ||
| ? await context.browser.saveScreenshot(normalizedSavePath) | ||
| : await (await context.browser.$(options.element?.selector ?? 'body')).saveScreenshot( |
Member
There was a problem hiding this comment.
I don't think the double await is required here; context.browser.$ is chainable
Author
There was a problem hiding this comment.
Good call, updated to use the chainable element directly in 5334ae3.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
expect(page).toMatchScreenshot()by passing an explicit page target through the browser matcher so providers can use native page-level screenshots instead of body locator screenshots.Why
This came from profiling a downstream browser visual-regression suite where
toMatchScreenshothad become the dominant local-test cost. The suite captures every known theme/viewport variant, so a single test file can produce many screenshots and small per-screenshot overhead quickly turns into seconds of local feedback time.The key profiling result was that the comparator was not the bottleneck. Most time was spent before comparison: asking Playwright for screenshots and decoding PNGs that often turned out to be byte-identical to the stored reference.
For the common passing case, if the browser returns the exact same PNG bytes as the reference, Vitest does not need to decode both images or run pixel comparison. This PR short-circuits only that known-safe built-in comparator path while preserving the existing fallback behavior for mismatches, updates, unstable screenshots, and custom comparators.
Profiling Notes
Before this patch, the downstream suite was spending about
31.97swall time with screenshot assertions enabled versus about16swith screenshot assertions disabled.Instrumentation around the Vitest matcher and Playwright provider showed this distribution for the full browser run:
160screenshots (40tests x4variants)84.9ssummed530ms/ screenshotelement.screenshot()68.7ssummed429ms/ screenshot11.5ssummed72ms/ screenshot95.7mssummed0.6ms/ screenshot0A smaller dedicated probe showed the same shape: compare was usually
0-7ms, while the useful work was dominated by roughly90-110msof Playwright capture plus35-75msof PNG decode per screenshot.A separate Playwright probe also showed that, for explicit page captures, going through the native
page.screenshot()API was faster than routing the same capture through a body/document locator. That is the motivation for the explicitexpect(page)/target: 'page'path instead of treating page screenshots as another element screenshot.This PR does not make
expect(page).toMatchScreenshot()imply full-page screenshots. It preserves the caller/configuredscreenshotOptions.fullPagevalue, so apps that want full-document captures can opt intofullPage: truethemselves.That is why this patch targets the matching/reference path, page-level capture path, and PNG work rather than the diff algorithm itself.
Local Performance
Measured in the downstream app that motivated this change, using
4workers against the same visual suite:31s20-21s16sSo the visual assertion overhead dropped to roughly
4-5sover the same suite without screenshot assertions. Compared with the original~31srun, this removes about10sof local feedback time while still always running the visual assertions.These numbers are from an app-level suite, not Vitest's own fixtures, but they are the reason for the shape of the patch: avoid decode/compare work when the captured PNG is already byte-identical to the reference, use the faster native page capture path for explicit page screenshots, and avoid extra encode work when writing screenshots that were already captured as PNG buffers.
Safety Notes
expect(page).toMatchScreenshot()uses explicit page-target handling, but it does not forcefullPage: true; full-page screenshots remain controlled byscreenshotOptions.fullPage.page.screenshot()body fallback are kept separate.Test Plan
pnpm typecheckCI=true PROVIDER=playwright TEST_BROWSER=chromium pnpm -C test/browser run test-expect-dom toMatchScreenshot.test.tsCI=true PROVIDER=webdriverio TEST_BROWSER=chrome pnpm -C test/browser run test-expect-dom toMatchScreenshot.test.ts