Skip to content

perf(studio): cacheable image previews and colour placeholders - #69

Merged
Madraka merged 1 commit into
mainfrom
perf/studio-media
Oct 21, 2026
Merged

Madraka merged 1 commit into
mainfrom
perf/studio-media

Conversation

@Madraka

@Madraka Madraka commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Fixes 3 and 5 of the internal performance review (Studio media).

Before: GET /api/sites/:id/assets/:id/content answered private, no-store, so the browser re-downloaded every original on every visit (3.4 MB of 2880 px PNGs for the Media grid on cloudhal.com), each through the authenticated D1 path (0.4–2 s to first byte), with only a spinner meanwhile. The ETag/304 code existed but no-store disabled it.

Now:

  • Studio's preview URLs carry the bytes' hash: …/content?v=<first 16 hex of sha256> (assetPreviewUrl, AssetPage). A matching v gets private, max-age=86400. That's a day, not a year, so a file taken down does not linger in staff browsers; the bytes behind the URL never change. Without v (or with a stale one) the route answers private, no-cache, and the browser revalidates with the ETag and gets a 304.
  • ImagePreview (@cloudhal/ui) takes an optional placeholder: { color, lqip }. While loading, it paints the image's dominant colour and blurred preview (both computed at upload, RFC-0028) instead of the spinner; the loading label stays for screen readers. AssetFrame passes asset.image.
  • contentHeaders moved to routes/asset-content-headers.ts, so routes/assets.ts shrinks 853 → 833 lines (baseline lowered).

Tests: shared assets suite (no-cache without v, 304 with the ETag, a day with a matching v, no-cache with a stale one); image-preview-placeholder.test.tsx; Studio design rules and media-lib updated. API 371/371, UI 201/201, Studio design rules + media 518/518.

Not in this PR: resized variants (fix 14, needs an RFC) and public /media/ URLs for published assets (fix 13).

🤖 Generated with Claude Code

Studio's previews were `private, no-store`, so the media grid downloaded
every original (3.4 MB on cloudhal.com) on every visit, each through the
authenticated D1 path, behind a spinner.

- Preview URLs carry the bytes' hash (`?v=<sha16>`); a matching `v` gets
  `private, max-age=86400` (a day, so a takedown does not linger long).
  Without it the route answers `private, no-cache`, so the ETag gives a 304.
- `ImagePreview` takes an optional `placeholder` (`Asset.image.color`,
  `.lqip`, computed at upload) and paints it while the image loads,
  instead of a spinner; Studio's asset tiles pass it.
- `contentHeaders` moves to `routes/asset-content-headers.ts` (assets.ts
  833 lines, baseline lowered).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
@Madraka
Madraka merged commit cf62e26 into main Oct 21, 2026
4 checks passed
@Madraka
Madraka deleted the perf/studio-media branch October 21, 2026 22:28
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