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 бессмысленны.