DocumentFragment и производительность
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Вставлять узлы по одному — как носить книги в библиотеку по штуке: один заход ничего не стоит, сотня заходов уже заметна. DocumentFragment — та самая тележка, на которой всё уезжает за один раз.
Как только элемент попал в документ, он живой: браузер должен знать, где он лежит, какого он размера и что от этого поехало. Сам по себе append дёшев — браузер ленив и откладывает пересчёт раскладки до конца текущей задачи. Дорого становится, когда между вставками кто-то спрашивает геометрию: offsetHeight, getBoundingClientRect, scrollTop. На такой вопрос нельзя ответить старыми данными, и раскладка считается заново — столько раз, сколько витков в цикле.
Три вставки — три вынужденных пересчёта. С тремя пунктами это незаметно, с тремя сотнями строк таблицы страница ощутимо замирает. Лечится это двумя независимыми способами: убрать измерения из цикла и уменьшить число правок живого дерева. Второй способ и есть фрагмент.
DocumentFragment — узел без тега. У него есть дети, но нет родителя, он не участвует в раскладке и ничего не рисует. Когда фрагмент вставляют в документ, внутрь уезжают его дети, а сам он остаётся пустой оболочкой: это перемещение, а не копирование.
У фрагмента нет innerHTML, classList, closest и атрибутов — всё это живёт на Element. Зато есть общие для контейнеров append, children, querySelector. И пока фрагмент не вставлен, документ его содержимого не видит.
Рецепт всегда один: создаём фрагмент, набиваем его в цикле, а живое дерево трогаем ровно один раз — последней строкой. Внутри цикла ни одного обращения к документу, значит браузеру нечего пересчитывать.
Приятный побочный эффект: ссылки на созданные элементы остаются у вас в руках. Можно навешивать обработчики и складывать узлы в Map ещё до вставки — в отличие от сборки страницы через строку разметки, где элементы приходится потом искать заново.
Фрагмент не спасёт от чтения offsetHeight внутри цикла. Сначала уберите измерения, и только потом сокращайте число вставок — порядок именно такой.
Когда узел сложнее одного тега, собирать его руками утомительно. Тег <template> хранит разметку в спящем виде: содержимое не рисуется, картинки не грузятся, скрипты не выполняются. А свойство content — это ровно наш DocumentFragment, который можно клонировать сколько угодно раз.
Важно клонировать tpl.content, а не вставлять его напрямую: прямая вставка опустошит шаблон, и на второй итерации копировать будет нечего. И обязательно с аргументом true — иначе скопируется пустая оболочка без потомков.
Фрагмент — инструмент против количества, а не ритуал. Для трёх пунктов меню выигрыш неизмерим, а лишняя переменная в коде вполне заметна. Часто задачу закрывает replaceChildren: он и чистит контейнер, и вставляет новое содержимое одной операцией.
| Приём | Правок живого дерева | Когда уместен |
|---|---|---|
| append в цикле | по одной на узел | один-три узла, геометрия не читается |
| DocumentFragment | одна | десятки и сотни узлов, нужны ссылки на элементы |
| replaceChildren(...nodes) | одна | полная замена содержимого |
| innerHTML = строка | одна | разметка целиком из своих данных, без чужого HTML |
Правило простое: сначала измерьте, потом оптимизируйте. Если список рисуется на каждое нажатие клавиши и в нём триста строк — фрагмент или replaceChildren окупятся сразу. Если он строится один раз при загрузке и состоит из пяти пунктов — берите то, что понятнее читать.
- Вставка узла в документ дешёвая; дорого обходится чтение геометрии между вставками — оно заставляет пересчитывать раскладку на каждом витке цикла.
- DocumentFragment — узел без тега: живёт вне документа, ничего не рисует, при вставке отдаёт детей и остаётся пустым.
- Схема одна: создать фрагмент, набить в цикле, один раз вставить в живое дерево — ссылки на элементы при этом остаются у вас.
- template.content — готовый фрагмент; для каждого повторения делайте cloneNode(true), иначе шаблон опустеет после первой вставки.
- Для трёх узлов черновик не нужен: replaceChildren или обычный append читаются лучше. Сначала измерьте, потом оптимизируйте.
1/3Что выведет frag.children.length сразу после box.append(frag)?
2/3Почему цикл из сотни вставок может ощутимо тормозить страницу?
3/3Элемент с id="hint" лежит в фрагменте, который ещё не вставлен в страницу. Что вернёт document.getElementById("hint")?
Шесть карточек за одну вставку
В #grid нужно положить по одному блоку div.card на каждый город. Живое дерево при этом должно измениться ровно один раз.
Копим карточки в фрагменте, вставляем последней строкой:
Внутри цикла нет ни одного обращения к документу, поэтому браузеру нечего пересчитывать. Число городов может вырасти до тысячи — правка живого дерева всё равно останется одна.
Строки расписания из шаблона
Разметка строки уже лежит в <template>. Склонируйте её для каждой пары, заполните b временем и span названием, накопите в фрагменте и вставьте в список.
Ключевое — cloneNode(true): без копии первая же вставка опустошила бы шаблон, а без аргумента true скопировался бы фрагмент без потомков.