
DOM: The Document Object Model
HTML is just text. Browser converts it into a Tree Structure (DOM) to manipulate it with JS.

HTML is just text. Browser converts it into a Tree Structure (DOM) to manipulate it with JS.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

First time web dev, tried console.log(document):
console.log(document);
// Output:
#document
html
head
body
div
"Wait, my HTML shows as a tree?"
HTML file looked like:
<html><head>...</head><body>...</body></html>
Browser showed it like family tree. Why? I wrote angle brackets and tags (just text), but the console showed a hierarchy like an org chart in Excel.
"What happened?"Tried modifying HTML with JavaScript:
document.getElementById("box").style.color = "red";
Worked, but didn't know the principle. Just copy-pasted from Stack Overflow and used it.
What's document? How does getElementById find HTML? Does it grep through the HTML file? Or is the HTML loaded entirely into memory somewhere?
Senior: "That's DOM API. Converts HTML to object for manipulation."
Me: "Object? HTML becomes object? HTML is a .html file, how can it be an object?"
Senior smiled. "Browser reads HTML file and builds tree structure in memory. That's the DOM."
Still didn't get it. "But I wrote <div>Hello</div> in HTML. How does that become an object? Is the object stored in the file?"
Most importantly: "Can't I just edit HTML?" Why not open the HTML file in a text editor and change it? Why manipulate DOM with JavaScript?
At first, I thought the HTML file itself changed in real-time. I thought document.getElementById("box").textContent = "World" would automatically modify the .html file on the server. Obviously that wasn't true, and after some trial and error, I realized: "Oh, HTML and DOM are different things."
Senior's analogy:
"Oh, HTML is initial design, DOM is real-time state!""HTML is blueprint (diagram).
<div id="box">Hello</div>Just text. Like paper. Or more precisely, like a CAD file. Like a
.dwgfile drawn by an architect. You can't live in it.Browser reads this blueprint and builds building (DOM tree) in memory.
{ tagName: 'div', id: 'box', textContent: 'Hello', style: { color: 'black' }, children: [] }JavaScript remodels the building. Blueprint (HTML file) unchanged. But current building (DOM) changes. Painting walls, moving furniture, adding rooms."
This analogy clicked immediately. HTML file is just a text file with .html extension, browser reads it and builds a tree in memory. JavaScript manipulates that tree.
So document.getElementById doesn't open a file, it searches the tree in memory. style.color = "red" doesn't modify the HTML file, it changes an object property in memory.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
<div id="app">Hello</div>
</body>
</html>
.html)HTML is serialized text. String of tags and angle brackets. UTF-8 encoded byte stream. What gets transmitted over network, saved to disk, managed in Git—this text file.
I didn't understand this at first. "Doesn't HTML show on screen?" But the HTML file itself doesn't appear on screen. Browser must read this file and build something for it to appear.
Browser reads HTML, creates in-memory:
#document
└─ html
├─ head
│ └─ title ("Test")
└─ body
└─ div#app ("Hello")
DOM is parsed tree structure. Browser reads HTML text character by character, splits into tokens, creates nodes from tokens, assembles nodes into tree. This process is parsing.
My thought when I first understood this: "Oh, like a compiler." Just as a C compiler reads source code and builds an AST (Abstract Syntax Tree), browser reads HTML and builds a DOM Tree.
So DOM only exists in memory. Not saved to file. Disappears when you close browser. Refresh rebuilds DOM by re-reading HTML file.
<html>
<head>
<title>Example</title>
</head>
<body>
<div id="container">
<h1>Title</h1>
<p>Content</p>
</div>
</body>
</html>
DOM Tree:
document (Document node)
└─ html (Element node)
├─ head
│ └─ title
│ └─ "Example" (Text node)
└─ body
└─ div#container
├─ h1
│ └─ "Title"
└─ p
└─ "Content"
My thought when I first saw this tree: "Wait, this is the same Tree from data structures class?"
Correct. DOM is a tree data structure. Has root node (document), each node has parent and children, sibling relationships too. That's why DOM API has methods like parentNode, childNodes, nextSibling.
document)div, p, span)id="app", class="box")<!-- comment -->)I found it interesting that Text nodes exist separately. Looking at <p>Hello</p>, the p tag is an Element node, and Hello is a child Text node. So changing p.textContent actually changes the Text node's value.
I didn't know about Comment nodes at first either. "Aren't comments only in code?" But comments also appear as nodes in DOM tree. So iterating with childNodes includes comments too.
// By ID
const box = document.getElementById("box");
// CSS selector
const boxes = document.querySelectorAll(".box");
// Tag name
const divs = document.getElementsByTagName("div");
// Class name
const buttons = document.getElementsByClassName("btn");
// First match
const firstBox = document.querySelector(".box");
I didn't understand the difference between getElementById and querySelector at first. "They both find elements, why two?" Turns out there's a performance difference.
getElementById uses an internal ID hashmap in the browser, finding in O(1). Meanwhile querySelector parses CSS selector and traverses DOM tree, relatively slower. Of course both are fast enough in modern browsers, but if you can find by ID, getElementById is slightly more efficient.
const box = document.getElementById("box");
// Change text
box.textContent = "New Text";
// Change HTML
box.innerHTML = "<span>Bold</span>";
// Change attribute
box.setAttribute("data-id", "123");
// Add/remove class
box.classList.add("active");
box.classList.remove("hidden");
box.classList.toggle("visible");
The difference between textContent and innerHTML confused me at first. Both change content, what's different?
Turns out textContent handles pure text only, innerHTML does HTML parsing. So putting <script> in innerHTML can enable XSS attacks. (Though modern browsers don't execute scripts inserted via innerHTML.)
box.style.color = "red";
box.style.fontSize = "20px";
box.style.backgroundColor = "#f0f0f0";
// Must use camelCase
// CSS: background-color
// JS: backgroundColor
CSS property names changing to camelCase in JavaScript felt inconvenient at first. Having to write background-color as backgroundColor. Why can't I just use string like style["background-color"]? Turns out that works too:
box.style["background-color"] = "red";
But most code uses camelCase. Because box.style.backgroundColor is easier to type and IDE autocomplete works better.
// Create
const newDiv = document.createElement("div");
newDiv.textContent = "I'm new!";
newDiv.className = "box";
// Append
document.body.appendChild(newDiv);
// Insert at specific position
const parent = document.getElementById("container");
const firstChild = parent.firstChild;
parent.insertBefore(newDiv, firstChild);
// Remove
box.remove();
// Or
parent.removeChild(box);
The difference between appendChild and insertBefore confused me at first. appendChild adds to the end, insertBefore adds before specific node.
But there's no insertAfter. Because you can use nextSibling:
// insertAfter implementation
function insertAfter(newNode, referenceNode) {
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}
When I first saw this pattern, I thought "clever." Inserting before nextSibling is effectively inserting after.
Code I wrote as a junior:
// ❌ Very Bad Example
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = i;
document.body.appendChild(div); // Touches Real DOM 1000 times!
}
Result: Browser stuttered. Chrome DevTools Performance tab showed Rendering taking 80% of time.
Senior: "DOM manipulation is expensive. Do it 1000 times, of course it's slow."
Me: "Why? Just adding objects to memory?"
Senior: "DOM manipulation makes browser redraw screen. Calculate layout, paint pixels. That's expensive."
Every time appendChild runs, browser does:
1000 loops = 1000 Reflows & 1000 Repaints!
I initially thought "why is adding objects slow?" Turns out DOM manipulation isn't just memory manipulation. Browser re-runs rendering pipeline every time DOM changes.
Rendering Pipeline:
Every appendChild re-executes steps 4-6. Repeat 1000 times, naturally slow.
// ✅ Good Example
const fragment = document.createDocumentFragment(); // Virtual container
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = i;
fragment.appendChild(div); // Memory only. No Reflow.
}
document.body.appendChild(fragment); // Touches Real DOM ONCE!
Result: Blazing fast. Performance tab now showed Rendering under 5%.
DocumentFragment is a virtual container existing only in memory. Not attached to DOM tree, so manipulating it doesn't trigger Reflow. And when you appendChild(fragment), the Fragment itself disappears and only child nodes get added to DOM.
After learning this pattern, I realized: "Oh, that's why React is needed." React uses Virtual DOM to batch changes and apply to Real DOM all at once. Like automatic Fragment usage.
When DOM structure or size changes:
// Triggers Reflow
element.style.width = "100px";
element.style.height = "200px";
element.style.display = "none";
element.style.padding = "10px";
element.classList.add("big");
// Even reading triggers Reflow
const height = element.offsetHeight; // Forced Synchronous Layout!
const width = element.getBoundingClientRect().width;
Cost: Very expensive (recalculate entire layout)
When Reflow occurs, browser recalculates layout tree. Not just the element, but potentially parent, children, sibling positions and sizes too.
For example, changing parent's width affects children's width. Especially with flex or grid layout, one element change can affect entire layout.
I found it surprising that reading offsetHeight triggers Reflow. "It's just reading, why expensive?" Turns out browser uses Lazy Evaluation. Multiple DOM manipulations don't immediately calculate layout, deferred until actually drawing to screen.
But reading offsetHeight requires layout calculation right now. So browser immediately executes Reflow. This is called Forced Synchronous Layout.
When only color or background changes:
// Only Repaint (no Reflow)
element.style.color = "red";
element.style.backgroundColor = "#fff";
element.style.visibility = "hidden"; // differs from display: none
element.style.outline = "1px solid blue";
Cost: Cheaper than Reflow but still expensive
Repaint skips layout calculation and only re-executes Paint stage. Re-painting pixels.
The difference between visibility: hidden and display: none comes from this:
display: none: Element removed from layout → Triggers Reflowvisibility: hidden: Element occupies space but invisible → Only RepaintSo if performance matters when hiding elements, visibility: hidden is slightly more efficient. Though only useful when maintaining layout.
DOM alone can't render screen. No style information. Browser also parses CSS files to build CSSOM (CSS Object Model).
/* style.css */
body {
font-size: 16px;
}
.box {
width: 100px;
height: 100px;
background-color: red;
}
CSSOM Tree:
body
font-size: 16px
.box
width: 100px
height: 100px
background-color: red
Then combines DOM and CSSOM to build Render Tree. Render Tree only includes elements that will actually be drawn on screen. For example, elements with display: none aren't included in Render Tree.
I initially thought "style information is all in DOM," but turns out DOM and CSSOM are separated. So style.color = "red" in JavaScript doesn't change CSSOM, it changes element's inline style.
// ❌ Bad (3 Reflows)
element.style.width = "100px";
element.style.height = "100px";
element.style.margin = "10px";
// ✅ Good (1 Reflow)
element.style.cssText = "width: 100px; height: 100px; margin: 10px;";
// Or
element.className = "box-large"; // Define all in CSS
// ✅ Better (separate reads/writes)
const height = element1.offsetHeight; // read
const width = element2.offsetWidth; // read
element1.style.height = height + 10 + "px"; // write
element2.style.width = width + 10 + "px"; // write
Mixing reads and writes triggers Forced Synchronous Layout every time:
// ❌ Worst case (2 Forced Reflows)
element1.style.width = "100px"; // write
const height = element1.offsetHeight; // read → Forced Reflow!
element2.style.width = "200px"; // write
const width = element2.offsetWidth; // read → Forced Reflow!
Browser tries to batch "write" operations for later processing, but "read" in between forces immediate calculation.
So reads first, writes later is better for performance.
When I learned React, I wondered:
"How does
setStaterender so fast?"
// Update 1000 items
items.forEach(item => {
const element = document.getElementById(item.id);
element.textContent = item.name; // 1000 DOM manipulations!
});
This code triggers 1000 Repaints. Even Reflows if element size changes.
I initially thought "1000 times is like 0.1 seconds, that's fine?" but actually much slower. Each DOM manipulation runs browser's rendering pipeline.
What React does:
Virtual DOM (Fake DOM) creation
// JavaScript object in memory
{
type: 'div',
props: { id: 'app' },
children: ['Hello']
}
setState({ text: 'World' })
→ Updates Virtual DOM only (doesn't touch Real DOM)
Old Virtual DOM: { children: ['Hello'] }
New Virtual DOM: { children: ['World'] }
→ Difference: only text changed
// Only 1 Real DOM modification
element.textContent = 'World';
Result: Minimize DOM manipulation count → Fast!
Virtual DOM is a Pure JavaScript Object. Doesn't use DOM API at all. So manipulation is extremely fast. Changing object properties takes nanoseconds.
React maintains two Virtual DOMs:
Calling setState updates Work-in-Progress Tree. Then Reconciliation algorithm compares both trees to find differences.
Found differences get applied to Real DOM. This is called Commit Phase.
I initially thought "Virtual DOM isn't always faster, right?" Virtual DOM also compares JavaScript objects, so with large trees it could be slow.
Correct. Virtual DOM isn't always faster than Real DOM. But usually faster because:
const button = document.getElementById("btn");
button.addEventListener("click", () => {
console.log("Clicked!");
});
// Remove event
button.removeEventListener("click", handleClick);
When I first learned event listeners, I thought: "How's this different from HTML's onclick attribute?"
<!-- In HTML -->
<button onclick="handleClick()">Click</button>
<!-- In JavaScript -->
<button id="btn">Click</button>
<script>
document.getElementById("btn").addEventListener("click", handleClick);
</script>
Turns out addEventListener is much better:
onclick allows one, addEventListener allows multipleremoveEventListenercapture, once, passiveEspecially the passive option was interesting when I learned about it:
element.addEventListener("touchstart", handleTouch, { passive: true });
Setting passive: true tells browser "this event handler won't use preventDefault()." Then browser can immediately start scrolling, improving performance.
<div id="outer">
<div id="inner">
<button id="btn">Click</button>
</div>
</div>
Event propagation when button clicked:
button (click!) → inner → outer → document
When I first learned event bubbling, I thought "why is this needed?" Can't events just fire on the clicked element?
Turns out bubbling enables event delegation. And convenient for common handling across multiple elements.
// Check bubbling
document.getElementById("outer").addEventListener("click", () => {
console.log("Outer clicked");
});
document.getElementById("inner").addEventListener("click", () => {
console.log("Inner clicked");
});
document.getElementById("btn").addEventListener("click", () => {
console.log("Button clicked");
});
// Button click output:
// Button clicked
// Inner clicked
// Outer clicked
To stop bubbling, use stopPropagation():
document.getElementById("btn").addEventListener("click", (e) => {
e.stopPropagation();
console.log("Button clicked");
});
// Now only "Button clicked" outputs
My mistake code:
// ❌ Bad (separate listener for each of 1000 buttons)
const buttons = document.querySelectorAll(".btn");
buttons.forEach(btn => {
btn.addEventListener("click", handleClick); // 1000 listeners!
});
Problems:
Improvement:
// ✅ Good (only 1!)
document.body.addEventListener("click", (e) => {
if (e.target.classList.contains("btn")) {
handleClick(e);
}
});
Possible thanks to bubbling! Button click bubbles up to body, so catching once at body is enough.
When I first learned this pattern, I thought "this is really clever." A pattern from jQuery days, still valid today.
Especially useful when dynamically adding elements:
// Using event delegation
document.body.addEventListener("click", (e) => {
if (e.target.classList.contains("delete-btn")) {
e.target.parentElement.remove();
}
});
// Later added buttons automatically work
const newBtn = document.createElement("button");
newBtn.className = "delete-btn";
document.body.appendChild(newBtn);
Without event delegation, would need to re-register event listeners every appendChild.
Events actually propagate in two phases:
document → outer → inner → buttonbutton → inner → outer → documentBy default addEventListener operates in Bubble Phase. To operate in Capture Phase:
element.addEventListener("click", handleClick, { capture: true });
// Or
element.addEventListener("click", handleClick, true);
I initially thought "when do I use Capture?" but rarely used in practice. Only useful in special cases. For example, when parent needs to catch and block child events first:
// Block all clicks
document.body.addEventListener("click", (e) => {
e.stopPropagation();
console.log("No clicks allowed!");
}, { capture: true });
Shadow DOM is core technology of Web Components. Creates hidden DOM tree inside DOM tree.
const host = document.getElementById("host");
const shadowRoot = host.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>
p { color: red; }
</style>
<p>Text inside Shadow DOM</p>
`;
Styles inside Shadow DOM don't leak out. And outside styles don't come in. Perfect encapsulation.
I initially thought "how's this different from iframe?" but completely different:
Shadow DOM is also used in browser built-in elements. For example, <video> tag's play button is implemented with Shadow DOM. Can see it in DevTools with "Show user agent shadow DOM" option enabled.
<video controls>
#shadow-root
<div class="controls">
<button class="play"></button>
<button class="pause"></button>
</div>
</video>
Rarely need to use Shadow DOM directly in practice, but useful when building libraries. No worries about style conflicts.
| Concept | Explanation |
|---|---|
| HTML | Text file (blueprint) |
| DOM | Object tree in memory (real-time state) |
| CSSOM | Object tree parsed from CSS |
| Render Tree | Combined DOM + CSSOM |
| Manipulation | JavaScript modifies with DOM API |
| Reflow | Layout recalculation (structure/size change) |
| Repaint | Redraw pixels (color change) |
| Optimization | Fragment, Virtual DOM, Event Delegation |
| Virtual DOM | JS object tree used by React etc. |
| Shadow DOM | Encapsulated DOM subtree |
Before understanding DOM:
After understanding DOM:
Now I understand why:
document.getElementById is fast (hashmap lookup)innerHTML += in a loop is death (re-parse + Reflow every time)And CSSOM is interior design blueprint, Render Tree is completed building, Reflow is structural renovation, Repaint is painting.
This analogy clicked for me.