B6 · Part 1 · Beginner
Inside the browser
From a script tag to pixels on screen — and where your code sits in that.
08On this page
The journey your file actually takes
You write app.js. Somewhere between that and a button
lighting up on screen, a lot happens that nobody shows you. Here is
the whole route, and then each stop in turn.
Step 1 — the parser meets your script tag
The browser reads HTML top to bottom, building the DOM as it goes.
Then it hits a <script>, and by default it does
something drastic: it stops. Parsing halts, the file is
downloaded, the file is executed, and only then does HTML parsing
resume.
This is not an accident. A classic script is allowed to call
document.write and inject markup at that exact position,
so the browser cannot safely read ahead. One slow script in
<head> means a blank page until it lands — no text,
no layout, nothing.
HTML parser
Downloading in the background
Running on the main thread
What the user sees
| plain | defer |
async |
|
|---|---|---|---|
| Blocks HTML parsing | yes | no | only while it executes |
| Runs when | immediately, in place | after the DOM is parsed | the moment it arrives |
| Keeps source order | yes | yes | no — whoever lands first |
| Can it see the whole DOM? | only what is above it | yes, all of it | no guarantee |
| Use it for | almost nothing | your app code | independent scripts: analytics |
<script type="module"> is deferred automatically —
you get defer behaviour without asking. That is one more
reason the module version is the sane default.
⚠ The classic "it's null!" bug
A plain script in
<head> that calls
document.querySelector("#app") gets null —
not because the selector is wrong, but because the parser never
reached that element. Move the tag to the end of
<body>, add defer, or wait for
DOMContentLoaded.
Step 2 — what the engine does with the file
Your source is text. The CPU runs machine code. The engine (V8 in Chrome and Node, SpiderMonkey in Firefox, JavaScriptCore in Safari) closes that gap in stages:
-
Tokenise — chop the text into meaningful pieces:
const,total,=,42. - Parse into an AST — an abstract syntax tree, the shape of your program as data. A syntax error is thrown here, before a single line runs, which is why one stray brace kills the entire file rather than just its own line.
- Compile to bytecode — compact instructions an interpreter executes right away. This is why JS starts fast instead of waiting on a full compile.
- Watch, then optimise — the engine counts how often each function runs. A function that runs constantly is "hot", so an optimising compiler rewrites it into real machine code, betting on the types it has seen so far.
- Deoptimise when the bet fails — feed a function that always got numbers a string, and the optimised version is thrown away and the engine falls back to bytecode.
Two useful consequences at beginner level. First, syntax errors are found before execution — so a typo at the bottom of the file stops the top of it from running. Second, keeping a function's inputs the same shape is not folklore, it is what keeps the optimised version alive. The full story, plus how to see it happening, is in Engine & memory.
console.log("does this line run?");
// A syntax error further down — uncomment to see
// const broken = ;
console.log("parse succeeded, so both lines print");
Remove the comment markers and run it again: the first log never appears. Nothing executed, because parsing never finished.
Step 3 — pixels, and the 16 millisecond budget
Most screens redraw 60 times a second. That gives the browser about 16.7ms to produce each frame — and your JavaScript shares that window, because it is the same thread. A frame is built in four stages:
- Style — which CSS rules apply to which element now.
-
Layout (reflow) — the geometry: how big everything is and
where it sits. Changing
width,topor text content forces this, and it cascades to children. - Paint — filling in pixels: colours, shadows, text.
-
Composite — stitching the painted layers together, usually on
the GPU.
transformandopacitycan often be done here alone, skipping layout and paint entirely — which is exactly why animating those two is smooth and animatingleftis not.
Rendering only gets its turn when the stack is empty. So a function that takes 100ms does not "slow down" the animation — it deletes six frames outright.
Dry run: counting the frames a 100ms handler actually costs
Same numbers as the demo above, as a table. Each frame gets a 16.7ms budget; the click handler needs 100ms in one go, starting partway through frame 3.
| Frame | Main thread | Rendered on screen? |
|---|---|---|
| 1 | idle between frames — style, layout, paint, composite run in budget | yes |
| 2 | same as frame 1 | yes |
| 3 | a click starts the 100ms handler — the thread is now busy | no — due, but the stack is not empty |
| 4–8 (5 frames) | still inside the same 100ms handler | no — each one arrives, finds the thread busy, and is simply never built |
| 9 | handler returns partway through — thread frees up | yes — but jumps straight to the new state instead of animating through it |
16.7ms × 6 ≈ 100ms — one blocking call is long enough to swallow six consecutive frame budgets whole (frame 3 through frame 8). Nothing queues those frames for later: a frame the browser could not build in its window is simply skipped, not delayed.
Where the event loop fits in a frame
Put the last chapter and this one together and the browser's turn order becomes concrete. Per pass, roughly:
- run one task from the queue — a timer, a click handler, a network callback
- drain every microtask queued by it, completely
-
run
requestAnimationFramecallbacks — the moment designed for "change something visual" - style, layout, paint, composite — if there is time before the next frame
Two practical rules fall out of that list.
requestAnimationFrame is where visual updates belong,
because it runs immediately before rendering rather than at some
arbitrary point mid-frame. And reading a geometric property like
offsetHeight right after writing a style forces the
browser to run layout early, in the middle of your loop, to
answer you honestly — do that inside a loop over 200 elements and you
have written the classic layout thrash.
// Thrashing: read, write, read, write - a forced layout each pass
for (const box of boxes) {
box.style.height = box.offsetHeight + 10 + "px";
}
// Batched: all reads, then all writes - one layout
const heights = boxes.map((b) => b.offsetHeight);
boxes.forEach((b, i) => { b.style.height = heights[i] + 10 + "px"; });
The events that tell you the page is ready
| Event | Fires when | Use it for |
|---|---|---|
DOMContentLoaded |
HTML parsed, deferred scripts done. Images may still be coming. | almost everything — finding elements, wiring listeners |
load |
every image, stylesheet and iframe has finished | measuring images, final-size layout work |
beforeunload |
the user is leaving | "you have unsaved changes" — sparingly |
If your script is deferred or sits at the end of
<body>, you usually need neither. The DOM is
already there.
Seeing it for yourself
None of this is theoretical — the browser will show you every stage. In DevTools, the Network panel shows which script blocked the parser, and the Performance panel records a real load: a flame chart of the call stack over time, long tasks flagged in red, and the layout/paint work in between. Record five seconds of your own page and the pipeline in this chapter appears, labelled, in front of you.
Say it like this → "The browser parses
HTML into a DOM, and stops dead at every plain script tag. The engine
turns my code into bytecode and optimises what runs hot. Then
rendering and my JavaScript take turns on one thread, sixty times a
second — so a long function does not slow the page down, it skips
frames."
What to carry forward
-
defer(or a module) for your own scripts;asynconly for things that do not touch your DOM. - Syntax errors are found at parse time, before anything runs.
- 16.7ms per frame, shared with your code. Over ~50ms in one function and users feel it.
-
Animate
transformandopacity; they can skip layout and paint. - Batch DOM reads and writes instead of alternating them.
Interview answer, one sentence
"The parser stops for a synchronous script, so defer, async or a module keeps it from blocking; after that the event loop has roughly 16 ms a frame for JavaScript, style, layout and paint, and long tasks on the main thread are what make a page feel slow."
Before you move on
- Explain why a plain
<script>in<head>can makedocument.querySelectorreturnnull, and name two fixes. - Compare
deferandasyncon whether they block parsing, when they run, and whether source order is kept. - Explain why a syntax error at the bottom of a file stops a
console.logat the top from ever running. - Given a function that blocks the thread for 100ms, calculate roughly how many 16.7ms frames it costs — and say why "delayed" is the wrong word for what happens to them.
- Say which two CSS properties can animate without triggering layout or paint, and why batching DOM reads before writes avoids layout thrash.
Opens in the editor: write it, run it, and check it against real tests.
All 35 for this level →Finished reading?
0 of 41 chapters read in this series