skip to content

You are building 500 list items in plain JavaScript. What does a DocumentFragment give you over appending each item to the live list, and what state is the fragment in after you insert it?

level: middleimportance: must knowfreq 64%

answer

  1. parentless container, never rendered
  2. children go in, the fragment does not
  3. one structural change to the live tree
  4. empty afterwards, so not reusable as-is
  5. clone it if you need it twice

basics

~20 s

A DocumentFragment is a parentless, never-rendered container you fill offscreen, so the live document is touched once instead of 500 times. Inserting the fragment inserts its children, not the fragment itself, which leaves the fragment empty afterwards.

solid answer

~50 s

`document.createDocumentFragment()` gives you a lightweight container that is not part of any document and is never rendered. You append all 500 items into it, then insert the fragment once, so the live tree is modified a single time rather than 500 times. The key behaviour is that a fragment is transparent on insertion: the insertion algorithm moves the fragment's *children* into the target and the fragment node itself never appears in the DOM — so afterwards `frag.childNodes.length` is 0 and reusing the same fragment gives you an empty insert. It still implements `ParentNode`, so you can call `querySelector`, `append` or `getElementById` on it while building. Worth being honest in an interview: `parent.append(...arrayOfNodes)` performs the same single insertion, so the fragment earns its keep mainly when you need to hold, query or pass around a detached subtree.

code

javascript · 18 lines
javascript
const list = document.createElement('ul');
const frag = document.createDocumentFragment();

for (let i = 0; i < 3; i++) {
  const li = document.createElement('li');
  li.textContent = `row ${i}`;
  frag.append(li);
}

console.log(frag.querySelectorAll('li').length); // 3 - queryable while detached

list.append(frag);
console.log(list.children.length);   // 3
console.log(frag.childNodes.length); // 0 - children moved out
console.log(list.firstElementChild.tagName); // "LI", never the fragment

list.append(frag);                   // silent no-op: nothing left to insert
console.log(list.children.length);   // 3

go deeper

for a junior

Know that a DocumentFragment is an offscreen holder you fill with nodes and insert once, and that after inserting it the fragment is empty because its children moved into the page.

for a middle

Explain the transparency rule — insertion moves the fragment's children and never the fragment node — and why that combined with the one-parent rule leaves the fragment empty. Note that it supports querySelector but has no innerHTML.

for a senior

Be honest about what the fragment buys: one structural change to the live tree and one MutationObserver record, not a magic performance fix, since variadic append does the same. Argue for it as a way to own and pass around a detached subtree.

for a principal

Take a position on where DOM assembly belongs at all — hand-built fragments are fine at the leaves of a system but a poor substitute for a rendering layer, and you should be able to say when hand-rolled DOM assembly is the right call and when it is technical debt.

## What a DocumentFragment actually is A `DocumentFragment` is a node type whose only job is to hold other nodes temporarily. Create one with `document.createDocumentFragment()` or `new DocumentFragment()`. Two properties define it: - It has **no parent** and belongs to no rendered tree, so nothing inside it is displayed, styled, measured or matched by selectors run on `document`. - It is **transparent to insertion**: when you insert a fragment anywhere, the DOM's insertion algorithm has a special case — it inserts the fragment's children and never the fragment node itself. That second point is the one interviewers probe. ## The build-then-insert pattern ```js const frag = document.createDocumentFragment(); for (const item of items) { const li = document.createElement('li'); li.textContent = item.label; frag.append(li); } list.append(frag); // one insertion into the live tree console.log(frag.childNodes.length); // 0 ``` Every `frag.append(li)` mutates a detached object that no rendering machinery is watching. Only the final `list.append(frag)` touches the document, and it moves all 500 children across in one operation. ## Why the fragment ends up empty The rule is the same one that makes ordinary insertion a move rather than a copy: a node has at most one parent, so moving the children into `list` removes them from `frag`. Combined with the fragment's transparency, this means: - `frag.parentNode` is always `null`, before and after. - `list.lastElementChild` is the last `<li>`, not the fragment. - The fragment is reusable but **empty** — a second `list.append(frag)` inserts nothing, silently. Code that builds a fragment once and appends it in two places is a real bug: the second target gets nothing. If you need the same content twice, clone before inserting: `list.append(frag.cloneNode(true))` inserts a copy and leaves the original fragment populated. ## What you can do with a fragment while building A fragment implements `ParentNode`, so `append`, `prepend`, `replaceChildren`, `children`, `firstElementChild`, `querySelector` and `querySelectorAll` all work on it. It also implements `NonElementParentNode`, so `frag.getElementById('x')` works. What it is *not* is an `Element`: it has no tag name, no attributes, and no `innerHTML` property, so you cannot build one by assigning markup to it. Being able to query a detached subtree is genuinely useful — you can wire up structure, look up nodes inside it, and attach listeners before anything is visible to the user. ## Where the batching argument stands today The folklore claim is that appending 500 nodes one at a time causes 500 layout passes. That was never quite how browsers behave, and modern engines defer and coalesce work aggressively. What is unambiguously true is the DOM-level fact: with a fragment, the live tree is structurally modified once, and mutation observers and other machinery watching the document see a single childList record instead of 500. It is also worth knowing the alternative. Because `append` is variadic, this achieves the same single insertion without a fragment: ```js const nodes = items.map(makeRow); list.append(...nodes); ``` So the honest answer to "why a fragment" is not only speed. A fragment is the right tool when you want to **hold** a detached subtree — build it in one function, return it, query it, pass it between modules, insert it later — or when you are draining nodes out of the document and want somewhere to park them. ## Other places fragments appear Fragments are not just something you construct by hand; several APIs hand you one. Understanding the transparency rule is what makes those APIs behave predictably: whatever hands you a fragment, inserting it deposits its children and leaves the fragment hollow. ## The mental model Think of a fragment as a shopping bag rather than a box. You carry the items in the bag, but when you insert the bag into the tree the *items* go in and the bag stays behind — empty, still yours, still reusable if you refill it.

  • Someone builds a fragment once and appends it to two different containers. Why does the second container stay empty?
    Because insertion moves the fragment's children rather than copying them, and the fragment is transparent — so the first append emptied it. The second append inserts a fragment with zero children, which is a silent no-op. Insert `frag.cloneNode(true)` for the second target, or build a second fragment.
  • Can you build a DocumentFragment by assigning markup to it?
    Not directly — `innerHTML` is defined on `Element` and `ShadowRoot`, and a fragment is neither, so it has no such property. You build one by creating nodes and appending them, or you obtain a fragment from an API that parses markup for you. That is a deliberate consequence of a fragment having no tag and no attributes.
  • Does `parent.append(...nodes)` make DocumentFragment unnecessary?
    For the narrow case of inserting a ready array of nodes, yes — it is a single insertion too. The fragment still wins when you want a detached subtree as a value: something a builder function can return, that you can `querySelector` into before it is live, or that you can park removed nodes in. Choose on ergonomics, not on a performance myth.
  • What does a MutationObserver on the list report when you insert a 500-child fragment?
    One childList record for the list, whose `addedNodes` holds all 500 children — not 500 separate records. That is a direct consequence of the fragment being inserted in one operation, and it is one of the concrete, observable differences from appending in a loop.

saying these in an interview costs you the question

  • Expecting the fragment node itself to appear in the DOM
  • Reusing the same fragment for a second insertion
  • Thinking a fragment supports innerHTML like an element
  • Claiming a fragment is required to avoid 500 reflows
  • Believing content inside a fragment is rendered or styled

context