Skip to content

Cache downsampled preview thumbnails for same-origin preview media #2500

Description

@propr-dev

Problem

On a fresh page load, compact preview thumbnails take a couple of seconds to appear in task lists, goals and the Inbox. Each thumbnail re-downloads the full-resolution capture and re-runs the multi-pass canvas downsampling (downsampleToCanvas in previewDownsampling.ts) on the main thread.

Two causes on main:

  • Every load refetches the full image. Task and goal previews are rewritten to ProPR's authenticated media route (/api/preview-media/{pulls|comments}/:owner/:repo/:number/:assetId, see projectApplicationMedia in packages/api/services/previewMediaProjection.ts). The UI loads them as same-origin blobs (usePreviewMediaSource). The route responds with Cache-Control: private, no-store, so the browser cache never helps, and the API itself fetches the asset from GitHub on each request.
  • Every load re-downsamples. Nothing persists the downsampled result.

Hard constraint (learned in #2502)

Direct https://github.com/user-attachments/assets/<id> URLs redirect to S3 without CORS headers. A canvas that draws them is tainted and can't be exported with toDataURL/toBlob, and createImageBitmap doesn't get around it. Client-side thumbnail caching is only possible for same-origin sources, meaning previews served through /api/preview-media/... as blobs. Don't attempt CORS probes or no-cors fallbacks.

Proposal

  1. Persist downsampled thumbnails for same-origin sources. After PreviewImage downsamples a blob-backed preview, store the result (WebP or PNG blob plus dimensions) in IndexedDB. On the next mount, paint from the cache before fetching or decoding anything.
    • Key: account/desktop scope + media URL + CSS width × height + device pixel ratio. Use the same scope key usePreviewMediaSource already uses, so a cached thumbnail never crosses accounts or desktop profiles.
    • TTL: 7 days. Prune expired entries at idle, cap total entries or bytes, and clear the account's entries on logout and account switch.
    • If IndexedDB is unavailable (private mode or blocked), fall back to an in-memory map without failing.
    • On a cache hit, skip usePreviewMediaSource's full-size fetch entirely for compact thumbnails. Fetch the original only when the user opens the full view or lightbox.
  2. Let the browser cache the media route privately. GitHub user-attachment asset IDs are immutable, so /api/preview-media/... can return Cache-Control: private, max-age=86400, or something similar, instead of no-store. That speeds up the full view too. Keep Vary: Authorization, Cookie, and confirm that revoking access is acceptable within the max-age window. If it isn't, keep no-store and rely on step 1.
  3. Check coverage. Confirm that every surface rendering compact thumbnails (task list, task detail, goals, Inbox, dashboard) receives projected /api/preview-media/... URLs. Where a surface still gets raw GitHub URLs, extend the projection rather than adding client workarounds. Previews that stay on raw GitHub URLs keep today's behavior.

Acceptance criteria

  • On a second load of a task list, goal or Inbox view, compact thumbnails paint from the cache, with no media fetch and no downsampling passes. Verify with a component or browser test that counts fetches and halving passes.
  • A cached thumbnail is never shown under a different account or desktop profile, and logout clears that account's entries.
  • Expired entries are ignored and pruned. Storage stays within the cap.
  • Raw GitHub-URL previews keep working; nothing tries to export a tainted canvas.
  • The full view and lightbox still load the original image.

Out of scope

Service-worker caching of opaque GitHub responses (#2502's final design). It skips the network fetch but still decodes and downsamples on every load, and it can't cache the thumbnail itself.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions