Reduce route CLS without weakening caching
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:
- Next.js recommends
loading.tsxfor route streaming and instant loading UI: https://nextjs.org/docs/app/getting-started/linking-and-navigating#streaming - Next.js recommends a route group when a loading fallback must apply to only one page: https://nextjs.org/docs/app/getting-started/project-structure#opting-specific-segments-into-a-layout
- TanStack Query recommends server prefetching plus
HydrationBoundaryfor React Server Components and the Next.js App Router: https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr - Next.js documents
force-dynamicanddefault-no-storeas request-time rendering and uncached fetch behavior in the non-Cache-Components model: https://nextjs.org/docs/app/guides/caching-without-cache-components
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 whileisFetching. - Do not add
revalidate,force-static, orunstable_cachefor this work.
Loading components
- Keep low-level placeholders small.
- Add domain loading compositions beside the final components, such as
BottleOverviewLoadingandBottleListLoading. - Replace the current universal
LoadingListusage 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
LoadingListafter 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.1for representative bottle and entity navigation. - Background refetches retain existing content.
- Anonymous and
_sessionresponse 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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