Skip to content

perf(dev): re-enable the Turbopack dev filesystem cache (5.4x faster restarts) - #6151

Merged
waleedlatif1 merged 7 commits into
stagingfrom
perf/dev-fs-cache
Aug 1, 2026
Merged

perf(dev): re-enable the Turbopack dev filesystem cache (5.4x faster restarts)#6151
waleedlatif1 merged 7 commits into
stagingfrom
perf/dev-fs-cache

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Why

experimental.turbopackFileSystemCacheForDev has been false since #5408 — a landing-page homepage redesign. That PR's description covers hero cards, feature-card aspect ratios, eyebrow chips and a voice-input button color. It never mentions Turbopack, caching, or dev performance. It was collateral, and it overrode the Next default (true since v16.1).

It is not the flag #6078/#6080 measured. That A/B was turbopackFileSystemCacheForBuild, and its conclusion stands — the build cache is a 3.2x regression and stays off in this PR. The two flags look alike and are opposite decisions; both now carry comments saying so, so neither gets flipped from the other's evidence.

Forensic corroboration: the abandoned cache on my machine last wrote Jul 7 17:51; #5408 was authored Jul 7 18:02 PDT.

Measurement

/workspace/[workspaceId]/w (the canvas), n=3 per arm, dev server stopped with SIGINT between every run:

restart compile dev-server RSS
cache off (current) 31.4s / 30.1s / 31.9s 9.0–9.8 GB
cache on (this PR) 5.6s / 5.6s / 5.5s 4.4–5.1 GB

5.4x faster restarts, ~2x less resident memory. Ranges do not overlap.

Cold compile against an empty cache is unchanged (~32s either way) — the cache only pays back on restart, which is the loop that actually hurts.

Cache growth

The dev cache is unbounded on disk. The abandoned one here had reached 78 GB across 1,848 SST files, and a stale cache is slower to read back, so left alone it erodes the win it exists to provide.

scripts/prune-turbopack-cache.ts runs on predev (and predev:minimal / predev:capped) and drops the cache once it crosses a cap — default 20 GB, SIM_TURBOPACK_CACHE_MAX_GB to override. bun run dev:cache:prune forces it. It never blocks next dev on a maintenance failure.

Verified: keeps under cap, prunes over cap, --dry-run, --force, invalid-env fallback, and that Bun fires pre* hooks.

Skill

Adds .agents/skills/dev-performance recording the cost model (next dev compiles per route on demand — startup time is meaningless), reference numbers, the two opposite cache flags, and the benchmarking method.

Including one trap worth reading: stopping the dev server with kill -9 mid-cache-write discards the cache, which makes this exact win read as no win. It produced a false negative during the investigation.

Test plan

  • n=3 per arm A/B, SIGINT restarts, non-overlapping ranges
  • prune script: under cap / over cap / dry-run / force / invalid env
  • bun run skills:check in sync
  • biome clean

…restarts)

`turbopackFileSystemCacheForDev` has been `false` since #5408 — a landing-page
homepage redesign whose description covers hero cards, feature-card aspect
ratios, eyebrow chips and a voice-input button color, and never mentions
Turbopack, caching, or dev performance. It was collateral, not a decision, and
it overrode the Next default (true since v16.1).

It is not the flag #6078/#6080 measured. That A/B was `...ForBuild` and its
conclusion stands — the build cache is a 3.2x regression and stays off. The two
flags look alike and are opposite decisions; both are now commented as such.

Measured on `/workspace/[workspaceId]/w`, n=3 per arm, SIGINT between runs:

  cache OFF   31.4s / 30.1s / 31.9s   RSS 9.0-9.8 GB
  cache ON     5.6s /  5.6s /  5.5s   RSS 4.4-5.1 GB

5.4x faster restarts, ~2x less resident memory. Cold compile against an empty
cache is unchanged (~32s either way) — the cache only pays back on restart,
which is the loop that actually hurts.

