internal/analysis/vm-script-caching-investigation-2026-06-07.md
Related issue: #3282
| Area | Status | Finding |
|---|---|---|
| vm.Script caching benefit | Not supported by current evidence | Exploratory microbenchmarks show same-source cache hits and colder unique-source compiles behave very differently |
| Hypothesis from #3282 | Not demonstrated as raw compile cost | The original ~3ms observation was not reproduced with its original harness |
| Implementation effort | Not justified without stronger signal | Adds cache invalidation and memory-management complexity before a production-like win is proven |
Issue #3282 proposed caching pre-compiled vm.Script objects to save the ~3ms "per-request JS-parse cost" observed in profiling. This note measured isolated vm.Script compilation, cached execution, and repeated vm.runInContext execution on macOS arm64. It did not reproduce the original #3282 profiling harness or a Linux x86_64 production-like environment.
Directly measured in this investigation:
Inferred, not directly measured here:
new vm.Script(...) compilation, such as React execution, props handling, context setup, or profiler warmup effects.Caching vm.Script is most likely to matter when most of these heuristics hold:
The render template in react_on_rails_pro does real work (React rendering and props serialization). The available evidence says execution is likely to dominate; it does not prove that every production workload has zero measurable benefit from vm.Script caching.
The original local experiment bundle was an ephemeral tmp/ path and was not retained. A narrow standalone reproduction script is now committed as internal/analysis/vm-script-caching-repro-2026-06-13.mjs. It is intended to reproduce the shape of the microbenchmark, not to establish portable production throughput. The example reproduction output below was captured on an Apple M5 Max host, not the original Apple M1 host.
| Code Size | Compile Time | Cached Exec | RunInContext Exec | Speedup | Interpretation |
|---|---|---|---|---|---|
| 60 chars | 0.54μs | 0.62μs | 1.37μs | 2.2x | Trivial script |
| 8 KB | 0.54μs | 38.04μs | 38.54μs | 1.01x | Noise-band difference |
| 69 KB | 2.75μs | 400.12μs | 404.00μs | 1.01x | Noise-band difference |
| 650 KB | 0.83μs | 2016.46μs | 2016.25μs | 1.00x | Internally inconsistent row |
Observation: For scripts with real execution time (38μs+), cached execution and repeated same-source vm.runInContext differed by mostly <1%. The 650 KB compile median is lower than the 69 KB median, so it should be treated as timer and benchmark noise. This table does not support a precise per-MB compilation-throughput claim.
Different AST patterns were tested to find maximum parse cost:
| Pattern | Code Size | Compile Time | 1ms Threshold |
|---|---|---|---|
| Nested objects | 2.7 KB | 1.0μs | Not reliably estimated here |
| Generators | 9 KB | 1.2μs | Not reliably estimated here |
| Class definitions | 16 KB | 1.4μs | Not reliably estimated here |
| Regex literals | 29 KB | 1.8μs | Not reliably estimated here |
| String literals | 111 KB | 3.8μs | Not reliably estimated here |
Observation: These generated AST patterns also landed in the low-microsecond range, but the data is too noisy to estimate a production 1ms compile threshold from this table.
How much time is saved across N executions of a trivial script (32 chars)?
| Executions | Cached Total | RunInContext Total | Time Saved |
|---|---|---|---|
| 10 | 15μs | 33μs | 18μs |
| 100 | 112μs | 183μs | 71μs |
| 1,000 | 627μs | 1,410μs | 784μs |
| 10,000 | 3,844μs | 11,043μs | 7.2ms |
Observation: Need ~10,000 executions of trivial scripts to save 7ms total. The render template runs once per request — no amortization possible.
Scripts that do real work (object creation, loops, array operations):
| Script Type | Cached/exec | RunInContext/exec | Speedup |
|---|---|---|---|
| Trivial (return 1+1) | 0.37μs | 1.05μs | 2.8x |
| Light (loop 10x) | 0.47μs | 1.17μs | 2.5x |
| Medium (100 sqrt calls) | 12.87μs | 13.80μs | 1.07x |
| Heavy (1000 objects) | 39.15μs | 39.99μs | 1.02x |
Observation: In these synthetic cases, once execution time exceeded a few microseconds, caching benefit dropped into measurement noise.
Issue #3282 stated:
Per-request JS-parse cost: ~3 ms
This investigation did not rerun the original profiling harness, so it cannot prove what the original number contained. Based on the isolated microbenchmarks above, the ~3ms "parse" attribution likely included one or more non-compile costs:
vm.createContext cost, not vm.Script compile)The corrected conclusion is narrower: the available isolated measurements did not reproduce a raw vm.Script compile cost anywhere near 3ms. They do not, by themselves, prove that a 200-byte post-#3281 render template would always compile in less than 1μs on every runtime and host.
Caching helps when:
// Config literals — 2-3x faster
const config = new vm.Script(`({ apiUrl: "...", timeout: 5000 })`);
// Simple expressions — 2-3x faster
const formula = new vm.Script(`price * quantity * (1 - discount)`);
// Feature flags — 2x faster
const check = new vm.Script(`user.role === "admin"`);
Caching does NOT help when:
// Data transformation — 1.05x (no real gain)
const transform = new vm.Script(`
data.map(item => ({ ...item, computed: heavy(item) }))
`);
// React rendering — 1.02x (execution dominates)
const render = new vm.Script(`
ReactDOMServer.renderToString(React.createElement(App, props))
`);
| Condition | Cache signal |
|---|---|
| Script execution near 1μs | Strongest chance of a visible win |
| Script execution above ~10μs | Usually diluted by execution cost |
| Script runs 1x per request | Usually not enough reuse by itself |
| Script is pure config/expression | More plausible |
| Script does React rendering | Needs production-like proof first |
Keep issue #3282 closed unless new production-like evidence shows a material win.
The current evidence does not justify adding vm.Script caching to the renderer. It points toward React rendering and props handling as better performance targets:
Adding vm.Script caching without stronger evidence would:
Benchmark scripts and raw data are captured in this committed analysis note:
internal/analysis/vm-script-caching-repro-2026-06-13.mjstmp/ path.Later runs on the same host varied with source shape, V8's compilation cache, GC pauses, and sampling noise; run the script above for current local values. The compile measurements allocate many temporary vm.Script objects, so treat adjacent execution timings as directional rather than as stable benchmark numbers. The same-source/precompiled ratio is the relevant comparison only for a renderer path whose request source text is stable across calls. Current render paths that interpolate props or other request-specific text should be compared against the unique-source/precompiled contrast or a production-like benchmark.
vm.Script caching reproduction
==============================
Node: v22.12.0
Platform: darwin arm64
CPU: Apple M5 Max
| Size | Code Len | Samples | Same-source Compile | Unique-source Compile | Cached Exec | Same-source Run | Unique-source Run | Same/Precompiled | Unique/Precompiled |
| ------ | -------- | ------- | ------------------- | --------------------- | ----------- | --------------- | ----------------- | ---------------- | ------------------ |
| tiny | 54 | 5000 | 0.46us | 3.08us | 0.96us | 1.46us | 4.38us | 1.52x | 4.57x |
| small | 379 | 3000 | 0.42us | 7.67us | 1.67us | 2.17us | 9.88us | 1.30x | 5.92x |
| medium | 8323 | 1000 | 0.79us | 137.44us | 31.58us | 33.75us | 195.25us | 1.07x | 6.18x |
| large | 72453 | 300 | 2.50us | 1797.06us | 283.71us | 304.02us | 2131.60us | 1.07x | 7.51x |
| huge | 662053 | 80 | 14.08us | 12638.65us | 2943.60us | 2917.54us | 16437.29us | 0.99x | 5.58x |
Note: Same-source compile uses V8/Node compilation-cache behavior by default.
Same-source run also benefits from that cache after warmup.
Unique-source compile/run varies the source text each sample to show a colder path.
Same/Precompiled compares stable source text; Unique/Precompiled is colder-path contrast.
Run with `node --no-compilation-cache` to compare with V8 compilation caching disabled.
The original output label used Uncached Med, but repeated same-source vm.runInContext can benefit from V8's compilation cache after warmup. Treat that column as same-source vm.runInContext timing, not a demonstrated cold compile path.
vm.Script Caching Benchmark
===========================
Node: v22.12.0
Platform: darwin arm64
| Size | Code Len | Cached Med | Uncached Med | Compile Med | Speedup |
|--------|----------|------------|--------------|-------------|---------|
| tiny | 60 | 0.62 | 1.37 | 0.54 | 2.20x |
| small | 344 | 0.42 | 1.00 | 0.62 | 2.40x |
| medium | 8090 | 38.04 | 38.54 | 0.54 | 1.01x |
| large | 69477 | 400.12 | 404.00 | 2.75 | 1.01x |
| huge | 650011 | 2016.46 | 2016.25 | 0.83 | 1.00x |
Heavy Parse Patterns Test
============================================================
Pattern | Chars | Compile
------------------------------------------------------------
1000 regex literals | 28954 | 1.8μs
2000 unique identifiers | 92698 | 3.4μs
2000 string literals | 110952 | 3.8μs
200 nested objects | 2718 | 1.0μs
1000 arrow functions | 21724 | 1.4μs
200 generators | 8981 | 1.2μs
The available microbenchmarks do not support implementing vm.Script caching for the render path. They show cached execution and same-source vm.runInContext differences near the measurement-noise band once scripts perform real work, while also showing that cold or unique-source compilation can be much more expensive than the original same-source compile medians implied.
The stronger original wording was overconfident. The ~3ms attributed to "JS parsing" in #3282 was not reproduced here as raw vm.Script compile cost, but identifying it as execution time remains an inference until the original or an equivalent production-like harness is rerun.
Status: Investigation corrected. Optimization still not justified by current evidence.