Motivation
For applications that re-execute the same multi-MB JS bundle on every request
(e.g. SSR / per-visit isolation patterns), Script::Compile parsing dominates
runtime — large bundles add hundreds of ms of cumulative parse cost per visit
that has nothing to do with the script's actual work.
V8 has a built-in remedy in v8::ScriptCompiler::CachedData: compile once,
serialize the bytecode, and feed it back on subsequent compiles to skip parsing.
mini_racer currently always goes through the plain v8::Script::Compile path
(mini_racer_v8.cc:585), so this win isn't reachable from Ruby.
This mirrors the architecture of MiniRacer::Snapshot#dump / Snapshot.load
(#395) — opaque bytes serialized by V8, persisted by the caller, fed back on
the next boot — just at the per-script granularity instead of per-isolate.
Proposed API
Three candidate shapes; happy to take direction on which the maintainers prefer.
(A) kwarg on eval, blob returned alongside result
result, fresh_cache = ctx.eval(src, filename: 'bundle.js', cached_data: prev_cache)
Symmetric with Snapshot#dump / .load. Backward-incompatible return shape
unless gated on the kwarg being present.
(B) kwarg on eval, blob fetched separately
result = ctx.eval(src, filename: 'bundle.js', cached_data: prev_cache)
fresh_cache = ctx.last_compiled_cache # nil if cache was consumed
Fully backward-compatible signature.
(C) explicit compile step (closer to V8 native shape)
script = ctx.compile(src, filename: 'bundle.js', cached_data: prev_cache)
script.cached_data # bytes for persistence
script.run # or implicit run via eval(script)
More surface area, but composable; lets callers warm the cache without running.
Implementation sketch
- Switch
v8_eval to v8::ScriptCompiler::Compile with kConsumeCodeCache when
a blob is supplied; check source.GetCachedData()->rejected and silently
fall back to a fresh compile on rejection.
- Without a blob, compile normally and call
ScriptCompiler::CreateCodeCache(unbound_script) to produce the bytes.
- Extend the request / response payloads on the bridge (third optional
element either way) — same pattern as the snapshot serialization.
- TruffleRuby shim: ignore
cached_data, return nil for the cache blob.
Total change is ~150 LoC + specs + CHANGELOG.
Happy to send a PR once there's directional agreement on the API shape.
Motivation
For applications that re-execute the same multi-MB JS bundle on every request
(e.g. SSR / per-visit isolation patterns),
Script::Compileparsing dominatesruntime — large bundles add hundreds of ms of cumulative parse cost per visit
that has nothing to do with the script's actual work.
V8 has a built-in remedy in
v8::ScriptCompiler::CachedData: compile once,serialize the bytecode, and feed it back on subsequent compiles to skip parsing.
mini_racer currently always goes through the plain
v8::Script::Compilepath(
mini_racer_v8.cc:585), so this win isn't reachable from Ruby.This mirrors the architecture of
MiniRacer::Snapshot#dump/Snapshot.load(#395) — opaque bytes serialized by V8, persisted by the caller, fed back on
the next boot — just at the per-script granularity instead of per-isolate.
Proposed API
Three candidate shapes; happy to take direction on which the maintainers prefer.
(A) kwarg on
eval, blob returned alongside resultSymmetric with
Snapshot#dump/.load. Backward-incompatible return shapeunless gated on the kwarg being present.
(B) kwarg on
eval, blob fetched separatelyFully backward-compatible signature.
(C) explicit compile step (closer to V8 native shape)
More surface area, but composable; lets callers warm the cache without running.
Implementation sketch
v8_evaltov8::ScriptCompiler::CompilewithkConsumeCodeCachewhena blob is supplied; check
source.GetCachedData()->rejectedand silentlyfall back to a fresh compile on rejection.
ScriptCompiler::CreateCodeCache(unbound_script)to produce the bytes.element either way) — same pattern as the snapshot serialization.
cached_data, returnnilfor the cache blob.Total change is ~150 LoC + specs + CHANGELOG.
Happy to send a PR once there's directional agreement on the API shape.