The cache is unbounded on disk: the abandoned one on this machine had reached
78 GB across 1,848 SST files, and a stale cache is slower to read back, so left
alone it erodes the win it exists to provide. `prune-turbopack-cache.ts` runs on
`predev` and drops it past a cap (default 20 GB, `SIM_TURBOPACK_CACHE_MAX_GB` to
override); `bun run dev:cache:prune` forces it. It never blocks `next dev` on a
maintenance failure.

Adds a `dev-performance` skill recording the cost model, the reference numbers,
and the benchmarking method — including that stopping the server with `kill -9`
mid-cache-write discards the cache and makes this exact win read as no win.
@waleedlatif1
waleedlatif1 requested a review from a team as a code owner August 1, 2026 05:54
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 1, 2026 6:07pm

Request Review

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes local dev behavior and Next experimental Turbopack settings; production build cache stays off. Pruning only affects .next/dev/cache and is designed not to block dev startup on errors.

Overview
Re-enables turbopackFileSystemCacheForDev in apps/sim/next.config.ts (was false since an unrelated landing-page PR). Measured on the canvas route: ~5.4× faster dev-server restarts and roughly half the RSS versus cache off; cold compile with an empty cache stays ~32s. Comments explicitly separate this from turbopackFileSystemCacheForBuild, which remains off.

Adds scripts/prune-turbopack-cache.ts to delete the unbounded Turbopack dev cache when it exceeds a cap (default 20 GB, SIM_TURBOPACK_CACHE_MAX_GB). Wired via predev / predev:minimal / predev:capped and dev:cache:prune (--force); failures only warn so next dev still starts.

Documents the dev cost model, benchmark method (SIGINT not kill -9, n≥3, RSS), and cache-flag rules in .agents/skills/dev-performance and mirrored .claude/commands / .cursor/commands content.

Reviewed by Cursor Bugbot for commit 4aa07e5. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR re-enables Turbopack's persistent development cache and adds automatic size-based pruning plus development-performance guidance. The pruning integration misses the documented root-level app-only startup path.

  • Enables turbopackFileSystemCacheForDev while retaining the disabled build cache.
  • Adds a Bun script that removes the Turbopack cache above a configurable size cap.
  • Adds lifecycle hooks for direct apps/sim development commands and a forced-prune command.
  • Adds synchronized performance-diagnostics guidance for supported coding assistants.

Confidence Score: 4/5

The root-level app-only development path must be wired to prune the newly enabled cache before this PR is safe to merge.

The cache is explicitly unbounded, but the documented root bun run dev path reaches the app through Turbo and bypasses the package-level predev hook intended to enforce the cap.

Files Needing Attention: apps/sim/package.json and the root Turbo dev orchestration

Important Files Changed

Filename Overview
apps/sim/next.config.ts Re-enables the development filesystem cache while preserving the independently disabled build cache.
scripts/prune-turbopack-cache.ts Implements configurable cache-size measurement and nonblocking recursive pruning at the expected Next development-cache path.
apps/sim/package.json Adds pruning hooks for direct package commands, but the root Turbo-based app-only command bypasses them.
.agents/skills/dev-performance/SKILL.md Documents the development cost model, cache decisions, and benchmarking procedure.
.claude/commands/dev-performance.md Mirrors the development-performance guidance for Claude commands.
.cursor/commands/dev-performance.md Mirrors the development-performance guidance for Cursor commands.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Developer runs root bun run dev] --> B[turbo run dev]
  B --> C[apps/sim dev task]
  C --> D[next dev]
  B -. lifecycle hook skipped .-> E[predev prune script]
  D --> F[Unbounded Turbopack cache remains uncapped]
Loading

Reviews (1): Last reviewed commit: "perf(dev): re-enable the Turbopack dev f..." | Re-trigger Greptile

Comment thread apps/sim/package.json Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Stack overview

This is the base of a five-PR stack. Combined, measured end-to-end on the canvas route (n=3 per arm, 14-core / 48 GB Mac):

