commit 7c12455751a62e4ca5d1e8e62082060c87634b01
parent 2db970950e401e0605ecd2e4ae139a248a60acaf
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 4 Aug 2026 22:38:47 -0400
Record the cacheComponents spike: measured, and the answer is no
The spike's question was whether uncached node:fs I/O gets captured into the
prerendered shell under cacheComponents. The build never gets far enough to
ask: `next build` fails with 49 errors, one per file, because "Route segment
config dynamic is not compatible with nextConfig.cacheComponents" — and all 49
files in editor/app declare force-dynamic. That is every page and every API
route; force-dynamic IS this app's rendering model.
So adoption is not the "~48 x await connection()" the plan estimated. It is
deleting the declaration that currently guarantees dynamic rendering, and THEN
proving per-route that the fs reads are excluded from the prerender — the
original question asked with the safety net removed. For a gain we already
have: sibling navigation is instant from the router cache today.
Recommendation in plans/cache-components-spike.md: do not adopt. Branch
deleted, next.config.ts reverted, nothing merged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat:
1 file changed, 55 insertions(+), 0 deletions(-)
diff --git a/plans/cache-components-spike.md b/plans/cache-components-spike.md
@@ -0,0 +1,55 @@
+# Stage 6 spike: `cacheComponents` — measured, and the answer is no
+
+Ran 2026-08-04 on branch `spike/cache-components` (deleted; nothing merged).
+
+## The question the spike was meant to answer
+
+Next instruments `fetch`, `cookies()`, `headers()`, `params`, `searchParams` —
+**not `node:fs`**. Every loader in this editor is a raw `readFile`/`readdir`. Under
+`cacheComponents`, does uncached fs I/O outside a Suspense boundary get captured into
+the prerendered static shell at build time (→ a frozen admin UI), or is it correctly
+excluded?
+
+## The answer: the build never gets far enough to ask
+
+```
+Route segment config "dynamic" is not compatible with `nextConfig.cacheComponents`.
+Please remove it.
+```
+
+**49 errors, one per file — exactly the 49 files in `editor/app/` that declare
+`export const dynamic = "force-dynamic"`.** `next build` fails outright.
+
+That is every page and every API route in the editor. `force-dynamic` is not incidental
+here; it *is* the app's rendering model. An admin UI over a live corpus with running
+job runners has nothing meaningful to prerender.
+
+## What adoption would actually cost
+
+Not the "~48 × `await connection()`" the plan estimated. It is:
+
+1. Delete `force-dynamic` from all 49 files — removing the one declaration that
+ currently *guarantees* dynamic rendering; then
+2. Prove, per route, that its uncached `node:fs` reads are excluded from the prerender —
+ i.e. the original question, now asked with the safety net removed rather than in
+ place. Getting it wrong ships a frozen admin page that looks fine in dev.
+3. Plus the costs already known: `use cache` has no correct invalidation key here
+ (writes happen in `common/` job runners that cannot import `next/cache`), and
+ `Activity`-based navigation keeps routes mounted and re-runs effects, needing
+ re-verification of the ~20 `router.refresh()`-on-mount components and much of the
+ 407-spec suite.
+
+## What it would buy: nothing we don't already have
+
+Sibling navigation is already instant — measured: with the destination prefetched, the
+click commits from the router cache with no pending state at all. The one thing
+`cacheComponents` would add is `unstable_instant`, which *guarantees* a loading fallback
+when the destination is NOT prefetched (see `editor/e2e/navigation.spec.ts` for why
+`loading.tsx` alone does not). That is a narrow gain for a migration that starts by
+deleting the app's rendering model.
+
+**Recommendation: do not adopt.** Revisit only if the editor grows nested layouts with
+their own data (there is exactly one layout today and zero nesting), which is where
+`unstable_instant`'s cross-layout validation earns its keep.
+
+Taken from those docs regardless, and already shipped: `loading.tsx` and `staleTimes`.