# 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 `` 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 `` 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 `` 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.