staging full stack
dev restart compile 30.1 / 31.4 / 31.9 s 4.3 / 4.3 / 4.4 s — 7.2×
dev cold compile 30.1 / 31.4 / 32.3 s 21.6 / 22.2 / 22.4 s
dev-server RSS 9.0–12.5 GB ~4.0 GB — 2.4×
canvas module graph 6,592 1,908
workspace layout graph 5,751 1,063
prod client JS (gzip) 26.09 MB 23.68 MB — −9.2%
PR what
#6151 (this) re-enable the dev filesystem cache + cap its growth
#6152 move mergeToolParameters to a registry-free leaf
#6153 generate serializable tool metadata artifacts
#6155 read metadata instead of the registry on client paths
#6156 CI guard so it can't regress

The two halves are independent and compose: #6151 makes restarts cheap, #6155 makes the graph small. Either is worth landing alone.

Behaviour neutrality: full suite 17,277 passed, plus a 4,404-probe differential comparing old registry reads against new metadata reads across every tool (resolve / existence / params / outputs / name) — 0 mismatches.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 4aa07e5. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 4aa07e5. Configure here.

… predev hook

Review read the root `bun run dev` path as bypassing the `predev` hook and so
never capping the newly-enabled cache. Turbo does fire `pre*` hooks — verified
live, the run prints the prune before `next dev` — but the concern is fair in
that the guarantee rested on package-manager lifecycle semantics that are
invisible at the call site.

Chaining it explicitly removes the question entirely: every `dev` variant now
runs `bun run dev:cache:cap && …`, which holds on any invocation path, is
visible in the command itself, and drops the three duplicated `predev:*` entries
for one shared script.

Verified on both paths — direct `bun run dev` and root `turbo run dev`, the
latter printing:

  sim:dev: $ bun run dev:cache:cap && next dev --port 3000
  sim:dev: $ bun run ../../scripts/prune-turbopack-cache.ts
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Note on the 4/5 score above

That review ran at ~06:00 UTC against an earlier head; the branch is now at a later commit and Greptile has not re-run despite repeated triggers (it does not appear to re-review on force-push for these stacked bases). Flagging it so the stale score isn't read as an open concern.

Its stated reason — the predev hook not capping the cache on the root bun run dev path — has since been fixed, with each fix verified rather than asserted. Every review thread on this PR is resolved, and the reasoning is in the thread replies.

The full Test and Build workflow was dispatched against the stack head and passed. Note that ci.yml only fires for PRs targeting main/staging/dev, so stacked PRs don't get it automatically — worth re-checking when each retargets to staging on merge.

…he cache

Stress-tested the failure mode rather than assuming it: deliberately corrupting
an SST block makes Turbopack abort with a FATAL panic — it does not self-heal.

  FATAL: An unexpected Turbopack error occurred.
  Cache corruption detected: checksum mismatch in block 4 of 00000221.sst

`bun run dev:cache:prune` and restart fixes it; verified the canvas serves 200
again afterwards. Documented in the skill and in the script's header, since the
symptom is a hard crash and the remedy is not guessable.

This is the honest cost of turning the cache on. It is worth paying — a 5.4x
faster restart against a rare, loud, single-command failure — but it should be
written down rather than discovered.

Worth distinguishing from the adjacent case: an ordinary hard kill does *not*
corrupt the cache. Turbopack discards a partially-written cache and rebuilds it
silently, which is exactly why a `kill -9`-based benchmark reads as "no cache
win" (noted in the benchmarking section).
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

bun run dev:full — the command devs actually run

Measured end-to-end on the full stack vs staging: start dev:full (app + realtime), request the canvas route, stop with SIGINT, repeat.

staging full stack
cold (first ever) 31.3 s / 11.43 GB 21.7 s / 10.19 GB
restart 1 29.7 s / 11.50 GB 4.2 s / 4.15 GB
restart 2 31.2 s / 11.46 GB 4.2 s / 4.10 GB
restart 3 30.4 s / 11.44 GB 4.1 s / 4.06 GB

7.3× faster (30.4 s → 4.2 s avg) and 2.8× less memory (11.46 GB → 4.10 GB), on every restart after the first. Realtime came up in every run; the canvas returned 200 every time.

The app itself is faster too, not just compilation

Warm server (fully compiled before sampling), median of 9 requests per route:

