completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Lazy parsing & preparsing

Learn how V8 preparses JavaScript functions, delays full parsing until calls, avoids inner reparse work, and uses compile hints for startup.

By the end, you can
  • 01
    Explain lazy parsingTell a full parse from a quick check, the preparse, and say why most functions get only the quick check at load.
  • 02
    Spot early full parsesFind the code that makes V8 fully parse a function at load: round brackets before function, startup calls, and the compile hint.
  • 03
    Measure 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.

Plain definition

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.

Real-life analogySkimming a book before reading a chapter

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:

A full parse and a quick check

CORE CONTRAST

A 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.

Creating a function does not run its bodyPop out in the code editor (opens in a new tab)JavaScript
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.

Four parsing words that are easy to mix up
WordWhat it meansWhy it matters
Full parseV8 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.
PreparseV8 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.
ReparseA 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 hitChrome 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.

A page that creates more functions than startup callsPop out in the code editor (opens in a new tab)JavaScript
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:

  1. Line 1 creates startApp, the function that startup runs.
  2. Line 6 creates registerClickHandler. It returns a new function, onClick, for a later click.
  3. Line 12 creates showShell, a small helper that startup calls.
  4. Line 16 creates openSettingsPanel. Only a click would call it.
  5. 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.

Left: the functions startup calls, startApp, registerClickHandler, and showShell, get a quick check and then a full parse. Right: the functions startup never calls, onClick and openSettingsPanel, get only the quick check.startup calls thesestartApp()registerClickHandler()showShell()quick check, then full parsestartup skips theseonClick()openSettingsPanel()quick check only
Line 20 calls 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 FACTS

V8 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.

Startup: a quick check now, a full parse on the first call
Step 0 of 8Ready
Your turn: follow the blue line

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.

Running in
  1. script
Next: line 1
  registerClickHandler();  return showShell();} function registerClickHandler() {  return function onClick() {    return openSettingsPanel();  };} function showShell() {  return "shell ready";} function openSettingsPanel() {  return "settings ready";} console.log(startApp());
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Lazy does not mean unchecked

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 CODE

A nested function is a function written inside another function. Older V8 versions did extra work with nested functions:

  1. At load, V8 preparsed the outer function, and the inner functions inside it.
  2. Later, a call made V8 fully parse the outer function.
  3. 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.

Fully parse the outer function and skip the inner bodies
Step 0 of 6Ready
Your turn: follow the blue line

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.

Running in
  1. script
Next: line 1
  function readTitle() {    return "Dashboard";  }   function renderChart() {    return "Chart";  }   return readTitle();} console.log(outerRoute());
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

The outer function outerRoute holds two inner functions. readTitle is called on line 10, so it gets a full parse. renderChart is never called, so its body is skipped.outerRoute()readTitle()renderChart()line 10 calls itfull parsenever calledbody skipped
When 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

PIFE

A 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.

