Skip to content

feat(crust): embed a V8 code cache in host Node.js binaries - #555

Merged
chenxin-yan merged 3 commits into
mainfrom
audit/tooling/to10-node-sea-code-cache
Nov 5, 2026
Merged

chenxin-yan merged 3 commits into
mainfrom
audit/tooling/to10-node-sea-code-cache

Conversation

@chenxin-yan

@chenxin-yan chenxin-yan commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

 tsdown exe: { targets: [target],
+             seaConfig: { useCodeCache } }   // true only when the node on PATH is the exact binary being embedded

A Node.js binary compiled its whole bundle on every start. crust build now asks node --build-sea to embed a V8 code cache. V8 accepts the cache only in the exact binary that wrote it, with the same V8 flags. Crust therefore enables it only when the node running the build is byte-identical to the official binary tsdown embeds for the target, and NODE_OPTIONS is unset. In practice that means the host target with a nodejs.org Node (mise, nvm, setup-node). Every other build is unchanged.

crust build prints the decision next to Compiler:, so a developer can see why a binary has no cache:

Code cache: on
Code cache: off (the build's node is not the official v26.13.0 binary)
Code cache: off (NODE_OPTIONS is set)
Code cache: on for linux-x64; off for darwin-arm64 (cross-built)

Evidence

  • 30-command CLI, Node 26.13, Linux x64, hyperfine -N, 120 runs: 255.2 M vs 270.1 M instructions (−5.5%), 46.3 vs 48.1 ms. --help: −15.1 M, −0.8 ms. The binary is 61 KB larger.
  • A Nix build of the same Node version and any NODE_OPTIONS produce useCodeCache: false (verified with DEBUG=tsdown:exe), so no binary ever carries a cache V8 would reject.
  • Real demo CLI, Linux x64: the four lines above, built with a Nix Node 26.13 (same version, different binary), the nodejs.org Node 26.13, a cross target, and NODE_OPTIONS. Through the feat(crust): replace the npm launcher with the binary through execve #542 launcher the cache is still accepted; NODE_OPTIONS=--max-old-space-size=4096 and --enable-source-maps print no warning at run time, --jitless prints the rejection warning.
  • tests/node-exe.integration.test.ts (4/4) checks that the binary prints SEA module code cache accepted exactly when the builder is the embedded binary, never rejected, and that import() of a built-in still works.

Merge Danger

Door: two-way
Blast Radius: Node.js binaries built on the host target with an official Node

A user who runs the binary with V8 flags like --jitless or --expose-gc in NODE_OPTIONS sees Warning: Code cache data rejected. on every start (documented). Common flags such as --max-old-space-size are fine. Host builds read two ~150 MB files (the builder's node and the downloaded one) once per target per build, ~0.4 s, and only when their sizes match. Reverting restores the previous output exactly.

View guided diff Turn on auto-fix

Stack

Stack tooling (10 PRs), merge in order. Each PR targets the branch of the one before it.

  1. fix(crust): forward signals in the npm launcher and refuse to publish a stale stage #516
  2. fix(mcp): validate exclude paths, detect Node SEA binaries, load the SDK lazily #517
  3. fix(skills): safe markdown/frontmatter output, non-flapping auto-repair, lazy UI imports #518
  4. fix(extensions,skills): keep command-tree type checking when completion()/skill() are added #519
  5. fix(utils,crust): clean up detached timed subprocesses when the parent exits #520
  6. feat(crust): precompile Bun binaries to bytecode by default #531
  7. fix(utils,core,crust): keep inherited signal ignores on Bun #540
  8. fix(core): treat a SIGINT relayed within 50 ms as the same Ctrl-C #541
  9. feat(crust): replace the npm launcher with the binary through execve #542
  10. feat(crust): embed a V8 code cache in host Node.js binaries #555 ← this PR

This was referenced Nov 3, 2026
@greptile-apps

greptile-apps Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no blocking defects were found.

Summary

Node.js binary builds now embed a V8 code cache when the selected Node matches the official host binary and NODE_OPTIONS is empty.

  • Eligible Node binaries skip compiling their bundle at startup.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Download target Node] --> B{NODE_OPTIONS empty?}
  B -->|No| E[Disable code cache]
  B -->|Yes| C{Host target?}
  C -->|No| E
  C -->|Yes| D{Node binaries identical?}
  D -->|No| E
  D -->|Yes| F[Enable code cache]
  E --> G[Print decision and build]
  F --> G
Loading

Reviews (23) · Last reviewed commit: "test(crust): compare the embedded Node w..." · Reviewed by Greptile

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/crust/src/node-exe.ts Resolved
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📦 Package size

Bundle cost (per entrypoint, minified + gzip)

Entry Base Head Δ
@crustjs/core 13.63 KB 13.63 KB ±0
@crustjs/core/tooling 3.92 KB 3.92 KB ±0
@crustjs/create 3.61 KB 3.61 KB ±0
@crustjs/effect 1.35 KB 1.35 KB ±0
@crustjs/env 1.10 KB 1.10 KB ±0
@crustjs/extensions 16.94 KB 16.94 KB ±0
@crustjs/man 1.43 KB 1.43 KB ±0
@crustjs/mcp 82.41 KB 82.41 KB ±0
@crustjs/progress 6.39 KB 6.39 KB ±0
@crustjs/prompts 10.40 KB 10.40 KB ±0
@crustjs/prompts/testing 2.37 KB 2.37 KB ±0
@crustjs/skills 18.95 KB 18.95 KB ±0
@crustjs/store 3.02 KB 3.02 KB ±0
@crustjs/style 5.72 KB 5.72 KB ±0
@crustjs/testing 0.22 KB 0.22 KB ±0
@crustjs/testing/interactive 0.54 KB 0.54 KB ±0
@crustjs/tui 0.62 KB 0.62 KB ±0
@crustjs/utils/artifacts 0.76 KB 0.76 KB ±0
@crustjs/utils/error 0.12 KB 0.12 KB ±0
@crustjs/utils/json 0.11 KB 0.11 KB ±0
@crustjs/utils/path 0.15 KB 0.15 KB ±0
@crustjs/utils/primitive 0.16 KB 0.16 KB ±0
@crustjs/utils/process 1.91 KB 1.91 KB ±0
@crustjs/utils/schema 0.26 KB 0.26 KB ±0
@crustjs/utils/signals 0.32 KB 0.32 KB ±0
@crustjs/utils/source 0.49 KB 0.49 KB ±0
@crustjs/utils/terminal 0.44 KB 0.44 KB ±0

Consumer bundles (minified + gzip)

Fixture apps from scripts/package-size-fixtures/, bundled against the measured tree. cli uses the finished-build define crust build applies (process.env.CRUST_INTERNAL_BUILD).

Fixture Base Head Δ
@crustjs/core cli 11.81 KB 11.81 KB ±0
@crustjs/core error-only 0.29 KB 0.29 KB ±0
@crustjs/core tooling-docs 0.71 KB 0.71 KB ±0
@crustjs/extensions help-only 5.78 KB 5.78 KB ±0

Install size (npm pack)

Runtime footprint = unpacked bytes of the package plus its production dependencies graph, each installed copy counted once. Peer and optional peer dependencies (e.g. typescript) are excluded. These are npm unpacked bytes, not exact disk usage.

Package Tarball Unpacked Δ unpacked Runtime footprint Δ footprint
@crustjs/core 67.61 KB 253.21 KB ±0 294.62 KB ±0
@crustjs/create 7.20 KB 22.11 KB ±0 63.53 KB ±0
@crustjs/effect 4.99 KB 13.92 KB ±0 13.92 KB ±0
@crustjs/env 4.25 KB 10.27 KB ±0 10.27 KB ±0
@crustjs/extensions 25.16 KB 86.33 KB ±0 240.17 KB ±0
@crustjs/man 3.60 KB 8.63 KB ±0 8.63 KB ±0
@crustjs/mcp 8.57 KB 25.10 KB ±0 66.52 KB ±0
@crustjs/progress 5.60 KB 14.76 KB ±0 119.90 KB ±0
@crustjs/prompts 24.00 KB 91.80 KB ±0 196.94 KB ±0
@crustjs/skills 18.01 KB 64.28 KB ±0 275.99 KB ±0
@crustjs/store 13.18 KB 48.71 KB ±0 90.13 KB ±0
@crustjs/style 18.54 KB 63.72 KB ±0 63.72 KB ±0
@crustjs/testing 3.19 KB 7.33 KB ±0 7.33 KB ±0
@crustjs/tui 2.71 KB 6.02 KB ±0 6.02 KB ±0
@crustjs/utils 13.70 KB 41.42 KB ±0 41.42 KB ±0

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

🦋 Changeset detected

Latest commit: d755ed3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@crustjs/crust Patch
create-crust Patch
@crustjs/core Patch
@crustjs/extensions Patch
@crustjs/man Patch
@crustjs/skills Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes Reviewed the cache eligibility check, tsdown SEA integration, regression tests, documentation, and changeset.

  • Cache eligibility: Enables V8 code caching only when the selected Node executable matches the embedded target byte-for-byte and NODE_OPTIONS is absent or empty.
  • Runtime coverage: Adds dynamic built-in import coverage and verifies cache acceptance without rejection warnings.
  • Documentation: Explains eligible builds, expected startup improvements, and warnings caused by runtime V8 flag changes.
  • Validation: All 11 focused unit and integration tests passed on Linux x64 with Node 26.13.0, including the cache-enabled path. All four integration tests also passed with NODE_OPTIONS set, exercising cache opt-out. Cross-platform execution was not tested.

Pullfrog  | View workflow run | Using gpt-6-astra | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes Reviewed the incremental changes in 182fc44 since the previous approval at 124c616.

  • Moved cache eligibility checks: Compared the builder and embedded Node binaries once per target during provisioning instead of once per command.
  • Reused target decisions: Returned the eligibility map to compiler selection and passed each target's explicit boolean into the generated tsdown build script, retaining the same selected Node and captured environment.
  • Validated behavior: Passed all 11 focused unit and integration tests on Linux x64 with Node 26.13.0, including cache acceptance with a byte-identical builder. Passed all four integration tests again with NODE_OPTIONS set, plus the package type-check. Cross-platform execution was not tested.

Pullfrog  | View workflow run | Using gpt-6-astra | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes Reviewed the test-only delta in 3ce3c31 since the previous approval at 182fc44.

  • Corrected builder identification: Queried the selected Node's process.execPath with a bounded subprocess instead of comparing the version-manager shim itself with the embedded binary.
  • Validated executable resolution: Passed a focused probe using both direct Node execution and a forwarding shim. Full SEA integration tests were not rerun in this environment, which has Node 24 rather than the suite's required Node 26 and reported missing Bun during setup.

Pullfrog  | View workflow run | Using gpt-6-astra | 𝕏

@chenxin-yan
chenxin-yan force-pushed the audit/tooling/to10-node-sea-code-cache branch from 96c5d32 to 62160c2 Compare November 4, 2026 19:25
@chenxin-yan
chenxin-yan force-pushed the audit/tooling/to10-node-sea-code-cache branch from 62160c2 to b3f80cf Compare November 4, 2026 19:35
@chenxin-yan
chenxin-yan force-pushed the audit/tooling/to10-node-sea-code-cache branch 2 times, most recently from 6f023d1 to dc22548 Compare November 4, 2026 20:07
@chenxin-yan
chenxin-yan force-pushed the audit/tooling/to10-node-sea-code-cache branch from dc22548 to bb15fa7 Compare November 4, 2026 20:21
@chenxin-yan
chenxin-yan force-pushed the audit/tooling/to10-node-sea-code-cache branch 2 times, most recently from c29e465 to 75a4e08 Compare November 4, 2026 22:11

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes Reviewed the incremental cache-decision reporting changes since the previously reviewed version.

  • Added cache diagnostics: Reported whether caching is enabled, grouped differing decisions by target, and explained disabled caching due to NODE_OPTIONS, cross-building, or a different Node binary.
  • Short-circuited cross-target comparisons: Skipped binary comparison when the target platform or architecture differs from the builder, while retaining explicit === true cache enablement.
  • Updated coverage and documentation: Added formatter tests and a build-output integration assertion, and documented the new diagnostics in the guide and changeset.
  • Validated the delta: Passed all eight focused unit tests and the package type-check after building workspace prerequisites. The four SEA integration tests skipped because this environment has Node 24 and the suite requires Node 26 or later; native SEA and cross-platform execution were not revalidated.

Pullfrog  | View workflow run | Using gpt-6-astra | 𝕏

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@chenxin-yan
chenxin-yan force-pushed the audit/tooling/to10-node-sea-code-cache branch 8 times, most recently from b6ef05d to a26c8ca Compare November 5, 2026 02:38
Base automatically changed from audit/tooling/to9-launcher-execve to main November 5, 2026 02:44
Node.js binaries compiled their whole bundle on every start. tsdown passes
seaConfig through to `node --build-sea`, so the build now sets useCodeCache,
which makes the SEA load the main module from a V8 code cache instead: a
30-command CLI runs ~15 M fewer instructions per start (~5.5%, 1 to 2 ms),
for ~60 KB.

The cache is written by the Node running --build-sea (the node on PATH) and
V8 rejects it, printing "Code cache data rejected." on every start, unless
the embedded binary is that exact build with the same V8 flags. Matching
platform and version is not enough: a Nix build of Node 26.10 produced a
cache the official 26.10 binary rejected, although both report the same
v8.cachedDataVersionTag(). So the build enables it only when the node on
PATH is byte-identical to the official binary tsdown embeds for the target
(which implies the host target) and NODE_OPTIONS is unset (--jitless there
also gets the cache rejected); every other build stays as before.

import() of built-ins works with the cache for ESM main scripts, which is
what Crust builds (Node 26 docs dropped the old import() caveat in
nodejs/node#62678); the integration fixture now exercises it.
Comparing the builder's Node with the binary a target embeds reads both
(~100 MB each), which added ~0.2 s to every command of a multi-command
build. The provisioning script already runs once per build with the same
Node and environment, so it now makes the decision per target and the
build passes it to each command.
@chenxin-yan
chenxin-yan force-pushed the audit/tooling/to10-node-sea-code-cache branch from a26c8ca to e417cb1 Compare November 5, 2026 02:44
The build compares against process.execPath; resolving the node on PATH can
land on a version-manager shim instead of the binary.
@chenxin-yan
chenxin-yan force-pushed the audit/tooling/to10-node-sea-code-cache branch from e417cb1 to d755ed3 Compare November 5, 2026 02:49
@chenxin-yan
chenxin-yan merged commit 2d03498 into main Nov 5, 2026
19 checks passed
@chenxin-yan
chenxin-yan deleted the audit/tooling/to10-node-sea-code-cache branch November 5, 2026 02:54
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