Skip to content

perf: have the next screenshot ready before the reviewer asks for it - #402

Merged
nGervasyuk merged 1 commit into
Visual-Regression-Tracker:masterfrom
nGervasyuk:perf/details-dialog-image-prefetch
Sep 1, 2026
Merged

nGervasyuk merged 1 commit into
Visual-Regression-Tracker:masterfrom
nGervasyuk:perf/details-dialog-image-prefetch

Conversation

@nGervasyuk

@nGervasyuk nGervasyuk commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

What

Stepping through a build's runs with the arrows left the pane blank under a Loading... for a second or more per step. Each screenshot is several megapixels (1284×2778, 2048×2732), and nothing was fetched until the reviewer had already moved.

How

Prefetch. The details dialog warms the browser cache for the runs on either side of the one on screen — two ahead, one behind, since review moves forward — nearest first, so the run the arrow lands on is requested first. All three pictures of a neighbour (baseline, checkpoint, diff). Names already requested are not requested again, carried in a ref across renders.

Caching. Prefetching only pays if the browser is allowed to keep what it fetched, and nginx was serving the image directory with no cache headers at all, so every revisit re-fetched. Images now get a year of immutable: an image name is unique per upload and the bytes behind it are never rewritten.

That needed its own location block rather than a header on the existing one — and it turns out that was necessary for correctness, not just tidiness. The SPA route ends in try_files ... /index.html =404, so a missing image was answered with the app's HTML. Combined with a year of immutable caching, the browser would have cached index.html under an image's name and never asked again. The new block ends in try_files $uri =404.

Verified on a built image:

$ curl -sI .../static/imageUploads/<name>.png
HTTP/1.1 200 OK
Content-Type: image/png
Cache-Control: public, max-age=31536000, immutable

$ curl -sI .../static/imageUploads/does-not-exist.png
HTTP/1.1 404 Not Found

Not done, on purpose

Keeping the previous run's image on screen while the next one loads. The header would read Step 5 over step 4's pixels — worse than a spinner. The prefetch removes the pause itself.

Tests

imagePrefetch.helper.test.ts — the neighbour window (order, both ends, the current run excluded, missing names, dedup) and the request side (URLs come from staticService.getImage, nothing asked for twice), with window.Image stubbed.

jest 36/36, Playwright 87/87 (matched against 87/87 on master), tsc unchanged at the 30 pre-existing errors, eslint and prettier clean on touched files, nginx -t passes in the built image.

Deploy after backend #373

On a deployment that serves images from S3, this prefetch is worse than useless until backend#373 is out.

There, REACT_APP_STATIC_URL is empty, so getImage returns an API URL that 302s to a pre-signed S3 URL. Today that signature is minted per request, so the URL differs every time and the redirect itself is not cacheable. The prefetch downloads the neighbour's screenshot, and when the reviewer actually opens that screen the browser asks the API again, gets a different URL, and downloads the same bytes a second time — double the traffic and the S3 bill, no gain.

backend#373 makes the signed URL stable for a window, has S3 advertise a lifetime on the object, and makes the redirect cacheable. With it in place the prefetch lands in the browser cache and opening the screen is free.

On HDD deployments (REACT_APP_STATIC_URL set, the nginx change in this PR) the prefetch is already effective on its own.

@nGervasyuk nGervasyuk self-assigned this Aug 31, 2026
@nGervasyuk
nGervasyuk force-pushed the perf/details-dialog-image-prefetch branch from d670d17 to 9db1d66 Compare August 31, 2026 11:35
@nGervasyuk
nGervasyuk requested a review from pashidlos September 1, 2026 06:33
@nGervasyuk
nGervasyuk force-pushed the perf/details-dialog-image-prefetch branch from 3ee37b6 to 9db1d66 Compare September 1, 2026 08:07

@pashidlos pashidlos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Stepping through a build's runs with the arrows left the pane blank under
a "Loading..." for a second or more per step: each screenshot is several
megapixels, and nothing was fetched until the reviewer had already moved.

The dialog now warms the browser cache for the runs on either side of the
one on screen — two ahead, one behind, since review moves forward, nearest
first so the run the arrow lands on is requested first. Names already asked
for are not asked for again.

Prefetching only pays if the browser is allowed to keep what it fetched, so
nginx now serves the image directory with a year of immutable caching: an
image name is unique per upload and the bytes behind it are never rewritten.
That needed its own location block, which also stops a missing image falling
through to index.html the way the SPA route does — the app's HTML answered
under an image's name and then cached for a year would have been a far worse
bug than the 404 it now returns.
@nGervasyuk
nGervasyuk force-pushed the perf/details-dialog-image-prefetch branch from 9db1d66 to 94f229c Compare September 1, 2026 14:03
@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

@nGervasyuk
nGervasyuk merged commit 50e0892 into Visual-Regression-Tracker:master Sep 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants