Skip to content

Feature request: expose V8 code cache (ScriptCompiler::CachedData) on Context#eval #411

@ursm

Description

@ursm

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions