Fundamentals · The platform
The DOM
⚡ TL;DR — The DOM is a live, in-memory object tree the browser builds from your HTML, and the API through which JavaScript reads and mutates the page. It is not your HTML, it is not what you see on screen, and the gap between those three things is where most DOM interview questions live.
🧠 Mental model
Hold three distinct things in your head — interviewers probe the seams between them:
| What it is | Gotcha | |
|---|---|---|
| HTML | The source text you shipped | Immutable; the DOM can diverge from it instantly |
| DOM | A live object tree parsed from that text | Includes nodes the parser invented (<tbody>) and fixed |
| Render tree | Only the nodes that get painted | Excludes display:none; excludes <head> |
The single most important word is live. The DOM is not a snapshot. Some of its APIs return collections that keep updating themselves under you — which is a genuine source of infinite loops.
⚙️ How it actually works
The parser turns bytes → tokens → nodes → tree. Crucially it is error-tolerant and will rewrite your markup: it auto-closes tags, and it will insert a <tbody> you never wrote. So the DOM you query is frequently not the HTML you authored.
The tree is made of Nodes, and Element is only one kind. This distinction is the source of endless off-by-one bugs:
Node ──┬── Element (<div>)
├── Text ("hello", and ALSO the whitespace between your tags)
├── Comment (<!-- -->)
└── Document
Whitespace between tags is a real Text node. That is why:
el.childNodes // NodeList — includes text + comment nodes. Usually not what you want.
el.children // HTMLCollection — elements only. Usually what you DO want.
el.firstChild // often a whitespace Text node 😖
el.firstElementChild // the element you actually meant ✅
Live vs static collections — the classic trap:
document.getElementsByTagName('div'); // HTMLCollection — LIVE, re-queries the document
document.querySelectorAll('div'); // NodeList — STATIC snapshot
💻 Code
The infinite loop that this causes, and why:
// ❌ Infinite loop. `divs` is LIVE — every appended div grows the collection.
const divs = document.getElementsByTagName('div');
for (let i = 0; i < divs.length; i++) {
document.body.appendChild(document.createElement('div'));
}
// ✅ Static snapshot: length is fixed at query time.
const divs = document.querySelectorAll('div');
Reading the three "content" properties — they are not interchangeable:
el.innerHTML // parses HTML. XSS SINK. Never feed it untrusted input.
el.textContent // raw text of ALL nodes, incl. <script>/hidden. Fast, no reflow.
el.innerText // "as rendered" text — respects display:none, text-transform.
// ⚠️ Reading it FORCES A REFLOW because it depends on layout.
That last line is a real performance answer: textContent is cheap, innerText is not.
⚖️ Trade-offs
innerHTMLis fast to write and dangerous to use. It re-parses a string into nodes — convenient, destroys existing node identity (listeners on replaced children die), and is the canonical XSS vector. PrefertextContentfor text,createElementfor structure.- Live collections are cheap to obtain, expensive to iterate. They re-query on access. In a hot loop, convert once:
[...el.children]. - The DOM is a slow API, not a slow data structure. Individual property reads are fast. What is slow is interleaving reads and writes, because reads force the browser to flush pending layout.
💣 Gotchas interviewers probe
- "Is the DOM the same as your HTML?" No — the parser corrects and invents nodes (
<tbody>), and JS mutates it afterwards. View-source ≠ DevTools Elements panel. NodeListis not an Array. It hasforEachbut nomap/filter.HTMLCollectionhas neither. Spread it:[...nodes].childNodesvschildren— the whitespace-Text-node trap. If someone'sfirstChild"randomly" returns text, this is why.innerTextforces reflow;textContentdoes not. Knowing why (innerText is layout-dependent) is the senior signal.document.writeafter load wipes the document. It is not just deprecated, it is destructive — and it is why sync scripts must block the parser.- The DOM is a language-agnostic spec (WebIDL), not a JavaScript feature. It is host-provided, which is why it doesn't exist in Node.
🎯 Say this in the interview
"The DOM is a live object tree the browser builds from the HTML, and the API JS uses to read and mutate the page. Three things I keep separate: the HTML I shipped, the DOM — which can already differ, because the parser corrects markup and injects nodes like
tbody— and the render tree, which is only the painted subset. The word that matters is live:getElementsByTagNamereturns a collection that keeps re-querying, so appending inside a loop over it never terminates, whereasquerySelectorAllis a static snapshot. And I'm careful withinnerTextversustextContent—innerTextis 'as rendered', so reading it forces a layout flush, whiletextContentis just the raw text and is cheap."
🔗 Go deeper
- javascript.info — Document — the clearest DOM walkthrough anywhere.
- MDN — DOM introduction — the spec-accurate view.
- MDN — Node vs Element — the type hierarchy the whitespace bug comes from.