Embedding V8
Learn how programs host V8 with isolates, contexts, handles, native templates, and startup snapshots for fast JavaScript runtimes.
- 01Tell an isolate from a contextSay that an isolate is one copy of V8 with its own memory, and a context is one JavaScript world inside it, with its own globals.
- 02Read the host's C++ stepsSay why the host's C++ code uses handles, HandleScopes, and templates, and why it picks a context before it runs code.
- 03Plan a small runtimeChoose which globals a host adds, and say how a startup snapshot makes startup faster.
A host puts V8 to work
V8 is a JavaScript engine: the program that reads your JavaScript and runs it. It does these jobs:
- It reads your code and runs it.
- It keeps your objects in memory. This memory is called the heap.
- It gives you the parts that are built into the language, such as
ArrayandMath. These are called built-ins.
V8 is not a browser. It has no timers, no web page, and no network tools. A bigger program must add those. That bigger program is called the host, or the embedder. Chrome is a host. Node.js is a host. Any C++ program can be a host too. Each one puts V8 inside itself and adds its own tools around it.
Embedding V8 means a host program does three things:
- It starts V8.
- It makes places where JavaScript code can run.
- It chooses which extra tools, such as timers, that code can use.
So start with one question: which global names did the host add? A global is a name your code can use anywhere, like setTimeout. In a browser, setTimeout and document both exist. In Node, setTimeout exists but document does not. V8 runs the same code in both places. The host decides what that code can reach.
1console.log(typeof globalThis.setTimeout, typeof globalThis.document);Line 1 looks up two names on globalThis, the object that holds every global. typeof gives the kind of each value as a word. In a browser, the line prints function object: setTimeout is a function and document is an object. In Node, the same line prints function undefined, because Node has no document. The code is the same. Only the host is different.
setTimeout and document. Node adds setTimeout but no document.Here are the four words this lesson uses. Each one gets its own section:
- An isolate is one whole copy of the V8 engine, with its own heap.
- A context is one separate JavaScript world inside an isolate, with its own globals.
- A handle is how C++ code holds on to a JavaScript value.
- Templates and snapshots help the host set up new worlds quickly.
Isolates: one engine with its own memory
An isolate is one running copy of the V8 engine. Each isolate has its own heap, the memory where its objects live. An object made in one isolate belongs to that isolate only. Code in another isolate cannot use it as a normal JavaScript value.
A host can make more than one isolate. It does this when it wants pieces of work kept apart, such as separate workers or separate programs.
An isolate keeps memory apart. It does not make every program safe on its own. The host still decides these things:
- when each isolate gets to run,
- which C++ tools each isolate can call,
- how messages travel between isolates.
Node workers are a real example. A worker runs JavaScript in another V8 isolate, next to your main code. It has its own globalThis.
1import { Worker } from "node:worker_threads";2 3const worker = new Worker(4 "const { parentPort } = require('node:worker_threads'); parentPort.postMessage(globalThis === global);",5 { eval: true, execArgv: [] },6);7worker.once("message", (isOwnGlobal) => console.log(isOwnGlobal));Line 3 starts a Node worker. Line 4 is the worker's own code. It checks whether its globalThis is the same object as its global, and sends the answer back. Line 7 prints the answer. The test sees true. This means the worker has its own global object. It does not share the main program's global object.
Think of a game console. It has its own memory card. That is like an isolate with its own heap. One game running on the console is like a context. The company that built the console chose which buttons it has. The host chooses which tools JavaScript gets in the same way.
- In real life: One console with its memory card
- In JavaScript: One isolate with its heap
- In real life: One game running on that console
- In JavaScript: One context with a global object
- In real life: Buttons wired by the device maker
- In JavaScript: Tools the host adds, like timers
- In real life: Another console
- In JavaScript: Another isolate
Two other lessons use these words too. Agents and realms shows how the JavaScript rules describe these worlds. Realms and globals shows the global objects you can see from your code. This lesson looks at what the host program does with V8.
Contexts: separate JavaScript worlds
A context is one separate JavaScript world inside an isolate. Each context has:
- its own global object,
- its own copies of the built-ins, such as
Array.
Before the host runs any code, it picks which context the code runs in. That way, two unrelated pieces of code don't share one global object by accident.
Node lets you try this from JavaScript. Its node:vm module can make new contexts. It is not the full C++ tool kit that hosts use, but it shows the same idea. The code below makes one context with a global called price. Then it runs a tiny piece of code inside that context.
1const vm = require("node:vm");2const context = vm.createContext({ price: 10 });3console.log(vm.runInContext("price * 2", context));Line 1 loads Node's vm module. Line 2 makes a new context. Its global object starts with price: 10. Line 3 runs price * 2 inside that context and prints 20. The price here is not a variable from the file around it. It is the global price of the new context.
A context holds more than names. Its built-ins are separate too. So an array made in a new Node context gives false for instanceof Array when you check it from outside. instanceof asks: "was this made by this exact constructor?" The array still works like any array. It was just made by the other context's Array.
The step-through below uses a small model of isolates and contexts written for this lesson. It makes one isolate with two contexts and runs the same code in each one.
Step through two contexts in one model isolate. Each context keeps its globals in its own plain object.
script
2const first = createContext(isolate, { price: 10 });3const second = createContext(isolate, { price: 3 });4console.log(run(first, "price * 2"));5console.log(run(second, "price * 2"));price, so the same price * 2 gives 20 in the first and 6 in the second.Proof from Node contexts
The step-through above is a model. It keeps one plain object for each context's globals. Now let's check the same idea in real Node, with node:vm. A name you put in one context does not appear in another context.
1import vm from "node:vm";2 3const first = vm.createContext({ price: 10 });4const second = vm.createContext({});5console.log(vm.runInContext("price * 2", first));6console.log(vm.runInContext("typeof price", second));7console.log(vm.runInNewContext("[]") instanceof Array);Line by line:
- Line 5 runs
price * 2in the first context. It prints20. - Line 6 asks for the type of
pricein the second context. That context is empty, so it printsundefined. - Line 7 makes an array in a brand new context. It checks
instanceof Arrayfrom outside and printsfalse, because the two contexts have differentArrayconstructors.
The lesson test runs this code in real Node. It does not run in your browser, because browsers can't load node:vm. It shows that contexts keep their globals and constructors apart. It does not show that a context is a separate operating-system process. A context alone is also not a safe place to run code you don't trust.
Browsers often use one context for each window and each frame on a page. A host can enter one context, run code, and then leave and go back to the context it used before. Because the host always picks the context on purpose, code never runs in the wrong global world by mistake.
Handles and HandleScopes
JavaScript code uses objects directly. C++ code that hosts V8 can't do that. It must use a handle: a small holder that points to a V8 value.
Why not just keep the memory address of the object? Because V8 has a garbage collector, a helper that cleans up unused objects. While it tidies the heap, it may move an object to a new place in memory. V8 updates every handle when that happens. A saved raw address would still point to the old place, which is wrong.
There are two kinds of handle to know:
- A local handle is for short work. It lives inside a HandleScope, a group that holds local handles. When the scope ends, all of its local handles go away together.
- A persistent handle, written
v8::Persistentorv8::Global, is for longer work. It does not go away when the scope ends. The host must reset it by hand when it no longer needs the value.
One more rule: a function can't simply return a local handle from a scope that is ending. It must use an EscapableHandleScope, which lets one handle escape to the outer scope.
1// Short C++ sketch of a host's code. It is not JavaScript.2v8::HandleScope scope(isolate);3v8::Local<v8::String> name = v8::String::NewFromUtf8Literal(isolate, "Asha");4// When the scope ends, its local handles go away together.This is C++, not JavaScript, and it is shortened. Line by line:
- Line 2 opens a scope for local handles.
- Line 3 makes a V8 string,
"Asha", and a local handle callednamethat points to it. - Line 4 is a comment with the main rule: when the scope ends, its local handles go away together.
After that, the string can be cleaned up, but only if nothing else still points to it.
name points to the string "Asha". When the scope ends, the handle goes away. If nothing else points to the string, it can be cleaned up.Picture a tray at a buffet. While you hold the tray, the plates on it stay with you. When you hand back the tray, all its plates go back at the same time. You don't return them one by one.
- In real life: A tray carries your plates
- In JavaScript: A HandleScope holds local handles
- In real life: A plate stays with your tray
- In JavaScript: A handle keeps a V8 value from being cleaned up
- In real life: Hand back the tray once
- In JavaScript: End the scope, and all its local handles go away
- In real life: Keep one plate for later
- In JavaScript: Use a persistent handle, and reset it yourself
So handles do for C++ what variables do for JavaScript. A JavaScript variable keeps its object from being cleaned up. A handle keeps a V8 value from being cleaned up while C++ code is using it.
Templates add C++ functions to JavaScript
How does a host give JavaScript a new function, like setTimeout? It writes the function in C++. Then it uses a FunctionTemplate. A FunctionTemplate is a recipe V8 uses to make a JavaScript function that runs C++ code when called. The C++ code it runs is called a callback.
The host puts that function on a context's global object. When JavaScript calls it, V8 hands the call to the C++ callback, along with the arguments.
First, here is the same idea in plain JavaScript. A new, empty object stands in for a fresh global object. We add one function to it, then call it. This is not C++. It just shows the host's choice before the C++ sketch.
1const globalObject = {};2 3globalObject.double = (price) => price * 2;4console.log(globalObject.double(10));Line 1 makes an empty object called globalObject. Line 3 adds a function named double to it. Line 4 calls double(10) and prints 20. A real host does this with V8's C++ tools and a C++ callback. The rule is the same: if the host doesn't add a function, JavaScript can't call it.
double. Line 4 calls double(10) and gets 20.1// Short C++ sketch of a host's code. It is not JavaScript.2auto global = v8::ObjectTemplate::New(isolate);3global->Set(v8::String::NewFromUtf8Literal(isolate, "double"),4 v8::FunctionTemplate::New(isolate, DoubleCallback));5auto context = v8::Context::New(isolate, nullptr, global);Line by line:
- Line 2 makes a template for the global object: a recipe for what the global object will hold.
- Lines 3 and 4 add the name
doubleto that recipe. The name points to a FunctionTemplate that runs the C++ callbackDoubleCallback. - Line 5 makes a new context whose global object follows that recipe. Code in this context can call
double().
The browser's document and Node's built-in tools are added in this same way, just with many more functions.
Build a tiny host
Now you play the host. The playground below uses this lesson's small model. It does these things each time you click:
- It makes a new isolate.
- It makes a context with one global,
price: 10. - It adds only the functions you turned on.
- It runs the code you picked.
The model understands only a very small language: numbers, names, + - * /, round brackets, and calls to functions the host added. It never runs your text as real JavaScript. It does not use eval or new Function, the two ways to turn text into running code.
Try this: pick double(price). Then turn Add double off and look at the result again. The result changes to a message saying the function is missing. The Reset button puts back the first setup.
1const isolate = createIsolate();2const context = createContext(isolate, { price: 10 }, { double });3const result = run(context, "double(price)");4console.log(result);1Its globals: price = 10
doubleCode can call only these functions.
20The tiny language ran the code you picked.
The context has one global, price = 10. The code double(price) gives 20.
This is a model for learning. It is not how V8 reads code, and it has no handles or safety checks. Its tiny language keeps the page safe while it shows the host's choice. A real host gives V8 full JavaScript code and handles errors with V8's C++ tools.
Snapshots make startup faster
Every new context needs its built-ins and a global object, ready to use. Making all of that from nothing takes time. A startup snapshot is a saved copy of a heap that is already set up. V8 can load that saved copy instead of building everything again, so startup is faster.
V8's guide to custom startup snapshots works in two steps:
- Set up a heap once, and save it as a snapshot.
- When a new isolate starts, tell it to load that snapshot.
A snapshot is not a backup of a running app. It only saves what is inside V8's heap. It can't save things outside the heap, such as open files. And any value saved in it stays fixed. If you save the current time in a snapshot, every start gets that same old time.
1// Short C++ sketch of a host's code. It is not JavaScript.2v8::Isolate::CreateParams params;3params.snapshot_blob = custom_snapshot_blob;4auto* isolate = v8::Isolate::New(params);Line by line:
- Line 2 makes a settings object,
params, for a new isolate. - Line 3 puts the saved snapshot into those settings, as
snapshot_blob. - Line 4 starts a new isolate with those settings.
Real code needs more setup steps than this. The sketch only shows where the snapshot goes in.
Node and Chrome both use startup snapshots. To see why this matters to people using an app, read startup performance. Work done before the program starts is work the user doesn't wait for. The snapshot must also give the same result every time, and it must match the exact V8 build that loads it.
What runtime authors decide
A runtime is a host program that runs JavaScript, such as Node or a browser. Most developers never call V8's C++ tools. But the people who build runtimes make choices that every developer feels. They choose:
- which globals exist,
- how their C++ functions report errors,
- how many isolates to make,
- when to make new contexts,
- what can safely go in a startup snapshot.
That is why the same V8 can feel different in a browser, in Node, or in a test tool. The core JavaScript language comes with V8. Extra tools, such as timers and document, come with the host. So before you use a tool, check which runtime will run your code.
- Has its own heap: the memory for one copy of V8.
- Has its own global object and its own built-ins, like
Array. - Decides whether
setTimeoutexists. - Keeps a V8 value from being cleaned up while C++ code uses it.
- Can hold
price: 10without changing any other JavaScript world. - Picks a saved, ready-made heap to load at startup.
Put each card under the part that does that job. Read the explanation after each choice.
This helps when you write a library too. Don't assume a browser document exists in code that might run in Node. Keep code that needs host tools in one small place. Write tests that say which runtime tools you need.
A host that uses V8 directly has more jobs. Here are three:
- It decides when queued promise callbacks run. These callbacks are called microtasks. The host can let V8 run them automatically, or run them itself at points it picks.
- It must follow V8's C++ tools as they change between V8 versions. A Node add-on can use Node-API instead. Node-API stays the same across Node versions.
- It must not trust separate isolates alone for safety. Packages such as
isolated-vmrun code in separate V8 isolates inside Node. That alone does not make code you don't trust safe to run.
Common mix-ups
- “V8 is the browser.” V8 is only the engine. The browser adds the rest around it: the web page,
fetch, drawing on screen, and the loop that handles events. - “Every context is a separate isolate.” One isolate can hold many contexts. Each context has its own globals.
- “A context makes untrusted code safe by itself.” A new global object is not enough. To run code you don't trust, the host needs a full set of safety rules.
- “A handle is a JavaScript variable.” A handle is used by C++ code, through V8's C++ tools. JavaScript code never sees it.
- “A snapshot saves everything an app is doing.” A snapshot saves only a heap that was set up ahead of time. It can't save things outside the heap, such as open files.
The table below lists terms that are easy to mix up. Read each row as: "this term means this, and it does not mean that."
| Word | What it means | What it does not mean |
|---|---|---|
| V8 | The engine that reads and runs JavaScript code. | It is not the whole browser, and it is not the whole Node.js program. |
| Host API | A tool the host adds around V8, such as setTimeout or document. | It is not part of the JavaScript language itself. |
| Separate contexts | Two JavaScript worlds in one isolate. Each has its own globals and built-ins. | They are not enough, on their own, to make code you don't trust safe to run. |
| Snapshot | A heap saved after setup, so the next start is faster. | It is not a tool for looking at memory, and it can't save just any data. |
A good habit: before you explain a behavior, name the part that causes it. Is it the JavaScript language, the V8 engine, V8's C++ tools, or the host? That one question clears up most confusion about globals, separate worlds, and startup.
Practice exercises
What does this line print in a browser? Then say who decides that document exists: V8 or the host.
console.log(typeof globalThis.setTimeout, typeof globalThis.document);A browser prints function object. Node has setTimeout too, but no document, so in Node the second word is undefined. The host decides that document exists, not V8.
Each object stands in for the global object of one context. Type the two printed lines in order, with a comma between them.
const first = { price: 10 };
const second = {};
console.log(first.price * 2);
console.log(typeof second.price);The program prints 20 and then undefined. Two context globals work the same way: a name in one global object is not in the other.
A tiny host works with prices. Which function does it add to its global object?
const globalObject = {};
globalObject.double = (price) => price * 2;
console.log(globalObject.double(10));Add double. A host gives JavaScript a tool by picking a name and connecting that name to a function.
When a HandleScope ends, which handles go away together?
The local handles go away together when their HandleScope ends. C++ code doesn't have to let go of them one by one.
You are building a small runtime that runs price rules for tests. What must you decide before you run a rule?
Decide which host APIs, or globals, to add before any code runs. That choice sets what the code can ask the host to do.
A runtime starts many times a day. What work, done again at every start, can a startup snapshot skip?
A startup snapshot skips repeated setup work, such as setting up the built-ins again every time.
Check your understanding
For each question, first ask which part it is about: V8 itself, a context, the C++ code in the host, or a choice the host makes.
Question 1 of 7Who decides whether
documentexists onglobalThis?Choose an answer to see the explanation.
Question 2 of 7What does this code print for the two objects?
Read the code, then predictPop out in the code editor (opens in a new tab)JavaScriptconst first = { price: 10 }; const second = {}; console.log(first.price * 2, typeof second.price);Choose an answer to see the explanation.
Question 3 of 7Which sentence describes an isolate?
Choose an answer to see the explanation.
Question 4 of 7Why does C++ code hold a V8 object through a local handle?
Choose an answer to see the explanation.
Question 5 of 7What does a FunctionTemplate describe?
Choose an answer to see the explanation.
Question 6 of 7What is a startup snapshot for?
Choose an answer to see the explanation.
Question 7 of 7Which Node tool lets JavaScript make a context with its own globals?
Choose an answer to see the explanation.
Key takeaways
- V8 runs JavaScript. The host decides which extra tools and globals your code gets.
- An isolate is one copy of V8 with its own heap. A context is one JavaScript world inside it, with its own globals.
- Handles let C++ code hold V8 values safely. A HandleScope lets go of all its local handles at once.
- Templates are recipes. They let a host add C++ functions and objects to JavaScript.
- A startup snapshot is a heap saved after setup, so each start does less work.
Remember the one-liner.
A host turns V8 into a runtime by choosing its worlds, its tools, and how it starts.
Coming next: how Node.js is built. You'll see how V8, a library called libuv, and C++ code fit together into one server runtime.