Skip to content

perf(detail): render slides + thumbnail strip from sized variants, not full-res preview#522

Merged
Zheaoli merged 1 commit into
mainfrom
feat/detail-variant-decode
Jun 10, 2026
Merged

perf(detail): render slides + thumbnail strip from sized variants, not full-res preview#522
Zheaoli merged 1 commit into
mainfrom
feat/detail-variant-decode

Conversation

@Zheaoli

@Zheaoli Zheaoli commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Why (the actual root cause)

The detail-view jank that persisted after the histogram PR is image decoding, per flame-graph profiling. On this deployment preview compression is off (previewImageMaxWidthLimit unset), so preview_url is the full-resolution (~30MP) image. The in-place carousel rendered the raw preview_url for up to 5 visible slides plus 21 thumbnail-strip cells, so:

  • Open decoded ~600 megapixels (21 thumbnails + slides) → ~1.3s of jank on a real device.
  • Switch decoded a ~45MP image each time.

This is device-bound (fast headless machines hid it at ~67ms; slower real devices stalled). The earlier histogram PR fixed per-switch recompute but never touched decoding, so the jank remained.

Fix — route both through the existing variant system (same one the grid uses)

  • ProgressiveImage inline (non-zoom) preview now serves a ~1280 variant via makeVariantLoader instead of preview_url. A fixed width (not next/image DPR srcset) so retina screens aren't pushed to the 2560 tier. ~30MP → ~1.1MP per slide. The zoom path is untouched (still loads the original for pixel-peeping). Falls back to preview_url only when a photo has no variants; an onError steps variant → preview_url, never up to the original.
  • Thumbnail-strip cells (48px) serve the smallest 320 tier (requested at width 96). No variants (or onError) → decoded blurhash, never the full-res preview_url. ~30MP → ~0.07MP per thumbnail.

Mirrors the variant → fallback → blurhash cascade in blur-image.tsx. The makeVariantLoader min(ceilToTier, readyMaxWidth) clamp keeps it safe during backfill (serves a softer-but-smaller tier, never a 404).

Out of scope / guardrails

  • No change to the zoom/WebGL path, the GL-context LRU, or the FLIP transition's [data-flip-target] measurement.
  • Optional, separate: enabling previewImageMaxWidthLimit would downsample future uploads' previews, but rendering from variants already covers existing photos so it isn't required.

Verification

tsc + eslint clean for changed files. Expected post-deploy (throttled re-trace): open decode drops from ~600MP to single-digit MP, per-switch decode from ~45MP to ~1MP. Prerequisite being confirmed in parallel: the albums are variants_ready (a scan is underway; any stragglers without variants are a backfill subset, not a rendering issue).

…t full-res preview

The real cause of the detail-view jank (flame graph) is image decoding, not React
or WebGL: on this deployment preview compression is off, so `preview_url` is the
full-resolution (~30MP) image. The in-place carousel rendered the raw preview_url
for up to 5 visible slides plus 21 thumbnail-strip cells, so opening an album
decoded ~600 megapixels and every switch decoded a ~45MP image — device-bound
jank (fast headless machines hid it; real devices stalled).

Route both through the existing variant system (the same one the grid uses):

- ProgressiveImage inline (non-zoom) preview now serves a ~1280 variant via
  makeVariantLoader instead of preview_url — a fixed width (not next/image DPR
  srcset) so retina screens don't get pushed to the 2560 tier. ~30MP → ~1.1MP
  per slide. The zoom path is untouched (still loads the original for
  pixel-peeping). Falls back to preview_url only when a photo has no variants,
  and an onError steps variant → preview_url, never up to the original.
- Thumbnail strip cells (48px) serve the smallest 320 tier (requested at width
  96); no variants (or onError) → decoded blurhash, never the full-res
  preview_url. ~30MP → ~0.07MP per thumbnail.

Mirrors the variant→fallback→blurhash cascade in blur-image.tsx. No change to the
zoom/WebGL path, the GL-context LRU, or the FLIP transition's target measurement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
picimpact Ready Ready Preview, Comment Jun 10, 2026 4:51am

@Zheaoli Zheaoli merged commit 734ac6e into main Jun 10, 2026
6 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.

1 participant