Performance follow-ups

This commit is contained in:
adilallo
2026-05-26 07:24:36 -06:00
parent 3be188a3cc
commit eded97559d
16 changed files with 432 additions and 72 deletions
+160
View File
@@ -0,0 +1,160 @@
# Next 16 substrate evaluation (Phase 3)
Evaluation of `experimental.cacheComponents` (formerly `experimental.ppr`)
and React Compiler against this repo on Next.js 16.2.6. Performed as a
canary build pass without committing either flag to `main`.
## TL;DR
| Flag | Recommendation | Why |
| --- | --- | --- |
| `cacheComponents` (PPR successor) | **Defer** — requires a follow-up refactor before it can ship | Renamed from `ppr` in Next 16; now a boolean global toggle, no per-route `experimental_ppr` opt-in. Requires removing `force-dynamic` from `(app)` and `(admin)` layouts and re-expressing session-aware dynamism via Suspense + cache primitives. |
| React Compiler | **Defer** — config surface moved + missing dep | Moved out of `experimental` to the top-level `reactCompiler` key in Next 16. Requires installing `babel-plugin-react-compiler`. No blocking codebase incompatibilities found in the canary surface, but the install + eslint plugin setup is its own follow-up task. |
Neither flag was shippable as a pure config flip in this audit. The findings
below describe what changed in Next 16 and the work each would require.
## Repo baseline (Next 16.2.6, Turbopack, no experimental flags)
- Build status: clean (`npx next build`)
- Static routes: `/`, `/_not-found`, `/about`, `/blog`, `/components-preview`,
`/how-it-works`, `/learn`, `/templates`, `/use-cases`
- SSG routes: `/blog/[slug]`, `/use-cases/[slug]`, `/use-cases/[slug]/rule`
- Dynamic routes: all `/api/*`, `/create`, `/create/[screenId]`,
`/create/review-template/[slug]`, `/login`, `/monitor`, `/profile`,
`/rules/[id]`
- `.next/static` total: **3.6 MB** (uncompressed)
Note: Next 16 with Turbopack no longer prints per-route first-load JS sizes
in the build summary. Bundle analyzer (`ANALYZE=true`) is the canonical
source for size data — see Phase 4a.
## 3a. `cacheComponents` (PPR) — DEFER
### What changed in Next 16
`experimental.ppr` has been merged into `experimental.cacheComponents`:
```
Error: experimental.ppr has been merged into cacheComponents. The Partial
Prerendering feature is still available, but is now enabled via cacheComponents.
```
Crucially, the per-route incremental opt-in is gone:
```
cacheComponents: invalid type: string "incremental", expected a boolean
```
So `cacheComponents: true` flips PPR semantics on globally for every route.
### Blocker
With `cacheComponents: true`, the build fails:
```
./app/(admin)/layout.tsx:6:14
Route segment config "dynamic" is not compatible with `nextConfig.cacheComponents`.
Please remove it.
./app/(app)/layout.tsx:8:14
Route segment config "dynamic" is not compatible with `nextConfig.cacheComponents`.
Please remove it.
```
Both layouts use `export const dynamic = "force-dynamic"` to render
session-aware chrome (set in Phase 4b of the prior plan). `cacheComponents`
requires expressing that dynamism via `<Suspense>` boundaries plus
`unstable_noStore()`/`unstable_cache()` instead of route-segment `dynamic`.
### Estimated work to ship
1. Refactor [app/(app)/layout.tsx](../../app/(app)/layout.tsx) and
[app/(admin)/layout.tsx](../../app/(admin)/layout.tsx) so the
`ConditionalNavigation` session fetch sits inside a `<Suspense>` boundary
with a fallback that matches the generic chrome.
2. Mark the session-reading components with `unstable_noStore()` (or the
stable equivalent in Next 16) so they opt out of the static cache.
3. Verify the existing static routes (`/`, `/about`, `/blog`, etc.) still
prerender; add `<Suspense>` boundaries around any future dynamic islands.
4. Confirm `(marketing)` routes still serve from CDN with the static shell
while the personalized nav island streams.
This is the natural next step after Phase 4b made marketing static, but
it's not a config-only change. Ticket separately.
### Verification (when shipping)
- `(marketing)` routes still appear as `○ Static` in build output.
- `(app)`/`(admin)` routes' static shell prerenders; the personalized nav
streams (visible in `curl` of the HTML — partial shell first, then nav).
- TTFB on `(marketing)` unchanged or improved.
## 3b. React Compiler — DEFER
### What changed in Next 16
`experimental.reactCompiler` moved to the top-level `reactCompiler` key:
```
⚠ `experimental.reactCompiler` has been moved to `reactCompiler`. Please
update your next.config.mjs file accordingly.
```
And requires the babel plugin to be installed:
```
Failed to resolve package babel-plugin-react-compiler while attempting to
resolve React Compiler. We attempted to resolve React Compiler relative
to the next package. Is babel-plugin-react-compiler installed in your
node_modules directory?
```
### Estimated work to ship
1. `npm install --save-dev babel-plugin-react-compiler eslint-plugin-react-compiler`.
2. Add `reactCompiler: { compilationMode: "annotation" }` to `next.config.mjs`
(top-level, not under `experimental`).
3. Enable `eslint-plugin-react-compiler` and run it against the repo to
surface components that would bail (refs mutated during render, reads
of non-reactive globals inline, etc.).
4. Incrementally add `"use memo"` directives to high-render-frequency
containers (`CreateFlowProvider`, `AuthModalProvider`, list-heavy views).
5. Once stable, flip `compilationMode: "all"` and remove hand-written
`useMemo`/`useCallback` where the compiler subsumes them.
### Why annotation mode first
We have many hand-rolled memoized containers. The risk of `compilationMode: "all"`
on day one is that the compiler bails on a critical component in a way that
changes render counts. Annotation mode lets us migrate one component at a
time with eslint enforcement.
### Verification (when shipping)
- Bundle size before/after `next build` with the runtime added.
- Test suite green (`npx vitest run` — 196 files / 1251 tests today).
- Component render counts unchanged or reduced on key surfaces (use the
React DevTools profiler on `/create/informational` and `/`).
## Impact on Phase 4 (MessagesProvider)
If we later ship `cacheComponents`, the MessagesProvider refactor's win
shrinks meaningfully: the messages dictionary lives in the static shell of
every route, and only the dynamic island re-fetches. The static prerender
output is already cacheable at the CDN. So Phase 4 should be re-evaluated
**after** the `cacheComponents` work lands, not before.
If we don't ship `cacheComponents`, Phase 4's bundle-size measurement
(Phase 4a) is still the right gate — measure first, refactor only if the
data justifies it.
## What to do now
- Skip both flags for this performance follow-ups plan.
- File two follow-up tickets:
1. "Enable `cacheComponents`: refactor `(app)`/`(admin)` layouts to
Suspense + cache primitives, remove `force-dynamic` from route segments."
2. "Adopt React Compiler in annotation mode: install plugin, enable
eslint rule, migrate top containers."
- Proceed with Phase 4a (measure) and let the data drive Phase 4b.