|
| 1 | +# Performance Change Explanation |
| 2 | + |
| 3 | +This document explains what changed, why it changed, and how it affected speed, rendering mode, and perceived bundle/runtime cost. |
| 4 | + |
| 5 | +## Goals of This Refactor |
| 6 | + |
| 7 | +- Make first paint faster. |
| 8 | +- Avoid root-level blocking work. |
| 9 | +- Reduce hydration flicker. |
| 10 | +- Keep cache behavior simple and predictable. |
| 11 | +- Keep only required `Suspense` boundaries. |
| 12 | + |
| 13 | +## What Was Changed |
| 14 | + |
| 15 | +### 1) Root auth/session composition was simplified |
| 16 | + |
| 17 | +- Removed the dedicated `SessionProvider` wrapper file. |
| 18 | +- Root layout now uses `Providers` directly. |
| 19 | +- Session is resolved client-side in `ContextProvider` with `authClient.getSession()` after hydration. |
| 20 | + |
| 21 | +Why: |
| 22 | + |
| 23 | +- Server-side session calls at root can force request-time rendering and delay initial HTML. |
| 24 | +- Client-side session resolution preserves a fast static shell. |
| 25 | + |
| 26 | +### 2) Auth UI now waits for hydration |
| 27 | + |
| 28 | +- Added `isUserHydrated` state in `ContextProvider`. |
| 29 | +- Navbar hides Sign In/Sign Out until hydration completes. |
| 30 | +- Shows a pulsing placeholder instead of flashing between auth states. |
| 31 | + |
| 32 | +Why: |
| 33 | + |
| 34 | +- Prevents hydration flicker and auth-button state jumps. |
| 35 | + |
| 36 | +### 3) Suspense boundaries were minimized but kept where required |
| 37 | + |
| 38 | +Kept: |
| 39 | + |
| 40 | +- `Suspense` around `PostHogPageView` in layout. |
| 41 | +- `Suspense` around `ItemDisplayList` on home. |
| 42 | +- `Suspense` around async settings content in `/settings`. |
| 43 | + |
| 44 | +Removed: |
| 45 | + |
| 46 | +- Extra page-level `Suspense` around `LazyMap` section (not needed, `dynamic(..., { ssr: false, loading })` already handles client loading). |
| 47 | + |
| 48 | +Why: |
| 49 | + |
| 50 | +- With `cacheComponents`, Next.js requires some dynamic reads to be under `Suspense`. |
| 51 | +- Removing all Suspense around `ItemDisplayList` caused a build error (`blocking-route`) because it uses navigation/search param APIs. |
| 52 | + |
| 53 | +### 4) Cache strategy was reduced to minimal behavior |
| 54 | + |
| 55 | +- `getAllItems()` keeps `"use cache"` only. |
| 56 | +- Removed explicit `cacheLife(...)`. |
| 57 | +- Removed `cacheTag(...)`. |
| 58 | +- Item mutations use `revalidatePath("/")` (create/edit/delete/update). |
| 59 | + |
| 60 | +Why: |
| 61 | + |
| 62 | +- This is the smallest setup that still keeps homepage fresh after writes. |
| 63 | +- Matches the request to avoid extra cache complexity. |
| 64 | + |
| 65 | +### 5) CSS animation stability tweaks |
| 66 | + |
| 67 | +- Added animation stabilization hints in global CSS: |
| 68 | + - `animation-fill-mode: both` |
| 69 | + - `backface-visibility: hidden` |
| 70 | + - GPU/compositor hints for animated shell elements |
| 71 | +- Reduced over-broad transition usage on homepage wrappers. |
| 72 | + |
| 73 | +Why: |
| 74 | + |
| 75 | +- Lowers repaint artifacts and dithering during hydration. |
| 76 | + |
| 77 | +## Why `/` Is Static Instead of PPR |
| 78 | + |
| 79 | +Homepage (`/`) is static because: |
| 80 | + |
| 81 | +- It is cacheable (`"use cache"`). |
| 82 | +- It does not perform request-time server APIs like `headers()` at root render. |
| 83 | +- Dynamic client concerns are isolated under required `Suspense`. |
| 84 | + |
| 85 | +Build output reflects this as: |
| 86 | + |
| 87 | +- `/` => `○ Static` |
| 88 | + |
| 89 | +## Why `/settings` Is PPR |
| 90 | + |
| 91 | +`/settings` is partial prerender because: |
| 92 | + |
| 93 | +- It loads user-specific async data (`findPhoneNumber`). |
| 94 | +- That async section is wrapped in `Suspense`, so static shell can stream dynamic content. |
| 95 | + |
| 96 | +Build output reflects this as: |
| 97 | + |
| 98 | +- `/settings` => `◐ Partial Prerender` |
| 99 | + |
| 100 | +## Why It Feels Faster and "Smaller" |
| 101 | + |
| 102 | +Even without a formal bundle analyzer comparison, runtime cost dropped because: |
| 103 | + |
| 104 | +- Root server session blocking was removed. |
| 105 | +- Redundant provider layer was removed. |
| 106 | +- Auth-guess/cookie machinery was removed. |
| 107 | +- Unnecessary Suspense wrappers were removed. |
| 108 | +- Hydration state transitions were simplified. |
| 109 | + |
| 110 | +This reduces: |
| 111 | + |
| 112 | +- work before first meaningful HTML, |
| 113 | +- hydration churn, |
| 114 | +- unnecessary UI state corrections. |
| 115 | + |
| 116 | +## Important Build Constraints Learned |
| 117 | + |
| 118 | +1. With `cacheComponents`, dynamic reads can require `Suspense` to avoid build-time blocking-route errors. |
| 119 | +2. `revalidate` route segment config conflicts with `cacheComponents`. |
| 120 | +3. If using `revalidateTag` in this Next version, it requires a second argument/profile. |
| 121 | +4. Simpler invalidation (`revalidatePath("/")`) is often enough if your freshness scope is homepage-only. |
| 122 | + |
| 123 | +## Net Result |
| 124 | + |
| 125 | +- Build passes. |
| 126 | +- `/` is static and faster to first paint. |
| 127 | +- `/settings` remains PPR for personalized data. |
| 128 | +- Auth button flicker is controlled by hydration gating. |
| 129 | +- Cache behavior is simple and minimal. |
0 commit comments