more text

This commit is contained in:
tarun-elango
2026-04-26 14:09:04 -04:00
parent 26810e43d0
commit be31df2d44
22 changed files with 10664 additions and 0 deletions
+446
View File
@@ -0,0 +1,446 @@
# 03. DOM, Event Loop, and Rendering
This chapter is where browser behavior becomes concrete. Up to now, the story has been split between the JavaScript language and the browser host environment. Here those pieces meet.
When a frontend engineer says, "The UI is janky," or "The callback order is weird," or "This DOM update is expensive," they are talking about the interaction between three systems:
- the DOM and style data structures
- the event loop and scheduling model
- the rendering pipeline that turns document state into pixels
This chapter is central for both interviews and production debugging because it explains why JavaScript timing and browser painting behave the way they do.
It connects directly to:
- [02-javascript-in-the-browser.md](./02-javascript-in-the-browser.md), which introduced the browser runtime and host APIs
- [04-networking-storage-security.md](./04-networking-storage-security.md), where fetch and browser security policies rely on the same scheduling model
- [05-real-world-architecture-patterns.md](./05-real-world-architecture-patterns.md), where frameworks exploit these mechanics for app updates and performance
## The DOM: JavaScript's In-Memory View of the Document
The DOM, or Document Object Model, is the browser's in-memory representation of the HTML document as a tree of nodes.
That sentence is correct but too shallow on its own. The deeper idea is that the browser needs a structured, mutable object graph that:
- preserves document hierarchy
- can be queried and modified by code
- can participate in style calculation and layout
- can dispatch events along parent-child relationships
HTML text alone cannot do that. A tree of objects can.
### Example HTML
```html
<body>
<main id="app">
<h1>Dashboard</h1>
<button>Refresh</button>
</main>
</body>
```
### Simplified DOM Tree
```mermaid
graph TD
A[document]
A --> B[html]
B --> C[body]
C --> D[main#app]
D --> E[h1]
E --> F[text: Dashboard]
D --> G[button]
G --> H[text: Refresh]
```
### Why a Tree Structure Matters
The DOM is a tree because documents are nested. That gives the browser a natural way to answer questions like:
- which element contains which other element
- what styles may inherit downwards
- how events should travel during capture and bubble phases
- what needs to be recomputed when a subtree changes
The tree shape is not arbitrary. It is the data model that makes layout, events, and selectors possible.
### DOM Nodes Are Live Objects
DOM nodes are not snapshots. They are live objects managed by the browser. When you do this:
```js
const button = document.querySelector("button");
button.textContent = "Loading...";
```
you are mutating browser-managed document state. That may later trigger style recalculation, layout changes, paint work, accessibility tree updates, and more.
This is why DOM mutations are more expensive than changing a local JavaScript variable.
## The CSSOM: Style Information as a Structured Graph
The browser does not only need document structure. It also needs style information in a machine-friendly form.
The CSSOM, or CSS Object Model, is the structured representation of CSS rules and resolved style relationships.
At a high level:
- HTML becomes the DOM
- CSS becomes style rule structures, often referred to as the CSSOM in learning material
- the browser combines relevant document and style information to determine how elements should look
### Why the Browser Needs Both DOM and Style Data
The browser cannot draw from HTML alone because HTML says what exists, not exactly how it should appear. CSS cannot render on its own either because selectors need actual elements to match against.
Rendering requires both structure and style.
```mermaid
flowchart LR
A[HTML bytes] --> B[DOM tree]
C[CSS bytes] --> D[Style rules and CSSOM]
B --> E[Style calculation]
D --> E
E --> F[Render tree]
```
### The Render Tree
The render tree is a browser-internal structure derived from document structure and style information. It focuses on what actually needs to be rendered.
Important nuance:
- not every DOM node necessarily appears as a visible render object
- hidden elements may not participate the same way
- pseudo-elements and generated content complicate the picture
For interview purposes, it is enough to know that the render tree is closer to "what needs drawing" than the raw DOM is.
## Rendering Pipeline: From Document State to Pixels
The rendering pipeline is the sequence of work the browser performs to update what the user sees.
At a high level, a useful model is:
1. parse HTML into DOM
2. parse CSS into style structures
3. calculate styles
4. compute layout
5. paint pixels
6. composite layers to the screen
```mermaid
flowchart LR
A[DOM change or initial load] --> B[Style recalculation]
B --> C[Layout]
C --> D[Paint]
D --> E[Composite]
E --> F[Screen update]
```
### Style Recalculation
The browser determines which CSS rules apply to which elements and resolves computed styles.
This can become expensive if:
- the DOM is very large
- selectors are complex
- many elements are affected by a class or style change
### Layout
Layout determines geometry:
- element sizes
- positions
- line wrapping
- overflow effects
This stage answers questions like, "Where is the button?" and "How wide is the card after styles and available space are applied?"
### Paint
Paint turns layout and visual styles into draw commands: backgrounds, text, shadows, borders, images, and so on.
### Composite
Compositing combines painted layers into the final frame sent to the display.
This is one reason some CSS changes are much cheaper than others. Changes that can be handled at the compositor level may avoid full layout and paint work.
## Reflow vs Repaint
These are common interview terms, though different browser documentation may use language like "layout" and "paint" more directly.
### Reflow
Reflow usually refers to layout recalculation when geometry may have changed.
Examples:
- changing width or height
- adding or removing DOM nodes
- changing text content in a way that affects size
- reading and writing layout-sensitive properties repeatedly
Reflow is often expensive because geometry changes can ripple through other elements.
### Repaint
Repaint refers to visual updates where geometry stays the same but appearance changes.
Examples:
- changing background color
- changing text color
- changing visibility in some cases
Repaint is often cheaper than reflow, but still not free.
### Why This Matters in Practice
If you change a property that affects layout on a large page, you may trigger broad recomputation. If you can express the same visual effect using transform or opacity, the browser may keep the work closer to compositing, which is usually cheaper.
This is why frontend performance advice often sounds very specific. It is not superstition. It is rooted in the rendering pipeline's cost model.
## The Event Loop: How JavaScript Work Gets Scheduled
JavaScript on the main thread runs synchronously on a call stack, but browsers also need to handle timers, network responses, user input, and Promise callbacks. The event loop is the coordination mechanism.
### Core Mental Model
Think of the browser main thread as a single cashier handling one customer at a time.
- the call stack is the cashier's current customer
- tasks are customers waiting in the main line
- microtasks are high-priority notes that must be cleared before the cashier takes the next main-line customer
- rendering happens in between suitable opportunities, not in the middle of arbitrary JavaScript execution
### Simplified Event Loop Cycle
```mermaid
flowchart TD
A[Run one task] --> B[Execute synchronous JS on call stack]
B --> C{Call stack empty?}
C -- No --> B
C -- Yes --> D[Drain all microtasks]
D --> E[Browser may render]
E --> F[Take next task]
F --> A
```
### Tasks vs Microtasks
Useful everyday examples:
| Source | Category |
| --- | --- |
| `setTimeout` | task |
| DOM events like `click` | task |
| message events | task |
| `Promise.then` / `catch` / `finally` | microtask |
| `queueMicrotask` | microtask |
| mutation observer delivery | microtask-like scheduling behavior |
Important rule: after a task finishes and the call stack becomes empty, the browser drains the microtask queue before moving on to the next task.
### Why Microtasks Exist
Microtasks are a way to schedule follow-up work that should happen soon after the current JavaScript completes, before the browser continues normal task progression.
This is very useful for:
- Promise chaining semantics
- batching follow-up logic
- preserving invariants after synchronous code completes
The tradeoff is that excessive microtasks can starve rendering and delay other tasks.
## Promises vs `setTimeout`: Why the Order Surprises People
Consider this code:
```js
console.log("start");
setTimeout(() => {
console.log("timeout");
}, 0);
Promise.resolve().then(() => {
console.log("promise");
});
console.log("end");
```
Output:
```js
start
end
promise
timeout
```
### What Actually Happened
```mermaid
sequenceDiagram
participant JS as Call Stack
participant Micro as Microtask Queue
participant Task as Task Queue
participant Browser as Browser Loop
JS->>JS: log start
JS->>Task: schedule setTimeout callback
JS->>Micro: schedule Promise reaction
JS->>JS: log end
JS-->>Browser: stack empty
Browser->>Micro: drain microtasks
Micro->>JS: run promise callback
Browser->>Task: take next task
Task->>JS: run timeout callback
```
The key is that `setTimeout(..., 0)` means "put this in the task queue after at least the timeout threshold," not "run this before microtasks."
### Common Interview Mistake
People say, "Promises are faster than timers." That is not the right explanation.
The real explanation is scheduling category:
- Promise reactions are microtasks
- timer callbacks are tasks
- microtasks run before the browser takes the next task
That is a much stronger answer.
## Rendering Does Not Happen After Every Statement
Beginners often imagine that every DOM change instantly appears on the screen. That would be far too expensive.
Instead, browsers batch work. If your JavaScript makes several DOM mutations in one turn of the event loop, the browser usually waits for a rendering opportunity rather than repainting after every line.
This is one reason framework batching works well. It aligns with how browsers already prefer to operate.
## `requestAnimationFrame`: Coordinating With Rendering
`requestAnimationFrame` asks the browser to run a callback before the next repaint.
That makes it a better place than `setTimeout` for visual animation work because it aligns your code with the browser's frame schedule.
### Why It Exists
Animations are about frame production, not merely time delays. `setTimeout` can schedule callbacks, but it does not naturally align with paint timing. `requestAnimationFrame` does.
If the display is refreshing at around 60 frames per second, the browser has only about 16.7 ms per frame budget for scripting, style, layout, paint, and compositing. If your main-thread work overruns that budget, the user sees dropped frames or stutter.
## Event Propagation in the DOM
The DOM tree is also the path along which many events travel.
For a typical event, the browser can move through:
1. capture phase, from outer ancestors down toward the target
2. target phase, at the element itself
3. bubble phase, from the target back upward
```mermaid
flowchart TD
A[window/document] --> B[body]
B --> C[div container]
C --> D[button target]
D --> E[bubble back to container]
E --> F[bubble back to body]
```
This propagation model is one reason event delegation is so effective. Instead of attaching listeners to every item in a large list, you can often attach one listener to a parent and inspect the event target.
That reduces listener count and handles dynamically added children naturally.
## Forced Synchronous Layout: A Hidden Performance Trap
One of the easiest ways to create jank is to mix DOM writes and layout reads in the wrong order.
Example pattern:
```js
box.style.width = "200px";
const height = box.offsetHeight;
```
After the style write, the browser may need updated layout information before it can answer `offsetHeight`. So your read can force the browser to flush pending style and layout work early.
If this happens repeatedly inside loops, you get layout thrashing.
### Better Strategy
- batch reads together
- batch writes together
- use `requestAnimationFrame` for visual updates when appropriate
- avoid unnecessary layout-sensitive property access
## Rendering Optimization Concepts That Matter in Real Apps
### Keep DOM Size Reasonable
Larger DOM trees increase the cost of style recalculation, layout, and some forms of event handling. Infinite feeds, tables, and dashboards often need virtualization for this reason.
### Prefer Transform and Opacity for Animation
These often avoid layout changes and may stay closer to compositing work, which is usually more efficient.
### Avoid Long Main-Thread Tasks
If JavaScript monopolizes the main thread, input responsiveness and rendering suffer. Break up heavy work, defer non-urgent work, or move computation to workers when appropriate.
### Use Passive Listeners When Appropriate
For scroll and touch-sensitive interactions, passive listeners tell the browser your handler will not call `preventDefault()`, which can improve scrolling performance by reducing uncertainty.
### Use Event Delegation for Dynamic Interfaces
This reduces listener churn and exploits the DOM's event propagation model efficiently.
### Let the Browser Help You
Use the right browser primitives instead of manual polling:
- `requestAnimationFrame` for animation
- `IntersectionObserver` for visibility-based loading
- `ResizeObserver` for size observation
- `AbortController` for cancelable async work
These APIs exist because the browser can provide better scheduling and lower overhead than ad hoc userland loops.
## How Frameworks Relate to This Chapter
Frameworks like React, Vue, and Svelte do not replace the DOM, event loop, or rendering pipeline. They manage how your application decides to update them.
For example:
- framework state changes eventually become DOM updates or platform view updates
- Promise scheduling still uses browser microtasks
- network requests still return through browser-controlled async flow
- layout costs still depend on real DOM and CSS behavior
This is why performance debugging often drops below the framework layer. A component abstraction may be elegant, but if it triggers repeated layout invalidations or long scripting tasks, the browser still pays the bill.
## Interview-Ready Summary
- The DOM is a live tree of browser-managed document nodes that JavaScript can query and mutate.
- CSS is represented in structured form and combined with the DOM to compute renderable output.
- The rendering pipeline usually involves style calculation, layout, paint, and compositing.
- Reflow or layout work is expensive because geometry changes can affect other elements.
- Repaint updates appearance without necessarily changing geometry, but it still costs work.
- The event loop coordinates tasks, microtasks, and rendering opportunities on the main thread.
- Promise callbacks run as microtasks, so they run before the next task such as a timer callback.
- `requestAnimationFrame` aligns animation work with the browser's repaint schedule.
## What to Read Next
Continue with [04-networking-storage-security.md](./04-networking-storage-security.md). That chapter explains how the browser makes network requests, stores data, and enforces origin-based security boundaries around your app.