route scripts before → after HTML before → after median TTFB
/w 206 → 138 (−33%) 1151 → 1045 KB 123 → 108 ms
/logs 190 → 122 (−36%) 345 → 231 KB (−33%) 109 → 93 ms
/tables 170 → 102 (−40%) 336 → 222 KB (−34%) 127 → 109 ms
/files 200 → 132 (−34%) 349 → 235 KB (−33%) 145 → 128 ms

33–40% fewer script requests per page load — 68 fewer on the canvas — and a third less HTML, because the client-reference manifest shrank with the module graph. Alongside the −9.2% gzipped production bundle, this is a user-facing win, not only a developer one.

@waleedlatif1
waleedlatif1 changed the base branch from staging to main August 1, 2026 17:11
@waleedlatif1
waleedlatif1 changed the base branch from main to staging August 1, 2026 17:12
…he code

A whole skill was too much for what this is. The parts that are load-bearing —
why the two lookalike cache flags are opposite decisions, the measured numbers,
the corruption remedy, and the benchmarking trap — now live in the config and
script they describe, where someone changing the flag actually reads them.

The trap is the piece worth keeping: `next dev` compiles on demand so startup
time is meaningless, and stopping the server with `kill -9` makes Turbopack
discard a partially-written cache and rebuild silently — which reads as 'the
cache does nothing' and is how this flag stayed wrong for a month.

Dropped rather than relocated: generic advice that was not specific to this repo
(antivirus, Docker-on-macOS, orphaned processes) and a measured no-op
(`optimizePackageImports` for lucide-react changed nothing, 31.6s vs 31.7s).
…e pruning

Stress-tested the maintenance path rather than assuming it is free.

Cost: the size walk is ~30ms on a real cache and ~85ms at 2,000 files — under 2%
of a 4.2s warm restart, and invisible against a cold one. It runs before every
dev start, so it needed to be cheap; it is.

Concurrency: pruning while a dev server is live (which happens when a second
server is started from the same checkout) does not crash it. The running server
keeps its in-memory state and kept serving HTTP 200 with zero panics. It does
stop persisting for the rest of that session, so its next start is cold once —
verified recovering at 23.4s then 4.5s. Worth writing down because the directory
silently never reappears mid-session, which looks like a bug if you go looking.

The cap is a backstop, not routine: a normal session sits at 1-2 GB against a
20 GB default.
`apps/docs` is a Next app too (`next dev --port 3001`) and overrides nothing, so
it uses the Next default where the dev filesystem cache is on. It already had an
uncapped 1.1 GB cache here, and the root `bun run dev` (`turbo run dev`) starts
it — so a teammate using the documented command was accumulating a cache nothing
would ever prune.

The script now resolves its target from the working directory instead of
hardcoding `apps/sim`, and each app chains its own cap. Per-app rather than one
sweep on purpose: a single pass would let one app's dev start delete a cache
another app is holding open, which costs that session its persistence.

Verified both: `apps/sim` and `apps/docs` each report and cap their own 1.1 GB
cache, and both dev servers start clean (`Ready in 299ms` / `229ms`, docs serving).
`dev:cache:prune` duplicated `dev:clean`, which already existed in `apps/sim` and
does strictly more (`rm -rf .next/dev/cache` covers the Turbopack cache plus the
fetch and image caches). Two commands for one job is worse than one, and the
docs pointed at the newer, narrower of the two.

Removes it from both apps and gives `apps/docs` the `dev:clean` that `apps/sim`
already had, so the recovery command is the same everywhere. `dev:cache:cap`
stays — it is the chained step, used by more than one dev variant, and naming it
keeps the relative script path out of each command.

Verified `dev:clean` is a real remedy: corrupt a cache block, run it, restart —
canvas serves 200 with no panic.

Also corrects an overstatement. A damaged cache does not *always* abort
Turbopack; whether it panics depends on whether the damaged region is read, so
it is not reliably reproducible. Both notes now say "can abort" and give the same
remedy either way.
@waleedlatif1
waleedlatif1 merged commit 28abbc9 into staging Aug 1, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant