DocumentFragment: оптимизация

DocumentFragment — минимальный DOM-узел (типа 11), существующий только в памяти и не являющийся частью дерева документа; позволяет собрать набор узлов и вставить их за одну операцию, вызвав лишь один reflow.

Зачем нужно

Каждый appendChild к элементу, уже присутствующему в DOM, вызывает reflow — браузер пересчитывает расположение всех элементов. При вставке 100 элементов в цикле это 100 reflow. DocumentFragment — «виртуальная» корзина: сборка происходит в памяти без reflow, а вставка всего фрагмента в DOM вызывает один reflow. Это один из самых эффективных способов оптимизации DOM-операций.

Где используется

  • Рендер длинных списков: <ul> с сотнями <li>
  • Инициализация таблиц с данными: сборка <tr> в DocumentFragment
  • Динамические навигационные меню с множеством пунктов
  • Шаблоны <template>: клонирование контента из тега <template>
  • Виртуальная прокрутка: пакетная замена видимых строк

Основной контент

Базовое использование

const list = document.getElementById('items');
const items = Array.from({ length: 500 }, (_, i) => ({ id: i, name: `Элемент ${i}` }));

// ПЛОХО: 500 reflow
items.forEach(item => {
  const li = document.createElement('li');
  li.textContent = item.name;
  li.dataset.id = item.id;
  list.appendChild(li); // reflow на каждой итерации!
});

// ХОРОШО: 1 reflow
const fragment = document.createDocumentFragment;

items.forEach(item => {
  const li = document.createElement('li');
  li.textContent = item.name;
  li.dataset.id = item.id;
  fragment.appendChild(li); // нет reflow — fragment не в DOM
});

list.appendChild(fragment); // один reflow
// После вставки fragment становится пустым — узлы перешли в DOM

Работа с тегом template

// HTML-шаблон — скрытый, не рендерится браузером
// <template id="card-template">
//   <div class="card">
//     <h2 class="card__title"></h2>
//     <p class="card__body"></p>
//   </div>
// </template>

const template = document.getElementById('card-template');
const container = document.getElementById('cards');

const cards = [
  { title: 'Карточка 1', body: 'Описание первой' },
  { title: 'Карточка 2', body: 'Описание второй' },
  { title: 'Карточка 3', body: 'Описание третьей' },
];

const fragment = document.createDocumentFragment;

cards.forEach(({ title, body }) => {
  // cloneNode(true) — глубокое клонирование содержимого template
  const clone = template.content.cloneNode(true);

  clone.querySelector('.card__title').textContent = title;
  clone.querySelector('.card__body').textContent = body;

  fragment.appendChild(clone);
});

container.appendChild(fragment); // один reflow для всех карточек

Замена содержимого без innerHTML

// innerHTML сбрасывает обработчики событий на дочерних элементах.
// DocumentFragment + replaceChildren — более безопасная альтернатива

function renderList(container, items, renderItem) {
  const fragment = document.createDocumentFragment;
  items.forEach(item => fragment.appendChild(renderItem(item)));

  // replaceChildren — заменяет всё содержимое за один reflow
  container.replaceChildren(fragment);
}

function renderUserItem(user) {
  const li = document.createElement('li');
  li.className = 'user-item';
  li.textContent = user.name;
  li.addEventListener('click', () => selectUser(user.id)); // обработчики сохраняются
  return li;
}

renderList(userList, users, renderUserItem);

Сравнение подходов

// Замер производительности
const COUNT = 1000;
const parent = document.getElementById('target');

// Подход 1: прямой appendChild
console.time('direct');
for (let i = 0; i < COUNT; i++) {
  const div = document.createElement('div');
  div.textContent = i;
  parent.appendChild(div); // 1000 reflow
}
console.timeEnd('direct');

parent.innerHTML = ''; // очистка

// Подход 2: DocumentFragment
console.time('fragment');
const frag = document.createDocumentFragment;
for (let i = 0; i < COUNT; i++) {
  const div = document.createElement('div');
  div.textContent = i;
  frag.appendChild(div); // 0 reflow
}
parent.appendChild(frag); // 1 reflow
console.timeEnd('fragment');

// fragment обычно быстрее в 2–10 раз для 100+ элементов

Частые ошибки

  • Fragment опустошается после вставки: после parent.appendChild(fragment) фрагмент пуст — нельзя вставить его повторно. Для переиспользования создавайте новый или используйте cloneNode.
  • innerHTML вместо fragment: parent.innerHTML = html быстрее в браузерах (нативный парсинг), но сбрасывает все обработчики событий на дочерних элементах.
  • Fragment для одного элемента: создание fragment ради вставки одного узла — лишний overhead, прямой appendChild проще.
  • Манипуляции с fragment после appendChild: после вставки узлы принадлежат DOM, не fragment — попытки менять fragment бессмысленны.

Связанные темы

Ресурсы