skip to content

Creating and Moving Nodes

You will learn how to build, insert, move, and remove nodes — and why batching through a DocumentFragment matters. Interviewers ask this as the practical half of 'render a list without a framework'.

on this pageshow

questions

6

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

open as a page

In the browser DOM, you take an element that is already in the page and append it to a different container. Do you end up with one node or two, and what happens to the listeners already attached to it?

level: middleimportance: must knowfreq 58%

basics

~20 s

One node. Inserting a node that already has a parent detaches it from that parent first, so insertion moves rather than copies. The node keeps its identity, so listeners added with addEventListener, dataset values and expando properties all travel with it.

open as a page

In the browser DOM, how does parent.append() differ from parent.appendChild(), and what do the newer methods prepend, before, after, replaceWith and remove let you do that the older Node methods cannot?

level: juniorimportance: should knowfreq 62%

basics

~20 s

append() accepts any number of nodes or strings, turns strings into text nodes, and returns undefined; appendChild() takes exactly one Node and returns it. prepend, before, after, replaceWith and remove share append()'s flexibility and act relative to the element itself, so no parent reference is needed.

open as a page

In the browser DOM, what is the difference between node.cloneNode(true) and node.cloneNode(false), and what state does cloning fail to carry over to the copy?

level: middleimportance: should knowfreq 54%

basics

~20 s

cloneNode(true) copies the node and its whole subtree; cloneNode(false) copies only the node itself with its attributes. Either way the copy is detached and carries no listeners added with addEventListener and no custom JavaScript properties.

open as a page

Given `<div id="box">old</div>`, where do insertAdjacentHTML's four position strings put the new markup, and why would you use it rather than `box.innerHTML += markup`?

level: middleimportance: should knowfreq 45%

basics

~10 s

The four positions are beforebegin (previous sibling), afterbegin (first child), beforeend (last child) and afterend (next sibling). Unlike appending to innerHTML, it parses only the new string and leaves the element's existing nodes untouched.

open as a page

You move a live subtree into a different container by appending it there. The nodes are not re-created, so what state is still lost, and how do you protect what matters?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Anything the browser derives from the node being in a rendered document is discarded: focus, text selection, a scrollable element's offset, running CSS transitions and animations, and an iframe's loaded document, which reloads. Save that state before the move and restore it after.

open as a page