dcramer / dcramer/peated

Reduce route CLS without weakening caching

Open
#938 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
103
Forks
18
Avg merge
1h 19m
Merged PRs (30d)
555

Description

Problem

Peated uses generic loading rows for many unrelated routes. Detail pages can replace a short fallback with a much taller overview, and client queries can add sections after the initial page commit. This creates visible cumulative layout shift (CLS).

The fix must preserve the current cache and identity boundaries:

  • normal HTML stays dynamically rendered;
  • fetchCache = "default-no-store" stays in place;
  • authenticated responses remain private and uncached;
  • no new cross-request cache is added for viewer-dependent oRPC data;
  • the browser TanStack Query cache, query keys, stale times, and mutation invalidation continue to work;
  • cached content remains visible during background refetches.

Current guidance

This approach follows the current framework guidance:

Design

Loading boundaries

Use invisible route groups so each fallback owns one layout family:

bottles/
├── (index)/
└── (detail)/
    └── [bottleId]/
        ├── layout.tsx
        ├── (overview)/
        │   ├── page.tsx
        │   └── loading.tsx
        └── (tabs)/
            ├── loading.tsx
            ├── prices/
            ├── releases/
            └── tastings/

Apply the same model to entity details. Shared detail layouts remain mounted during sibling navigation.

Layout ownership
  • The loaded overview and its fallback must compose the same layout component.
  • Bottle and entity keep separate domain layouts.
  • Breakpoints, rail widths, grid areas, media ratios, and mobile ordering have one owner.
  • A route fallback must not use a bottle-row skeleton unless the final route renders bottle rows.
Query behavior
  • Prefetch critical first-viewport queries on the server with the same oRPC query options and current request identity used by the browser.
  • Hydrate successful queries into the existing request-local server QueryClient and browser QueryClient.
  • Do not hydrate anonymous output into a viewer-dependent query key. Bottle and entity serializers contain viewer annotations.
  • Keep deferred or failed queries local to stable section slots.
  • Show skeletons only when a query has no cached data (isPending). Keep existing data visible while isFetching.
  • Do not add revalidate, force-static, or unstable_cache for this work.
Loading components
  • Keep low-level placeholders small.
  • Add domain loading compositions beside the final components, such as BottleOverviewLoading and BottleListLoading.
  • Replace the current universal LoadingList usage over time. Its thumbnail/text/score geometry does not represent settings, notifications, profiles, badges, or activity.

Phases

  • Bottle overview vertical slice
    • Scope overview loading with a route group.
    • Share layout geometry between loaded and loading states.
    • Prefetch and hydrate reviews and tastings with request-matched identity.
    • Keep recommendations deferred in a stable rail slot.
    • Verify no duplicate client RPC after hydration.
  • Entity overview vertical slice
    • Scope overview and tab loading boundaries.
    • Share entity overview geometry.
    • Prefetch queries that determine whether above-the-fold sections exist.
  • Audit remaining route families
    • Profiles
    • Activity and notifications
    • Badges
    • Settings
    • Catalogs and workflows
  • Document loading and cache invariants in the frontend policy.
  • Retire or rename generic LoadingList after unrelated consumers migrate.

Acceptance criteria

  • Desktop and mobile detail navigation keep the header, tabs, media area, and first-viewport content stable under artificial latency.
  • Layout-shift entries without recent input stay below a CLS budget of 0.1 for representative bottle and entity navigation.
  • Background refetches retain existing content.
  • Anonymous and _session response cache headers do not change.
  • Authenticated bottle library and entity-follow annotations remain correct.
  • Back navigation uses the browser query cache.
  • Server-prefetched queries do not immediately repeat in the browser.
  • Mutation invalidation still refreshes hydrated queries.

Verification notes

The local development database migration chain currently fails before the latest bottle/entity schema migrations. Fix or refresh that environment before final browser CLS verification; do not work around it in this change.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the bottles route tree, especially the detail layout, overview and tabs loading.tsx boundaries, then review the existing LoadingList consumers and server/browser QueryClient setup. Implement the bottle overview vertical slice before the entity and remaining route families, preserving the listed cache and identity invariants. Verify navigation under artificial latency, CLS below 0.1, cache headers, hydration without duplicate RPCs, background refetch behavior, and mutation invalidation; fix the local migration environment before browser verification.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, react, typescript
Domain
frontend, performance
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.