Lazy parsing & preparsing
Learn how V8 preparses JavaScript functions, delays full parsing until calls, avoids inner reparse work, and uses compile hints for startup.
- 01Explain lazy parsingTell a full parse from a quick check, the preparse, and say why most functions get only the quick check at load.
- 02Spot early full parsesFind the code that makes V8 fully parse a function at load: round brackets before
function, startup calls, and the compile hint. - 03Measure the costUse DevTools and V8's function-event log to find functions that are read twice: a preparse, then a full parse.
Why engines put off work
A web page often sends the browser thousands of functions. Only a few of them run while the page first loads. The rest wait for a click, a menu, or a settings panel. Some never run at all.
The program that runs JavaScript is called an engine. Chrome and Node.js both use an engine called V8. Before an engine can run a function, it does two jobs:
- It parses the function: it reads the text and builds a tree that describes the code. That tree is called an AST, short for abstract syntax tree.
- It compiles the function: it turns the tree into bytecode, a list of simple steps the engine can run.
Both jobs take time and memory. If the engine did them for every function at load, it would waste work on code that nobody calls yet.
The rules of JavaScript, called the ECMAScript specification, say what a program must do. They don't say when the engine parses each function. This lesson shows what V8 does. It uses V8's own articles and real logs from Node.js. The engines in Firefox and Safari may choose differently, but your program prints the same result in every engine.
In V8, lazy parsing means this: when a script loads, V8 gives each ordinary function only a quick check, called a preparse. The full parse and the compile wait until code calls that function for the first time.
A teacher gets a new textbook before the school year starts. She skims it: the chapter titles, the headings, the page numbers. Now she knows what each chapter is about and where it starts. She reads a chapter closely only on the day she teaches it.
- In real life: Skim the chapter titles and headings
- In JavaScript: Preparse each function body
- In real life: Notice that a chapter is missing
- In JavaScript: Catch syntax errors early
- In real life: Note which chapters refer back to earlier ones
- In JavaScript: Note which outer variables a function uses
- In real life: Read a chapter closely on the day you teach it
- In JavaScript: Full parse and compile on the first call
This lesson builds on function expressions, closures, and loading performance. It also links to these lessons:
- Meet the engines, for the steps an engine takes.
- Parsing & ASTs, for how the parser builds its tree.
- Scope analysis, for where variables are stored.
- Startup performance, for making pages start faster.
A full parse and a quick check
CORE CONTRASTA full parse reads every part of a function body and builds its whole tree. V8 then hands that tree to its bytecode compiler, which is called Ignition.
A preparse is a lighter read. It still checks the grammar of the whole body, but it keeps only a few facts:
- that the function has no syntax errors,
- where the function ends,
- which variables from outside the function it uses.
Parsing and compiling are two different jobs. Parsing turns text into a tree. Compiling turns that tree into bytecode. V8 usually does both on a function's first call, but they are still two separate steps.
function openSettings() { return "settings opened";} console.log("home ready");Line 1 creates a function called openSettings. Nothing ever calls it. Line 5 prints home ready, so that is the only output. JavaScript promises that output in every engine.
The output can't tell you whether the engine preparsed, fully parsed, or compiled openSettings. Those steps happen inside the engine, where your code can't see them.
The table below names four engine words that are easy to mix up. Read each row as: the word, what it means, and why it matters.
| Word | What it means | Why it matters |
|---|---|---|
| Full parse | V8 reads the whole function and builds its full tree, which the compiler turns into bytecode. | A function needs it before it can run. It is costly if most functions never run. |
| Preparse | V8 quickly checks the syntax and notes where the function ends and which outer variables it uses. | It keeps startup fast. It still checks the whole body; it doesn't ignore it. |
| Reparse | A function that got only a preparse gets its full parse when it is first called. | It is extra work when a function runs right after the page loads. |
| Code cache hit | Chrome reuses saved bytecode for a script that hasn't changed, on a repeat visit. | It skips some parse and compile work, but only when the cache has a match for the same file. |
The next program creates four functions at the top level, and one more function inside one of them. At startup, line 20 calls only startApp(). The settings panel and the click handler are useful later, but startup doesn't need them.
function startApp() { registerClickHandler(); return showShell();} function registerClickHandler() { return function onClick() { return openSettingsPanel(); };} function showShell() { return "shell ready";} function openSettingsPanel() { return "settings ready";} console.log(startApp());In plain words, line by line:
- Line 1 creates
startApp, the function that startup runs. - Line 6 creates
registerClickHandler. It returns a new function,onClick, for a later click. - Line 12 creates
showShell, a small helper that startup calls. - Line 16 creates
openSettingsPanel. Only a click would call it. - Line 20 calls
startApp(). This is the only call at the top level of the file.
With V8's normal settings, the function on line 16 gets no full parse and no compile during the first load. It gets only the quick check.
startApp(), which calls two helpers. Those three get a full parse when called. The other two functions get only the quick check.The preparser
V8 FACTSV8 explained its preparser in a 2019 article, Blazingly fast parsing, part 2: lazy parsing. One point in it: the preparser can't just count curly braces to find where a function ends. JavaScript's grammar is too complex for that. For example, a brace can sit inside a string. So the preparser really reads the syntax. What it saves is the work of building the full tree and the bytecode.
The preparser also writes down a few facts about the functions inside each body:
- which variables each inner function creates,
- which outer variables each inner function uses,
- where each inner function starts and ends.
These facts matter for closures. A closure is a function that keeps using variables from the function around it. If an inner function uses an outer variable, the engine must know that before it decides where to store the variable. The saved facts also let a later full parse start faster, as the next section shows.
Step through a page that creates more functions than startup calls. Plain functions start with a quick check. Each one gets a full parse only when a call reaches it.
script
registerClickHandler(); return showShell();} function registerClickHandler() { return function onClick() { return openSettingsPanel(); };} function showShell() { return "shell ready";} function openSettingsPanel() { return "settings ready";} console.log(startApp());A lazily parsed function must still be valid JavaScript. Some mistakes, called early errors, stop the whole script before any line runs. Creating the same let name twice is one of them. The preparser finds these mistakes. So the preparser does two jobs: it checks that the code is correct, and it saves time.
Skipping inner functions when the outer one is fully parsed
NESTED CODEA nested function is a function written inside another function. Older V8 versions did extra work with nested functions:
- At load, V8 preparsed the outer function, and the inner functions inside it.
- Later, a call made V8 fully parse the outer function.
- During that full parse, V8 preparsed the inner functions all over again.
With many levels of nesting, this repeated work grew very fast. Since 2019, V8 saves the facts about inner functions from the first preparse. A later full parse of the outer function reads those facts and skips the inner bodies that still aren't called.
Step through how V8 handles nested functions: one quick check at load, saved facts about the inner functions, and then a full parse of the outer function that skips the inner bodies nobody calls.
script
function readTitle() { return "Dashboard"; } function renderChart() { return "Chart"; } return readTitle();} console.log(outerRoute());The key idea: V8 skips the inner bodies, but it doesn't forget them. It already knows where readTitle and renderChart start and end, and which variables they use. When line 10 calls readTitle(), that inner function gets its own full parse. renderChart is never called, so it keeps waiting.
outerRoute() runs, V8 uses the facts it saved earlier. It fully parses readTitle, which line 10 calls, and skips the body of renderChart.Functions that will likely run right away
PIFEA function written as a value, like the right side of const f = function () {}, is a function expression. Wrap one in round brackets and call it right away, like (function () { ... })(), and you get an IIFE: an immediately invoked function expression. Invoked means called.
V8 looks for one sign that a function will run right away: a round bracket ( just before the word function. V8 calls a function with that sign a PIFE, a possibly invoked function expression. It says "possibly" because the parser sees the bracket before it knows whether a call follows. For a PIFE, V8 skips the preparse. It fully parses and compiles the function at load.
(function bootPife() { console.log("pife boot");})(); function bootNormal() { console.log("normal boot");} bootNormal();Line 1 wraps bootPife in round brackets, and line 3 calls it straight away. Line 5 creates bootNormal the plain way, and line 9 calls it. The program prints pife boot, then normal boot.
function tells V8 that bootPife will likely run now, so it gets one full parse at load. bootNormal is read twice: a quick check, then a full parse on its call.V8's 2019 article also mentions two more shapes. Around Chrome 57, V8 learned to spot the shapes that the UglifyJS minifier writes. A minifier is a tool that shrinks code before you ship it. The two shapes are:
- a
!functionat the start of a line, - a
,functionafter a comma that follows an earlier PIFE.
An old package called optimize-js added these brackets for you. It guessed which functions would run at startup. That helped older V8 versions a lot. It helps modern V8 much less, because the parser got faster and no longer preparses inner functions twice. Add PIFE brackets only when a measurement shows that they help.
(function boot() { start(); })();function boot() { start(); } boot();button.addEventListener('click', function openPanel() { ... });!function(){ boot(); }(),function(){ more(); }();//# allFunctionsCalledOnLoadexport function openSettings() { ... }
Sort each piece of code by what V8 does with it when the script loads.
The cost of reading a function twice
REAL V8 LOGLazy parsing is a trade. A function that is never called saves its full parse. But a function that runs right away can pay twice: first the preparse at load, then the full parse on its first call. That second read is called a reparse.
V8 can write a log of these events. Below is the small program that the lesson tests run, and then a summary of what the log said.
function uncalledCold() { return 41; }function calledHot() { return 42; }console.log(calledHot());Line 1 creates uncalledCold, which nothing calls. Line 2 creates calledHot. Line 3 calls calledHot() and prints 42.
Node 22 (V8 12.4 family)
flags used: --log-function-events, --logfile, --no-logfile-per-isolate
uncalledCold: preparse event; no full-parse; no first-execution
calledHot: preparse event -> full-parse -> parse-function -> first-execution
pifeNow: full-parse at load; no parse-function event
normalNow: preparse event -> full-parse -> parse-function -> first-executionHow to read the summary, line by line:
- Line 1 names the Node.js version and its V8 version.
- Line 2 lists the flags the tests used. A flag is an option you add to the
nodecommand. - Line 3:
uncalledColdgot only a preparse. It got no full parse, and it never ran. - Line 4:
calledHotgot a preparse first. When line 3 called it, the log shows afull-parseand aparse-functionevent. That is the reparse. Then the function ran for the first time. - Line 5:
pifeNowcomes from a second small program, where it is a PIFE. It got its full parse at load and no reparse. - Line 6:
normalNow, a plain function in that same program, was read twice, likecalledHot.
The tests run Node.js with three flags:
--log-function-eventsturns the log on.--logfilenames the file to write.--no-logfile-per-isolatekeeps the file name exactly as written.
Before that, the tests run node --v8-options to check that these flags exist. They also remove the NODE_TEST_* environment variables, and they delete the log folder when they finish.
This log is proof for one V8 version only. Chrome and Node.js both use V8. Firefox and Safari use other engines, with different tools and different event names.
Explicit compile hints
CHROME 136In 2025, V8 wrote a post called Giving V8 a Heads-Up. It says Chrome 136 added explicit compile hints for whole files. A compile hint is a special comment that tells V8 what to do. The exact comment is //# allFunctionsCalledOnLoad, and it goes at the top of a JavaScript file.
The comment tells V8: "Every function in this file runs while the page loads." So V8 compiles them all at load. That can avoid a pause on each first call, and it avoids reading each function twice.
//# allFunctionsCalledOnLoadfunction hydrateHeader() { return "header";} function attachSearch() { return "search";} console.log(hydrateHeader() + " + " + attachSearch());Line 1 is the hint. Lines 2 and 6 create two functions. Line 10 calls both of them and prints header + search. Every function in this file really runs at load, so the hint fits.
The same V8 post warns that compiling too much costs time and memory. A group that designs new web features, the WICG, wrote an explainer that keeps two ideas apart. The hint for a whole file has shipped. Hints for single functions are only a draft idea. Chrome has no stable way to write them.
Measure parse and compile work
DEVTOOLS + FLAGSTo measure a real website, use Chrome DevTools. Open the Performance panel and record a page reload. Then search the main thread for two kinds of events:
- Compile Script: V8 parses and compiles a whole script.
- Compile Code: V8 compiles one function, often on its first call.
Match each event to the file and the page that caused it. For small tests on your own computer, V8's function-event log works well too. Always check that a flag exists in the version you test.
# Browser trace: open DevTools > Performance and record reload.# Look for V8 "Compile Script" and "Compile Code" events. node --v8-options | grep -E -- "--(lazy|log-function-events|logfile)"node --log-function-events --no-logfile-per-isolate --logfile=v8.log app.jsLines 1 and 2 are notes for the browser. Line 4 lists V8's flags and keeps only the lines about lazy parsing, the function-event log, and the log file. Line 5 runs app.js with the function-event log turned on, and writes the log to v8.log.
Code caching is a different idea, for repeat visits. V8's posts on code caching for developers and improved code caching explain it. Chrome can save the bytecode of a script that hasn't changed, and load it again on the next visit. That cuts parse and compile time on repeat visits. A first visit, or a changed file, still pays the full cost.
Lazy engine simulator
ACORN MODELThe playground below reads the code on the left with acorn, a JavaScript parser written in JavaScript. It finds every function and every call. Then it counts the work with a simple model. Each function costs one unit per line of code it has. These units are made up for teaching; they are not V8 times.
Pick a strategy to compare:
- Lazy default: preparse plain functions, fully parse PIFEs at load, and fully parse the others when they are first called.
- Eager all: fully parse every function at load.
- File hint: act as if the file starts with the compile hint.
The model only follows direct calls by name, like startApp(). It can't predict method calls, callbacks chosen while the code runs, code-cache hits, or the extra speed-ups a real engine adds later. Use it to compare strategies. To study a real app, record a real trace.
function startApp() { registerClickHandler(); return showShell();} function registerClickHandler() { return function onClick() { return openSettingsPanel(); };} function showShell() { return "shell ready";} function openSettingsPanel() { return "settings ready";} console.log(startApp());preparse 18, full 12, total 303/5startApp, registerClickHandler, showShellonClick, openSettingsPanelstartApp301818Plain functions get a quick check at load, and PIFEs get a full parse at load. Then only the functions startup calls get a full parse. A called function that already had a quick check is counted as double parsed.
Choices for real websites
In a real product, the biggest win is usually to send less code at startup:
- Split optional features into separate files.
- Keep admin tools out of the public pages.
- Load large libraries only when the user needs them.
Lazy parsing helps only after the code has arrived in the browser. It doesn't replace splitting your code or keeping your files small.
button.addEventListener("click", async () => { const { openEditor } = await import("./editor.js"); openEditor();});Line 1 waits for a click on a button. Line 2 uses import(), called a dynamic import. It downloads and runs a separate file, editor.js, only at that moment. Line 3 opens the editor. You can't run this example here, because it needs a real button on the page and a separate editor.js file.
A safe order of work:
- Measure compile events at startup, with the CPU slowed down in DevTools.
- List which functions run at load, and which run later, after a click or another action.
- Move optional code out of the first file you send, when you can.
- Use PIFE brackets or the file hint only for startup code you have measured.
- After making code compile earlier, measure memory and click response again.
Say a file has ten functions, and only two run at load. A compile hint for the whole file may make those two start faster. But it also compiles the other eight for nothing. If all ten run at load, the hint moves the work earlier, and V8 can do more of it at the same time as other work.
Common misconceptions
“Lazy parsing skips syntax errors.”
The preparser checks the syntax and finds early errors. A lazily parsed function is still checked.
“Lazy parsing and lazy loading are the same.”
Lazy loading waits to download code. Lazy parsing waits to fully parse code that has already arrived.
“Wrapping a function in brackets always makes it faster.”
It helps only when the function runs at startup. Fully parsing functions too early costs memory and can slow the page load.
“With code caching, parse time doesn't matter.”
Code caching helps only on repeat visits, and only when Chrome decides to cache. First visits, changed files, and running the code still take time.
“You can give a compile hint to one function today.”
The stable hint covered here works on a whole file. Don't make up a syntax for single functions.
The table below compares lazy parsing with three ideas that sound alike. Read each row as: the idea, what it delays or reuses, and the point to remember.
| Idea | What it delays or reuses | Remember |
|---|---|---|
| Lazy parsing | It waits to fully parse and compile each function until its first call. | It still checks the syntax and notes the outer variables each function uses. |
| Lazy loading | It waits to download a file until the page needs it. | A file loaded later is still lazily parsed after it arrives. |
| Code caching | It reuses compiled bytecode for a file that hasn't changed. | It helps repeat visits. First visits and changed files still pay the full cost. |
| Explicit compile hints | It asks V8 to compile a whole file at load. | It fits core files where almost every function really runs at load. |
Practice exercises
5 EXERCISESType what the program prints: the two names, in order.
const seen = ["preparse", "full parse"];
console.log(seen.join(" -> "));The program prints preparse -> full parse. Those are the two reads a function pays when it is preparsed at load and then called.
Type the short name V8 uses for this shape.
(function boot() {
return "eager candidate";
})();
console.log("PIFE");V8 calls this shape a PIFE and fully parses it at load. It is also an IIFE, because this example really does call it right away.
Type the total that this small cost model prints.
const costs = { preparse: 5, fullParse: 12 };
console.log(costs.preparse + costs.fullParse);The program prints 17. A real engine uses other units, but the idea is the same: preparse plus full parse.
Type the exact compile-hint comment from the lesson.
const firstLine = "//# allFunctionsCalledOnLoad";
console.log(firstLine);//# allFunctionsCalledOnLoadPut this at the top of a file only when every function in it really runs at load.
Type the two DevTools event names, joined by a plus sign.
const events = ["Compile Script", "Compile Code"];
console.log(events.join(" + "));Look for Compile Script and Compile Code in the Performance panel. Then check which script caused each one.
Quiz: check your understanding
8 QUESTIONSQuestion 1 of 8Why does V8 give most functions only a quick check at startup?
Choose an answer to see the explanation.
Question 2 of 8What does this program print?
Read the code, then predictconst seen = ["preparse", "full parse"]; console.log(seen.join(" -> "));Choose an answer to see the explanation.
Question 3 of 8Which shape does V8 treat as a possibly invoked function expression, a PIFE?
Choose an answer to see the explanation.
Question 4 of 8A plain function gets a quick check at load, and then startup calls it. What is the extra work?
Choose an answer to see the explanation.
Question 5 of 8This program keeps the compile hint in a string. What does it print?
Read the code, then predictconst firstLine = "//# allFunctionsCalledOnLoad"; console.log(firstLine);Choose an answer to see the explanation.
Question 6 of 8Which statement about compile hints for single functions is true?
Choose an answer to see the explanation.
Question 7 of 8Where can you measure parse and compile work?
Choose an answer to see the explanation.
Question 8 of 8The program prints only
home ready. What does that tell you aboutopenSettings?Choose an answer to see the explanation.
Key takeaways
- Most functions a page sends don't run at startup, so V8 gives plain functions only a quick check, a preparse, at load.
- The preparse still checks the syntax, finds early errors, and notes which outer variables a function uses.
- A function that is preparsed and then called right away is read twice: a preparse at load, then a full parse on its first call.
- PIFE brackets and the compile hint for a whole file make V8 fully parse code at load. Use them only when a measurement shows the code runs at load.
- Code caching helps repeat visits. It is a separate idea from what V8 parses on a first load.
Remember the one-liner.
Lazy parsing saves startup time: V8 checks every function now, and fully parses only the functions your code actually calls.
Next: Scope analysis & variable allocation. There, the facts the preparser saved help the engine decide where each variable is stored.