A PIFE next to a plain function that is called laterPop out in the code editor (opens in a new tab)JavaScript
(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.

Top row: bootPife, wrapped in round brackets, gets one full parse at load. Bottom row: bootNormal, a plain function, gets a preparse at load, then a full parse when line 9 calls it.bootPifein ( )full parseat loadbootNormalplain functionpreparseat loadfull parseline 9 call
The bracket before 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 !function at the start of a line,
  • a ,function after 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.

Full parse now, quick check first, or file hint?
  • (function boot() { start(); })();
  • function boot() { start(); } boot();
  • button.addEventListener('click', function openPanel() { ... });
  • !function(){ boot(); }(),function(){ more(); }();
  • //# allFunctionsCalledOnLoad
  • export function openSettings() { ... }
Try it yourself
0 of 6 correct

Sort each piece of code by what V8 does with it when the script loads.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

The cost of reading a function twice

REAL V8 LOG

Lazy 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.

The small program the lesson tests runJavaScript
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.

What V8's log showed in Node 22 (V8 12.4)Text
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-execution

How to read the summary, line by line:

  1. Line 1 names the Node.js version and its V8 version.
  2. Line 2 lists the flags the tests used. A flag is an option you add to the node command.
  3. Line 3: uncalledCold got only a preparse. It got no full parse, and it never ran.
  4. Line 4: calledHot got a preparse first. When line 3 called it, the log shows a full-parse and a parse-function event. That is the reparse. Then the function ran for the first time.
  5. Line 5: pifeNow comes from a second small program, where it is a PIFE. It got its full parse at load and no reparse.
  6. Line 6: normalNow, a plain function in that same program, was read twice, like calledHot.

The tests run Node.js with three flags:

  • --log-function-events turns the log on.
  • --logfile names the file to write.
  • --no-logfile-per-isolate keeps 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 136

In 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.

A file that starts with a compile hintPop out in the code editor (opens in a new tab)JavaScript
//# 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.

A file starts with the comment allFunctionsCalledOnLoad and holds hydrateHeader and attachSearch. An arrow leads to both functions, compiled at load.file.js//# allFunctionsCalledOnLoadfunction hydrateHeaderfunction attachSearchcompiled at loadhydrateHeader()attachSearch()
The comment on line 1 asks V8 to compile every function in the file at load. Here that fits, because line 10 calls both functions at load.
Use hints sparingly

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 + FLAGS

To 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.

How to measure parse and compile workShell
# 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.js

Lines 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 MODEL

The 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.

Lazy engine simulator
The code acorn readsPop out in the code editor (opens in a new tab)JavaScript
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());
Lazy default5 functions
lazypreparse 18, full 12, total 30
compiled3/5
double parsedstartApp, registerClickHandler, showShell
skipped full parseonClick, openSettingsPanel
top-level callsstartApp
Total workwork units
Lazy default30
Eager all18
File hint18
Try it yourself
CodeStrategy

Plain 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.

A teaching model: acorn finds the real functions in the code on the left. The numbers count made-up work units, not real V8 times.

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.

Load an editor only after a clickJavaScript
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:

  1. Measure compile events at startup, with the CPU slowed down in DevTools.
  2. List which functions run at load, and which run later, after a click or another action.
  3. Move optional code out of the first file you send, when you can.
  4. Use PIFE brackets or the file hint only for startup code you have measured.
  5. After making code compile earlier, measure memory and click response again.
A review question

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.

Lazy parsing and three ideas that sound alike
IdeaWhat it delays or reusesRemember
Lazy parsingIt 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 loadingIt waits to download a file until the page needs it.A file loaded later is still lazily parsed after it arrives.
Code cachingIt 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 hintsIt asks V8 to compile a whole file at load.It fits core files where almost every function really runs at load.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upName the two reads

Type what the program prints: the two names, in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const seen = ["preparse", "full parse"];
console.log(seen.join(" -> "));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeName the function in round brackets

    Type the short name V8 uses for this shape.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    (function boot() {
      return "eager candidate";
    })();
    console.log("PIFE");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAdd up the work of reading twice

      Type the total that this small cost model prints.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const costs = { preparse: 5, fullParse: 12 };
      console.log(costs.preparse + costs.fullParse);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWrite the hint for a whole file

        Type the exact compile-hint comment from the lesson.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const firstLine = "//# allFunctionsCalledOnLoad";
        console.log(firstLine);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengePick the DevTools events

          Type the two DevTools event names, joined by a plus sign.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const events = ["Compile Script", "Compile Code"];
          console.log(events.join(" + "));

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            8 QUESTIONS
            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Why does V8 give most functions only a quick check at startup?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this program print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const seen = ["preparse", "full parse"];
              console.log(seen.join(" -> "));

              Choose an answer to see the explanation.

            3. Question 3 of 8Which shape does V8 treat as a possibly invoked function expression, a PIFE?

              Choose an answer to see the explanation.

            4. 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.

            5. Question 5 of 8This program keeps the compile hint in a string. What does it print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const firstLine = "//# allFunctionsCalledOnLoad";
              console.log(firstLine);

              Choose an answer to see the explanation.

            6. Question 6 of 8Which statement about compile hints for single functions is true?

              Choose an answer to see the explanation.

            7. Question 7 of 8Where can you measure parse and compile work?

              Choose an answer to see the explanation.

            8. Question 8 of 8The program prints only home ready. What does that tell you about openSettings?

              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.

            CompleteFrontend Clear concepts. Working examples.