Startup: snapshots & code caching
Learn how JavaScript engines use startup snapshots, lazy deserialization, code caching, and background compilation to start pages faster.
- 01Name the work at every startTell apart the jobs that happen before your code runs: starting the engine, setting up built-ins, parsing, compiling to bytecode, and running your code.
- 02Compare the ways to reuse workExplain how startup snapshots, loading built-ins on first use, V8's code cache, Node's compile cache, and the HTTP cache differ.
- 03Write cache-friendly codeKeep script URLs and their content stable, avoid building often-run code from strings, and use service workers with care.
The work before your code runs
A lot of work happens before your first line of JavaScript runs. The engine is the part of the browser, or of Node.js, that runs JavaScript. Chrome and Node both use an engine called V8.
Before it runs your code, the engine must build the things JavaScript gives you ready-made. These are called built-ins. Examples are Object, Array, Math, RegExp, Promise, and often Intl. Every new page, worker, or Node program needs its own set. Building them from nothing every time would make every start slow.
Startup performance is how quickly a page or a program becomes ready to run. Engines make startup faster by not repeating work they already did: setting up built-ins, reading your code, and turning it into instructions.
A restaurant does not lay the table from nothing after each guest sits down. It sets the table once, before guests arrive. It opens extra dishes only when someone asks for them. And it keeps the notes that worked last time. Engines do the same things for JavaScript startup.
- In real life: Plates, forks, and glasses are already on the table
- In JavaScript: Built-ins are already set up and saved in a startup snapshot
- In real life: A covered dish is opened only when someone asks for it
- In JavaScript: A built-in is loaded only when your code first calls it
- In real life: A translated recipe is kept for the next dinner
- In JavaScript: Saved bytecode (a code cache) is reused when the same script loads again
- In real life: Cooking starts while the groceries are still arriving
- In JavaScript: The engine reads and compiles a script while it is still downloading
The table below lists the work that happens every time a page or program starts. Read each row as: this job, and why it is needed.
| The job | Why it is needed |
|---|---|
| Set up the built-ins | Your code expects Object, Array, Promise, Math, RegExp, and Intl to exist before it runs. |
| Create a context | A context is one separate JavaScript world. Each page, worker, or isolate needs its own global object and a heap with the built-ins in it. |
| Parse and compile scripts | The engine reads your source text (parse) and turns it into bytecode, simple instructions it can run (compile). Later it may make faster code for busy parts. |
| Run start-up code | Your own set-up code still runs, even when caching helps: a framework starting, page code, a command-line tool's set-up, or a server's set-up. |
This lesson builds on Loading performance. That lesson showed the browser cache and code caching from the website's side. Here you will see how the engine itself decides, and try the Node.js versions of these tools. The lesson also ties back to lazy parsing, bytecode and interpreters, Meet the engines, service workers, and runtimes.
Startup snapshots
SAVED SET-UPThe first trick is a startup snapshot. You need four words for it:
- The heap is the engine's memory, where all objects live.
- A context is one separate JavaScript world, with its own global object and its own built-ins. Each page, iframe, and worker gets one.
- To serialize means to turn objects in memory into bytes that can be saved in a file. To deserialize means to turn those bytes back into objects.
- An isolate is V8's word for one separate copy of the engine, with its own heap. A tab or a worker can have its own isolate.
A snapshot is a saved copy of the heap, taken right after the built-ins are set up. Here is how it is used:
- The engine sets up the built-ins once.
- It serializes that ready heap into a file called a snapshot blob. A blob is just a file of raw bytes.
- When a new isolate or context starts, the engine deserializes the blob instead of building everything again.
V8 explains this in its post Custom startup snapshots. The post is from 2015, so treat its numbers as history, not a promise. On one desktop computer, creating a context went from about 40 ms to less than 2 ms. On an average mobile phone, it went from about 270 ms to 10 ms.
A snapshot saves V8's heap, and nothing outside it. The V8 post warns: while you make a snapshot, do not touch the world outside the engine. These things can come out wrong, or stuck at one value forever:
- callbacks into native code (code written in C++, not JavaScript),
- the memory behind typed arrays,
- values from
Math.random()orDate.now(). The snapshot would keep the same "random" number and the same time for every start.
A snapshot only gives the engine a ready start. It does not run your app for you. After the heap is loaded, your script still runs. It may still parse, compile, create objects, fetch data, and call APIs.
The walkthrough below is a small model written in JavaScript. It is not V8's real code. Step through it and watch three things happen:
- the new context starts from the snapshot,
- some built-ins wait without being loaded,
- the page loads only the one built-in it uses.
Step through a small model of a V8 startup snapshot, where built-ins load on first use. The code is a model, not V8's real code, but each step follows what V8's public posts describe.
script
const lazyBuiltins = ["Math.max", "Intl.NumberFormat", "RegExp.prototype.exec"]; function createContextFromSnapshot(pageNeedsIntl) { const heap = { source: "startup-snapshot", ready: [...eagerBuiltins] }; const lazyQueue = [...lazyBuiltins]; const touchedBuiltin = pageNeedsIntl ? "Intl.NumberFormat" : "Math.max"; const index = lazyQueue.indexOf(touchedBuiltin); if (index !== -1) lazyQueue.splice(index, 1); heap.ready.push(touchedBuiltin); return { heapSource: heap.source, eagerCount: eagerBuiltins.length, remainingLazy: lazyQueue.length, touchedBuiltin, };} const boot = createContextFromSnapshot(false);console.log(boot.heapSource);console.log(boot.touchedBuiltin);console.log(boot.remainingLazy);const eagerBuiltins = ["Object", "Array", "Promise"];const lazyBuiltins = ["Math.max", "Intl.NumberFormat", "RegExp.prototype.exec"]; function createContextFromSnapshot(pageNeedsIntl) { const heap = { source: "startup-snapshot", ready: [...eagerBuiltins] }; const lazyQueue = [...lazyBuiltins]; const touchedBuiltin = pageNeedsIntl ? "Intl.NumberFormat" : "Math.max"; const index = lazyQueue.indexOf(touchedBuiltin); if (index !== -1) lazyQueue.splice(index, 1); heap.ready.push(touchedBuiltin); return { heapSource: heap.source, eagerCount: eagerBuiltins.length, remainingLazy: lazyQueue.length, touchedBuiltin, };} const boot = createContextFromSnapshot(false);console.log(boot.heapSource);console.log(boot.touchedBuiltin);console.log(boot.remainingLazy);The model prints three lines: startup-snapshot, Math.max, and 2. The heap came from the snapshot. The page used Math.max. Two built-ins are still waiting. Your page behaves the same either way, whether the engine built the heap slowly or loaded it quickly from a snapshot.
Loading built-ins on first use
LOAD ON FIRST CALLA snapshot saves time, but loading every built-in at once can waste memory. Many pages never use most of them. So V8 waits. It loads a built-in's code only when your code calls it for the first time. This is called lazy deserialization. "Lazy" means "only when needed".
V8's post Lazy deserialization gives the numbers. V8 v6.4 turned it on by default in 2018. It saved more than 500 KB of V8 heap memory per Chrome tab, on average. Popular sites used only about 30% of the built-in functions on average, and some used only 16%.
Math.max, so only its code is loaded. The other two built-ins stay placeholders until something calls them.Here is how it works, step by step:
- The code of every built-in is kept at a known place, in a special part of the snapshot.
- At first, each lazy built-in is only a placeholder: a stand-in that points to a helper built-in called
DeserializeLazy. - On the first call, V8 finds that built-in's real code and deserializes it.
- V8 puts the real code on the function. Later calls go straight to it.
So each built-in function is loaded at most once per isolate. The same is true for each bytecode handler, a small piece of engine code that runs one kind of bytecode instruction.
V8's post Embedded builtins is from August 2018. It describes a change that lets many isolates, and even many processes, share one copy of the built-in code. V8 reported that the median website's V8 heap was 19% smaller than a year before. (The median is the one in the middle.) This is related to snapshots, but it is a memory saving inside V8. It is not something a website can call.
Your app can use the same idea. Do not load and run every optional feature when the page starts. Engines are already careful with built-ins. Be just as careful with your own big parts: code for other pages, editors, chart libraries, language data, and admin-only tools.
Node.js startup snapshots
NODE 22.23.1Node.js lets you make your own startup snapshot. This site runs Node 22.23.1 in production. This lesson's test also tries the feature on whichever Node 22 / V8 12.4 version it runs on. After starting from a snapshot blob, the test prints the Node version, then StartupPerformance:build-snapshot.
node --snapshot-blob snap.blob --build-snapshot entry.jsnode --snapshot-blob snap.blob main.jsLine 1 runs entry.js once and saves the heap into snap.blob. Line 2 starts Node from that blob, then runs main.js.
globalThis.lessonSnapshot = { title: "StartupPerformance", compiledAt: "build-snapshot",};entry.js puts a plain object on globalThis, the global object. That object is saved into the blob.
console.log("Node " + process.versions.node);console.log( globalThis.lessonSnapshot.title + ":" + globalThis.lessonSnapshot.compiledAt,);main.js never creates lessonSnapshot. It is already there, because it came from the blob. Line 1 prints the Node version. Lines 2 to 5 print StartupPerformance:build-snapshot.
Node's v8 module has a second way to set this up. It uses two hooks, which are functions Node calls for you at the right moment:
addSerializeCallbackruns just before the blob is written. Use it to tidy up, or to drop data that should not be saved.setDeserializeMainFunctionsets the function that runs when Node starts from the blob. Then you do not need a separatemain.jsfile.
const v8 = require("node:v8"); const state = { words: ["snapshot", "cache"] }; v8.startupSnapshot.addSerializeCallback((data) => { data.words.push("serialized");}, state); v8.startupSnapshot.setDeserializeMainFunction((data) => { console.log(data.words.join(","));}, state);When Node starts from this blob, it prints snapshot,cache,serialized. The word serialized was added by the hook on line 5, just before the blob was written.
Building a snapshot must give the same result every time. Never put these into a blob: secrets, file paths that only exist on one machine, open network connections, random numbers, the current date, or data from one request. Treat the blob like a built file that many starts will reuse.
Code caching
SAVED BYTECODECode caching is a different trick from a startup snapshot. When the engine compiles a script, it turns the source into bytecode: simple instructions that the engine's interpreter runs. A code cache saves that result. The next time the exact same source loads, the engine can reuse it and skip some parsing and compiling.
V8's post Code caching for JavaScript developers says Chrome keeps this cache in two places, for both classic scripts and module scripts:
- In memory: a best-effort cache inside the isolate, looked up by the source code. "Best-effort" means the saved result may or may not still be there.
- On disk: a saved cache that Chrome manages. It is kept with the script's entry in the HTTP cache, which is looked up by the script's URL.
The disk cache needs a few visits before it helps. The table below shows each kind of load. Read each row as: this kind of load, when it happens, and what the engine does.
| Kind of load | When it happens | What the engine does |
|---|---|---|
| Cold run | The first time this URL with this content loads. | There is no saved bytecode. The browser downloads or checks the file, then parses, compiles, and runs it. Chrome remembers the file. |
| Warm run | The second load of the same URL and content. | The engine compiles again, then Chrome saves the bytecode to disk for next time. The in-memory cache may also help when you move between pages in the same process. |
| Hot run | The third load of the same URL and content. | Chrome hands the saved bytecode to V8. V8 loads it and skips the compile work it can skip. |
| Small script | The source is under 1 KiB (1,024 bytes), per V8's 2019 advice. | Chrome does not cache it, because saving it could cost more than it saves. |
| Changed URL or content | A new hash, a timestamp, random inline code, or a fresh 200 response with different content. | It is treated as new code. Old saved bytecode cannot be used safely. |
These rules come from V8's public posts. This lesson treats them as public advice, not a promise that browsers must keep. Two numbers to quote with care: Chrome 66's better code caching cut parse and compile time by 20–40% on many pages, and the developer post named a 1 KiB minimum size. These rules can change. So measure real performance recordings instead of relying on one magic number.
import vm from "node:vm"; const source = "function add(a, b) { return a + b; } add(2, 3);";const first = new vm.Script(source, { produceCachedData: true });const cachedData = first.cachedData ?? first.createCachedData(); const accepted = new vm.Script(source, { cachedData });console.log(accepted.cachedDataRejected); const changed = new vm.Script(source + " console.log('changed');", { cachedData });console.log(changed.cachedDataRejected);Node's node:vm module lets you try this yourself. In plain words, line by line:
- Line 1 loads the
vmmodule, which can compile and run scripts. - Line 3 is a small script that adds two numbers.
- Lines 4 and 5 compile it and keep the cached data: the saved bytecode, as bytes.
- Lines 7 and 8 compile the same source again, with the cached data.
cachedDataRejectedisfalse, so V8 used it. - Lines 10 and 11 add extra code to the source and try the same cached data. Now
cachedDataRejectedistrue. V8 refused it, because it no longer matches.
This lesson's test runs that real code. It checks only the true and false values, not the speed. Whether the cache was used is a fact you can check every time. The time saved changes from run to run.
import { enableCompileCache } from "node:module"; const result = enableCompileCache("./.node-compile-cache");console.log(result.status);console.log(result.directory.endsWith(".node-compile-cache"));Node 22.23.1 can also save bytecode for whole modules. Call module.enableCompileCache(), or set the environment variable NODE_COMPILE_CACHE=dir. Node's docs say:
- It saves V8's code cache for CommonJS and ES modules in a folder.
- The first load of your modules may get a little slower.
- Later loads can get faster, as long as the module files have not changed.
A model of the code cache
STEP + PLAYThe model below follows the rules you just read:
- the URL and the source must stay the same,
- the source must be at least 1 KiB,
- loads go cold, then warm, then hot,
- a changed source or a changed URL starts over,
- a visit after a long gap starts over. In this model, a long gap is more than 72 hours.
It is small enough to read line by line. Step through it first. Then change the third load in the playground.
Step through a small model of V8's code cache in the browser. It follows the cold, warm, hot order, the 1 KiB minimum size, and this lesson's 72-hour gap rule.
script
const largeSource = "console.log('dashboard shell');\n" + "export const route = 'dashboard';\n".repeat(44);const changedSource = largeSource + "export const build = 'v2';\n";const tinySource = "console.log('tiny');"; function decideEngineCacheLoads(loads) { const entries = new Map(); return loads.map((load) => { const sizeBytes = load.content.length; const previous = entries.get(load.url); const changed = previous ? previous.content !== load.content : false; const stale = previous ? load.hour - previous.lastSeenHour > rules.recentVisitHours : false; if (sizeBytes < rules.minimumSourceBytes) { return { outcome: "too-small", usesCachedBytecode: false }; } if (!previous || changed || stale) { entries.set(load.url, { content: load.content, hits: 1, lastSeenHour: load.hour, hasDiskCodeCache: false, }); return { outcome: "cold", usesCachedBytecode: false }; } previous.hits += 1; previous.lastSeenHour = load.hour; if (!previous.hasDiskCodeCache) { previous.hasDiskCodeCache = true; return { outcome: "warm", usesCachedBytecode: false }; } return { outcome: "hot", usesCachedBytecode: true }; });} const loads = [ { url: "/assets/app.8f3a1c.js", content: largeSource, hour: 0 }, { url: "/assets/app.8f3a1c.js", content: largeSource, hour: 1 }, { url: "/assets/app.8f3a1c.js", content: largeSource, hour: 2 }, { url: "/assets/app.8f3a1c.js", content: changedSource, hour: 3 }, { url: "/assets/tiny.js", content: tinySource, hour: 4 }, { url: "/assets/app.91bd22.js", content: largeSource, hour: 5 }, { url: "/assets/app.91bd22.js", content: largeSource, hour: 80 },]; const decisions = decideEngineCacheLoads(loads);console.log(decisions.map((item) => item.outcome).join(" -> "));console.log(decisions.map((item) => item.usesCachedBytecode ? "use cache" : "compile").join(" -> "));const rules = { minimumSourceBytes: 1024, recentVisitHours: 72 };const largeSource = "console.log('dashboard shell');\n" + "export const route = 'dashboard';\n".repeat(44);const changedSource = largeSource + "export const build = 'v2';\n";const tinySource = "console.log('tiny');"; function decideEngineCacheLoads(loads) { const entries = new Map(); return loads.map((load) => { const sizeBytes = load.content.length; const previous = entries.get(load.url); const changed = previous ? previous.content !== load.content : false; const stale = previous ? load.hour - previous.lastSeenHour > rules.recentVisitHours : false; if (sizeBytes < rules.minimumSourceBytes) { return { outcome: "too-small", usesCachedBytecode: false }; } if (!previous || changed || stale) { entries.set(load.url, { content: load.content, hits: 1, lastSeenHour: load.hour, hasDiskCodeCache: false, }); return { outcome: "cold", usesCachedBytecode: false }; } previous.hits += 1; previous.lastSeenHour = load.hour; if (!previous.hasDiskCodeCache) { previous.hasDiskCodeCache = true; return { outcome: "warm", usesCachedBytecode: false }; } return { outcome: "hot", usesCachedBytecode: true }; });} const loads = [ { url: "/assets/app.8f3a1c.js", content: largeSource, hour: 0 }, { url: "/assets/app.8f3a1c.js", content: largeSource, hour: 1 }, { url: "/assets/app.8f3a1c.js", content: largeSource, hour: 2 }, { url: "/assets/app.8f3a1c.js", content: changedSource, hour: 3 }, { url: "/assets/tiny.js", content: tinySource, hour: 4 }, { url: "/assets/app.91bd22.js", content: largeSource, hour: 5 }, { url: "/assets/app.91bd22.js", content: largeSource, hour: 80 },]; const decisions = decideEngineCacheLoads(loads);console.log(decisions.map((item) => item.outcome).join(" -> "));console.log(decisions.map((item) => item.usesCachedBytecode ? "use cache" : "compile").join(" -> "));- Load 1
/assets/app.8f3a1c.js1528 bytes · hour 0 coldthe first time this URL with this content loads
- Load 2
/assets/app.8f3a1c.js1528 bytes · hour 1 warmthe second matching load compiles again, then saves the bytecode for next time
- Load 3
/assets/app.8f3a1c.js1528 bytes · hour 2 hotthe same URL and source already have saved bytecode
Yes. The engine loads the saved bytecode instead of compiling.
hot: cached bytecode reused. the same URL and source already have saved bytecode
This model starts after the file's bytes have arrived. Two separate questions decide what happens:
- Where did the bytes come from? A fresh download (HTTP 200), a "not changed" reply (HTTP 304), the memory cache, or a service worker.
- Can the engine reuse saved bytecode? That is the code cache's job.
A yes to one question does not mean a yes to the other.
A hot script also does not mean every function inside it was compiled. V8's advice says the cache holds only what was compiled by the time the script finished running. A function that has not run yet may still be waiting to be compiled. A later page can call it for the first time, and only then is it compiled.
const compiledDuringWarmRun = new Set(); function visit(route) { const functionName = route === "dashboard" ? "renderDashboard" : "renderSettings"; compiledDuringWarmRun.add(functionName);} visit("dashboard");console.log(compiledDuringWarmRun.has("renderDashboard"));console.log(compiledDuringWarmRun.has("renderSettings"));This tiny model prints true, then false. Line 10 visits the dashboard, so renderDashboard was compiled during the warm visit. The settings page was never visited, so renderSettings was not. This is only a model. It is not a browser cache API.
Compiling while downloading
WORK AT THE SAME TIMENot every startup win comes from reusing old work. Some come from starting work earlier, while other work is still going. A thread is one line of work the computer runs. The main thread also handles clicks and draws the page, so work moved off it keeps the page responsive. V8's post Background compilation says:
- Since version 41, Chrome could parse JavaScript on a background thread, using
StreamedSource. Parsing could start as soon as the first piece of the file arrived from the network. - From Chrome 66, V8 also moved much of the bytecode compiling off the main thread. In V8's measurements, this cut main-thread compile time by about 5–20% on typical websites.
There are limits. At the time of that post, only two kinds of code were compiled in the background: top-level script code, and functions that run right away (immediately invoked function expressions). Inner functions were still compiled later, the first time they ran.
Later, Chrome 78 started streaming scripts during preload. So <link rel="preload" as="script"> could start compiling a script before the HTML parser reached its <script> tag.
<link rel="preload" as="script" href="/assets/app.js"><script src="/assets/app.js" defer></script> <script type="module"> const module = await WebAssembly.compileStreaming(fetch("/app.wasm")); console.log(module instanceof WebAssembly.Module);</script>WebAssembly (Wasm) is a compact binary code format that browsers can also run. It gives you this overlap directly, with WebAssembly.compileStreaming(fetch(...)). V8's Chrome 65 post says Wasm compiling can start while the module's bytes are still arriving, and it compiles functions on background threads. JavaScript has no such API. But the browser can still stream, parse, and compile suitable scripts on its own.
Writing cache-friendly code
STABLE BYTESThe most useful advice in V8's developer post is simple: do not change your code or your URLs unless you need to. A stable external script lets three caches work together: the HTTP cache, the service worker cache, and the code cache. These habits make every load look like new code:
- inline source that changes on every load,
- timestamps in script URLs,
- code that is generated fresh for each request.
const scripts = [ { url: "/assets/app.8f3a1c.js", sourceBytes: 1488, note: "stable hashed file" }, { url: "/assets/app.js?build=2026-09-26T15:23", sourceBytes: 1488, note: "timestamped URL" }, { url: "inline", sourceBytes: 640, note: "tiny inline bootstrap" },]; for (const script of scripts) { const stableUrl = script.url.includes("8f3a1c"); const largeEnough = script.sourceBytes >= 1024; console.log(script.note + ": " + (stableUrl && largeEnough ? "cache-friendly" : "review"));}Lines 2 to 4 describe three scripts. Line 8 looks for the stable hash, 8f3a1c, in the URL. Line 9 checks that the size is at least 1024 bytes. Only the first script passes both, so it prints cache-friendly. The other two print review.
The table below lists common choices. Read each row as: this choice, does it help the cache, and why.
| The choice | Does it help the cache? | Why |
|---|---|---|
| Hashed file names | Yes | /assets/app.8f3a1c.js keeps one URL for one version of the content. When the content changes, the hash and the URL change too. |
| Timestamps in script URLs | No | /app.js?ts=now makes a new URL every time, even when the source is the same. |
| Large, meaningful files | Usually | Files above the size limit can be cached. Don't split code into dozens of tiny files just to keep each file small. |
| One huge bundle | It depends | It may cache well on repeat visits. But the first load, memory use, cache resets after any change, and running code nobody needs can all get worse. |
eval and new Function | No, for often-run code | Code built from strings while the page runs is not a stable script file. It is hard to cache and hard to check. |
| Service worker precache | Yes, if you look after it | Chrome can build a full code cache for scripts added during the install event. Updating and cleaning up the cache is your job. |
- Serve
/assets/app.8f3a1c.jsand let browsers cache it for a long time. Append `?t=Date.now()` to every script request.- Split a page's code into many 600-byte helper scripts.
- Precache important JS during the service worker install event.
- Merge the code for every page into one 900 KB bundle.
Generate often-run handlers with `new Function(template)` on every page load.- Use preload or modulepreload for an important entry script.
Sort each choice: does it help the code cache, does it stop the cache from working, or does it depend on the rest of the setup?
A service worker is a script that sits between your page and the network, and can store files. V8's developer post says: JavaScript files added to Cache Storage during the service worker's install event can get a full code cache right away. If files are added to the cache later, a normal code cache can still be made one load sooner than the usual cold, warm, hot path. This only helps if you handle the service worker's updates and clean-up correctly.
A checklist for real projects
Knowing how the engine works helps you ask better questions. It does not mean you should copy every internal rule without checking. Look at real performance recordings, then use this list:
- Measure the first visit and repeat visits separately. A code cache can't help the first visit.
- Put content hashes in script URLs, so a URL stays the same until its content changes. Avoid timestamps and random query strings.
- Keep the source exactly the same between releases, unless what it does really changed.
- Put larger code in external script files. Keep inline scripts small, and the same on every load.
- Don't build often-run code with
evalornew Function, unless a measurement shows you need to. - Precache files with a service worker only if you also plan how to update and remove old versions.
- For Node command-line tools or servers, test whether a startup snapshot or the module compile cache speeds up your real start.
These four tools are easy to mix up. Read each row of the table as: what the tool saves, where it lives, what it helps with, and what it can't do.
| Tool | What it saves | Where it lives | What it helps | What it can't do |
|---|---|---|---|---|
| HTTP cache | The file's bytes and its headers. | The browser's cache, a CDN, or a service worker. | It skips the download when the URL and the cache headers allow it. | On its own, it does not skip parsing, compiling, or running. |
| V8 code cache | The compiled result of a script, mostly bytecode. | Chrome's in-memory cache, or on disk next to the cached script. | It skips some parsing and compiling when the same source loads again. | It can't help the first load. A changed source, or a script the rules skip, gets no help. |
| Startup snapshot | A ready heap with the built-ins, plus any extra set-up the host program saved. | Inside V8 itself, Chrome, Node, or another program that uses V8. | It skips building the built-ins and start-up libraries for each new isolate or context. | It can't safely save things outside the engine, like open files or network connections. |
| Node compile cache | V8's code cache for CommonJS and ES modules, on disk. | A folder you choose with module.enableCompileCache() or NODE_COMPILE_CACHE. | It speeds up later Node starts when the module files have not changed. | It may slow the first start, and it can make V8's code-coverage reports less exact. |
Common misconceptions
- “The HTTP cache and the code cache are the same.” The HTTP cache saves the file's bytes. V8's code cache saves the compiled result for the same source.
- “A startup snapshot runs my app ahead of time.” It only gives the engine a ready heap. Your app's code still runs after the snapshot is loaded.
- “Warm means saved bytecode was used.” In Chrome's public V8 rules, the warm run is the second load. It compiles again and then saves the bytecode to disk. The hot run is the one that uses it.
- “Tiny files always cache better.” Scripts under the size limit get no code cache at all. And each extra file adds a request and more work to schedule.
- “Service workers make scripts fast forever.” They can help. But old caches, the browser clearing storage when space runs low, encoding or module mismatches, and update bugs still cause problems.
- “Streaming compilation is JIT optimization.” Streaming parses and compiles to bytecode while the file downloads. JIT optimization, which turns often-run code into faster machine code, happens later and needs facts gathered from running code.
Practice exercises
5 EXERCISESRead the starter code. Which built-in does its output name? Type just the built-in's name.
const timeline = ["deserialize initialized heap", "lazy deserialize Math.max", "run page script"];
console.log(timeline[1]);The output is lazy deserialize Math.max. So the built-in is Math.max.
Type the exact text the program prints.
const outcomes = ["cold", "warm", "hot"];
console.log(outcomes.join(" -> "));It prints cold -> warm -> hot. The first load compiles. The second matching load compiles again and saves the bytecode. The third matching load can use it.
vm.Script rejected flagsThis program copies the two values from the Node demo. Type the two values it prints, with a comma between them.
const accepted = false;
const changedSourceRejected = true;
console.log(String(accepted) + "," + String(changedSourceRejected));The answer is false,true. The same source used the saved data. The changed source refused it.
Type the label the program prints for this hashed script.
const url = "/assets/app.8f3a1c.js";
const bytes = 1488;
console.log(url.includes("8f3a1c") && bytes >= 1024 ? "cache-friendly" : "review");It prints cache-friendly. One stable, hashed URL points to a file that is big enough to cache.
Which two jobs does the starter program join? Type its output.
const phases = ["download", "parse", "compile", "execute"];
console.log(phases.slice(1, 3).join(" + "));It prints parse + compile. With streaming, the browser can parse and compile a script while its bytes are still arriving, when the script is suitable.
Check your understanding
8 QUESTIONSQuestion 1 of 8What problem does a startup snapshot mainly solve?
Choose an answer to see the explanation.
Question 2 of 8What does this timeline program print?
Read the code, then predictconst timeline = ["deserialize initialized heap", "lazy deserialize Math.max", "run page script"]; console.log(timeline[1]);Choose an answer to see the explanation.
Question 3 of 8In Chrome's public V8 advice, when does the on-disk code cache help a stable external script?
Choose an answer to see the explanation.
Question 4 of 8What does this cache order program print?
Read the code, then predictconst outcomes = ["cold", "warm", "hot"]; console.log(outcomes.join(" -> "));Choose an answer to see the explanation.
Question 5 of 8Which Node 22.23.1 tool turns on the module compile cache on disk?
Choose an answer to see the explanation.
Question 6 of 8What does this cached-data program print?
Read the code, then predictconst accepted = false; const changedSourceRejected = true; console.log(String(accepted) + "," + String(changedSourceRejected));Choose an answer to see the explanation.
Question 7 of 8Which sentence about streaming and background compilation is true?
Choose an answer to see the explanation.
Question 8 of 8Which habit helps the cache most in a real project?
Choose an answer to see the explanation.
Takeaways and next
- A startup snapshot saves a ready heap, so each new context or Node program does not build the built-ins again.
- Lazy deserialization keeps built-ins as placeholders until code calls them for the first time.
- A code cache reuses the compiled bytecode of the exact same source. It is not the same as the HTTP cache.
- Chrome's public V8 advice describes cold, warm, and hot loads, a 1 KiB minimum size, and a head start for scripts a service worker precaches.
- Node 22.23.1 has startup snapshots,
v8.startupSnapshot, cached data forvm.Script, and a module compile cache. - Cache-friendly code keeps URLs and bytes stable, avoids building often-run code from strings, and measures first visits apart from repeat visits.
Remember the one-liner.
Engines start faster by setting up the built-ins once, loading only the ones your code calls, and reusing compiled code only when the script has not changed.
Next, tiered compilation shows what happens after startup. Engines watch code while it runs, gather facts about it, and recompile often-run code with faster and faster compilers.