6.8 KiB
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/statictotal: 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
- Refactor app/(app)/layout.tsx and
app/(admin)/layout.tsx so the
ConditionalNavigationsession fetch sits inside a<Suspense>boundary with a fallback that matches the generic chrome. - Mark the session-reading components with
unstable_noStore()(or the stable equivalent in Next 16) so they opt out of the static cache. - Verify the existing static routes (
/,/about,/blog, etc.) still prerender; add<Suspense>boundaries around any future dynamic islands. - 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○ Staticin build output.(app)/(admin)routes' static shell prerenders; the personalized nav streams (visible incurlof 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
npm install --save-dev babel-plugin-react-compiler eslint-plugin-react-compiler.- Add
reactCompiler: { compilationMode: "annotation" }tonext.config.mjs(top-level, not underexperimental). - Enable
eslint-plugin-react-compilerand run it against the repo to surface components that would bail (refs mutated during render, reads of non-reactive globals inline, etc.). - Incrementally add
"use memo"directives to high-render-frequency containers (CreateFlowProvider,AuthModalProvider, list-heavy views). - Once stable, flip
compilationMode: "all"and remove hand-writtenuseMemo/useCallbackwhere 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 buildwith 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/informationaland/).
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:
- "Enable
cacheComponents: refactor(app)/(admin)layouts to Suspense + cache primitives, removeforce-dynamicfrom route segments." - "Adopt React Compiler in annotation mode: install plugin, enable eslint rule, migrate top containers."
- "Enable
- Proceed with Phase 4a (measure) and let the data drive Phase 4b.