Reusing V8 Compiled Code with ScriptCompiler Code Caching
Skip re-parsing JavaScript on every startup by caching V8 bytecode blobs, but manage invalidation and version compatibility yourself.
28 Aug 2025, 09:27 UTC

The problem: your users pay for the same parse twice
If you embed V8 in a desktop application, server, or game, you likely load the same JavaScript bundles on every launch. V8 parses and compiles those scripts each time, even when the source has not changed. For large bundles, that repeated work appears as slow startup and higher CPU before the first line of user code runs.
What ScriptCompiler code caching actually does
V8 exposes ScriptCompiler::Source with two methods embedders can call: GetCachedData and SetCachedData. After a successful compilation, GetCachedData returns a blob containing compiled bytecode. You can persist that blob to disk or memory. On the next launch, pass the blob back through SetCachedData when compiling the same source, and V8 can skip parsing and generate code directly from the cached bytecode.
Important boundaries: the blob is bound to the V8 version and the source identity. If V8 cannot use the blob, it falls back to normal compilation without throwing a cache-specific error. V8 does not persist, evict, or invalidate blobs automatically; the embedder owns storage, keying, and cache invalidation.
Eager compilation versus lazy parsing
Pair caching with CompileOptions::kEagerCompile when you control loading and want predictable warm-up. Eager compilation produces bytecode immediately instead of lazily parsing functions on first use. That makes startup cost more predictable, but it can increase memory and CPU if you cache scripts that never execute.
| Approach | Best for | Trade-off |
|---|---|---|
| Lazy parsing | Scripts that may never run | Lower upfront cost, unpredictable first-run latency |
| Eager compile + cache | Large bundles that run at startup | Higher upfront CPU/memory, predictable warm-up |
Worked example: compile, persist, and reuse
This example runs in your embedder's C++ initialization code, not in a browser console. An embedder is the application embedding V8, and a V8 isolate is an isolated engine instance with its own heap and state. The example assumes you have an active v8::Isolate and a script source string.
// Pseudocode for illustration; adapt to your V8 version and embedder.
v8::ScriptCompiler::Source source(v8::String::NewFromUtf8(isolate, kScriptSource));
// 1. Try to load a previously persisted blob for this script.
std::optional> cached_blob = LoadBlobFromDisk(kCacheKey);
// 2. Attach cached data if present.
if (cached_blob) {
source.SetCachedData(v8::ScriptCompiler::CachedData::New(
cached_blob->data(), cached_blob->size()));
}
// 3. Compile eagerly so bytecode is ready immediately.
v8::Local script = v8::ScriptCompiler::Compile(
context, &source, v8::ScriptCompiler::kEagerCompile).ToLocalChecked();
// 4. If compilation produced fresh cache data, persist it.
if (source.GetCachedData()) {
SaveBlobToDisk(kCacheKey, source.GetCachedData());
}
Where to run: your embedder's C++ startup path, after V8 is initialized and before the script is needed. Permissions: the process needs write access to the cache directory. Placeholders: replace kScriptSource, kCacheKey, and storage helpers with your own implementation. Expected checks: confirm GetCachedData returns non-null after a fresh compile, and confirm compilation succeeds when the blob is missing or corrupt. Risks: a stale or version-mismatched blob causes V8 to fall back to uncached compilation, so you must verify version compatibility yourself.
Limitations and how to verify
- Version lock: record
V8::GetVersion()when generating a blob and reject blobs from a different V8 major version before deployment. - Stale caches: change your cache key when source changes, because V8 does not invalidate for you.
- Eager cost: do not eagerly compile every script; measure which bundles actually run during startup.
Practical check: compile once with the cache, persist the blob, then restart the embedder and compile again with SetCachedData. Measure end-to-end compile time before and after enabling the blob. Separately, corrupt a blob or modify the source and confirm compilation falls back cleanly to the uncached path without crashing.
Closing
V8 code caching removes redundant parse and compile work for repeatedly loaded scripts, but it is a building block, not a managed cache. You decide where blobs live, how they are keyed, when they expire, and whether the running V8 version matches the one that created them. Start with your largest startup script, measure the difference, and expand only where the win is real.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.