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 parsingSeparate full parsing from preparsing and describe why most startup functions begin with a lightweight pass.
- 02Predict eager casesIdentify PIFEs, startup calls, and explicit compile hints that make V8 parse a function earlier.
- 03Measure the trade-offUse DevTools and V8 function-event logs to spot duplicate preparse plus full-parse work.
Why engines delay work
A modern page often ships thousands of functions, but only a fraction run before the first screen is useful. If the engine fully parsed and compiled every body immediately, startup would spend CPU time and memory on menus, settings panels, validators, and feature code the user may never open.
Lazy parsing means the engine checks a function body with a lightweight preparser at load time, then waits to do the full parse and compile until the first call needs that function.
Imagine a teacher checking a textbook before class. They skim the table of contents and headings to know what exists, but they read a chapter closely only when that chapter is on today's plan. Lazy parsing uses the same budget: validate and remember structure now, read the details when execution reaches them.
- In real life: Skim chapter titles and headings
- In JavaScript: Preparse function bodies
- In real life: Notice a broken table of contents
- In JavaScript: Catch syntax and early errors
- In real life: Mark which chapters refer to earlier ideas
- In JavaScript: Record scope and outer-variable facts
- In real life: Read one chapter carefully when assigned
- In JavaScript: Full-parse and compile on first call
Where the analogy stops: A book can be skimmed by looking at headings. JavaScript syntax is more complex, so the preparser still has to parse real syntax; it just avoids building the full compiler-ready result.
This lesson builds on function expressions, closures, and loading performance. It also connects to the engine path from Meet the engines, the parser work in Parsing & ASTs, the variable facts in Scope analysis, and the startup trade-offs in Startup performance.
Full parse vs lazy parse
CORE CONTRASTA full parse builds the representation a compiler can use for a function body. In V8, that feeds Ignition's bytecode compiler. A preparse is still a real syntax pass, but it records less: enough to know the function is valid, where it ends, and which scope facts matter later.
| Term | What it means | Why it matters |
|---|---|---|
| Full parse | Builds the real parser output for a function body, including the AST shape needed by the bytecode compiler. | Needed before a function can run; expensive if many shipped functions never run. |
| Preparse | Checks syntax and early errors and records enough scope and boundary facts to come back later. | Fast startup path for ordinary functions; not the same as ignoring the body. |
| Reparse | A previously preparsed function is fully parsed when the first call reaches it. | Duplicate work when the function is called immediately after load. |
| Code cache hit | Chrome can reuse serialized bytecode for unchanged scripts on hot repeat visits. | Avoids some parse/compile work, but it is a cache with version and source-matching limits. |
The visible program below defines four functions. Startup calls only startApp(). That means the settings panel and the click handler body are useful code, but not startup code.
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());Line by line: line 1 defines the startup function; line 6 defines a helper that returns a future click handler; line 12 defines a small helper that is called now; line 16 defines a settings feature; line 20 calls only the startup path. Lazy parsing is the reason line 16 does not have to pay full parser cost during the first load.
The preparser
V8 FACTSV8's 2019 article Blazingly fast parsing, part 2: lazy parsing explains why the preparser cannot simply count braces. JavaScript grammar is too complex; it must parse syntax enough to find the real function end and enforce early errors. The saving is that it avoids building the full AST and bytecode-ready structures for bodies that are not needed yet.
The preparser also records just enough about inner functions for later scope analysis: declarations, references to outer variables, function boundaries, and data that lets the full parser resume without starting nested preparsing from scratch. That connects directly to closures: if an inner function references an outer variable, the engine must remember that fact before deciding where variables live.
Step through a page that defines more functions than startup calls. Watch ordinary functions start as preparsed, then become fully parsed only when a call reaches them.
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 still has valid JavaScript syntax. If it contained an early error, the script could not safely continue as if the body did not exist. The preparser is a correctness pass plus a performance shortcut.
Skipping inner functions when reparsing an outer function
NESTED CODEOlder lazy parsing could become non-linear for deeply nested code: an outer function was preparsed, then later fully parsed, and inner functions could be preparsed again along the way. V8's 2019 work stores inner-function preparse data so a later full parse of the outer body can skip inner bodies that still are not called.
Step through V8's lazy nested-function idea: preparse once, save inner-function facts, then skip uncalled inner bodies when an outer function is reparsed for execution.
script
function readTitle() { return "Dashboard"; } function renderChart() { return "Chart"; } return readTitle();} console.log(outerRoute());The key phrase is skip inner bodies, not ignore them. The engine already knows where readTitle and renderChart begin and end, and it knows the scope facts it must preserve. When readTitle() is called, that inner function gets its own full parse. Its sibling can keep waiting.
Possibly-invoked function expressions
PIFEA function wrapped in parentheses, such as (function () { ... })(), is often an IIFE: an immediately-invoked function expression. V8 calls the recognizable parser signal a PIFE, a possibly-invoked function expression, because the parser notices the likely call pattern early and can eagerly parse and compile that body.
(function bootPife() { console.log("pife boot");})(); function bootNormal() { console.log("normal boot");} bootNormal();In V8's 2019 post, the original parenthesized pattern is joined by a Chrome 57-era recognition of common UglifyJS output such as a leading !function or a comma-separated ,function after a PIFE. The old optimize-js package used static heuristics to add these hints. That helped older V8 versions much more than it helps modern V8, because parser speed and inner-function skipping improved. As of September 2026, treat PIFE wrapping as a measured startup hint, not a formatting rule.
(function boot() { start(); })();function boot() { start(); } boot();button.addEventListener('click', function openPanel() { ... });!function(){ boot(); }(),function(){ more(); }();//# allFunctionsCalledOnLoadexport function openSettings() { ... }
Sort each code shape by how it influences the parser decision.
Reparsing costs
REAL V8 LOGLazy parsing is a trade-off. A function that is defined but never called avoids full parse work. A function that is called immediately can pay twice: first the lightweight preparse, then the full parse on demand. V8's function-event log makes that structure visible without relying on invented flags.
function uncalledCold() { return 41; }function calledHot() { return 42; }console.log(calledHot());Node 22.23.1 (V8 12.4)
flags used: --log-function-events, --logfile, --no-logfile-per-isolate
uncalledCold: preparse-resolution; no full-parse; no first-execution
calledHot: preparse-resolution -> full-parse -> parse-function -> first-execution
pifeNow: full-parse at load; no parse-function event
normalNow: preparse-resolution -> full-parse -> parse-function -> first-executionThe tests spawn child Node processes with --log-function-events, --logfile, and --no-logfile-per-isolate, after verifying those flags with node --v8-options. They delete NODE_TEST_* environment variables and remove the generated log directory after parsing it.
Explicit compile hints
CHROME 136V8's 2025 post Giving V8 a Heads-Up says Chrome 136 ships file-based explicit compile hints. The exact magic comment is //# allFunctionsCalledOnLoad, placed at the top of a JavaScript file. It tells V8 that all functions in that file are expected to be called during page load, so eager compilation may save on-demand stalls and duplicate parsing.
//# allFunctionsCalledOnLoadfunction hydrateHeader() { return "header";} function attachSearch() { return "search";} console.log(hydrateHeader() + " + " + attachSearch());The same V8 post warns that compiling too much consumes time and memory. The WICG explainer separates the shipped file-level feature from per-function hints, which remain a design sketch rather than stable Chrome syntax as of September 2026.
Measure it
DEVTOOLS + FLAGSFor a real website, start in Chrome DevTools' Performance panel and record a reload. Search the main thread for Compile Script and Compile Code events, then connect those events to the bundle and the route that caused them. For controlled experiments, V8's function-event log is useful, but always check available flags in the runtime you are testing.
# 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.jsCode caching is a separate repeat-visit mechanism. The V8 posts on code caching for developers and improved code caching explain that Chrome can serialize bytecode for unchanged scripts and later deserialize it. That can reduce parse and compile time on hot visits, but it does not make a first visit or changed source free.
Lazy engine simulator
ACORN MODELThe playground below parses the visible source with acorn, extracts a real function tree, and runs a teaching model over it. The counters are deliberately simple parse-work units, not V8 timings, but the relationships are the ones you just saw: lazy preparsing, eager PIFEs, duplicate work for immediately-called ordinary functions, and whole-file compile hints.
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, openSettingsPanelstartApp301818The model preparses ordinary functions, eagerly compiles recognized PIFEs, then full-parses only startup calls. A called function that was preparsed is counted as double parsed.
Production choices
In production, the biggest win is usually shipping less startup code: split optional features, avoid pulling admin tools into the public route, and keep large libraries behind the interaction that needs them. Lazy parsing helps once source text is already present; it does not replace code splitting or byte budgets.
- Measure startup compile events on a throttled profile.
- Identify functions that are called during load versus later interactions.
- Prefer moving optional code out of the initial bundle when possible.
- Use PIFE shape or file-level compile hints only for profiled core startup code.
- Retest memory and interaction timing after making code more eager.
If a file contains ten functions and only two run during load, a file-wide compile hint may make the first two smoother while making the other eight wasteful. If all ten really run during load, the hint can move work earlier and make it easier to parallelize.
Common misconceptions
“Lazy parsing skips syntax errors.”
The preparser checks syntax and early errors. Lazy code is not unchecked code.
“Lazy parsing and lazy loading are the same.”
Lazy loading delays fetching code. Lazy parsing delays full parser work after source text is already available.
“PIFE wrapping is always faster.”
It helps only when the function is needed on startup. Over-eager parsing costs memory and can slow load.
“Code caching removes the need to care about parse cost.”
Code caching is heuristic and repeat-visit oriented. First visits, changed files, and execution still matter.
“Compile hints are per-function today.”
The stable Chromium feature covered here is file-level. Do not invent per-function syntax.
| Term | What it delays or reuses | Important boundary |
|---|---|---|
| Lazy parsing | Delays full parsing/compilation of function bodies | Still checks syntax and records scope facts. |
| Lazy loading | Delays fetching a script or chunk | A lazy-loaded chunk can still be lazily parsed after it arrives. |
| Code caching | Reuses compiled bytecode for identical source | Helps repeat visits; first visits and changed files still pay. |
| Explicit compile hints | Ask V8 to eagerly compile a whole file | Useful for core files where most functions really run on load. |
Practice exercises
5 EXERCISESType the two parse-stage names in the order printed.
const seen = ["preparse", "full parse"];
console.log(seen.join(" -> "));The snippet prints preparse -> full parse, the two stages a lazily-called startup function can pay.
Type the acronym for the eager-parsing hint shape.
(function boot() {
return "eager candidate";
})();
console.log("PIFE");The shape is a PIFE signal. It is also an IIFE because this example really calls it immediately.
Type the total printed by the small cost model.
const costs = { preparse: 5, fullParse: 12 };
console.log(costs.preparse + costs.fullParse);The program prints 17. A real engine has different units, but the duplicate-work idea is preparse plus full parse.
Type the exact compile-hint comment from the lesson.
const firstLine = "//# allFunctionsCalledOnLoad";
console.log(firstLine);//# allFunctionsCalledOnLoadPlace this at the top of a file only when its functions really are load-critical.
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 inspect which script caused them.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7Why do engines lazy-parse most function bodies during startup?
Choose an answer to see the explanation.
Question 2 of 7What does this startup 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 7Which shape did V8 historically treat as a possibly-invoked function expression?
Choose an answer to see the explanation.
Question 4 of 7What is the duplicate work when an immediately-needed function was not eagerly parsed?
Choose an answer to see the explanation.
Question 5 of 7What does the exact file-level compile hint print in this exercise?
Read the code, then predictconst firstLine = "//# allFunctionsCalledOnLoad"; console.log(firstLine);Choose an answer to see the explanation.
Question 6 of 7Which statement about per-function compile hints is accurate as of September 2026?
Choose an answer to see the explanation.
Question 7 of 7Where can you measure parse and compile work in a browser?
Choose an answer to see the explanation.
Key takeaways
- V8 lazy-parses ordinary function bodies because most functions shipped to a page are not called during startup.
- The preparser still checks syntax and early errors and records scope facts; it does not ignore the body.
- Calling a lazy function immediately creates duplicate work: preparse first, full parse on first call.
- PIFEs and file-level compile hints move selected code toward eager parsing, which helps only when profiling says the code is load-critical.
- Code caching helps repeat visits; it is separate from first-load lazy parsing decisions.
Remember the one-liner.
Lazy parsing buys startup time by proving function bodies are valid now and doing the full compiler-ready parse only for bodies execution reaches.
Next: Scope analysis & variable allocation, where those preparser facts become decisions about where variables live.