DocumentFragment и производительность

узлы≈ 6 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Вставлять узлы по одному — как носить книги в библиотеку по штуке: один заход ничего не стоит, сотня заходов уже заметна. DocumentFragment — та самая тележка, на которой всё уезжает за один раз.

Разделы темы:·

Как только элемент попал в документ, он живой: браузер должен знать, где он лежит, какого он размера и что от этого поехало. Сам по себе append дёшев — браузер ленив и откладывает пересчёт раскладки до конца текущей задачи. Дорого становится, когда между вставками кто-то спрашивает геометрию: offsetHeight, getBoundingClientRect, scrollTop. На такой вопрос нельзя ответить старыми данными, и раскладка считается заново — столько раз, сколько витков в цикле.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Три вставки — три вынужденных пересчёта. С тремя пунктами это незаметно, с тремя сотнями строк таблицы страница ощутимо замирает. Лечится это двумя независимыми способами: убрать измерения из цикла и уменьшить число правок живого дерева. Второй способ и есть фрагмент.

DocumentFragment — узел без тега. У него есть дети, но нет родителя, он не участвует в раскладке и ничего не рисует. Когда фрагмент вставляют в документ, внутрь уезжают его дети, а сам он остаётся пустой оболочкой: это перемещение, а не копирование.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

У фрагмента нет innerHTML, classList, closest и атрибутов — всё это живёт на Element. Зато есть общие для контейнеров append, children, querySelector. И пока фрагмент не вставлен, документ его содержимого не видит.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Рецепт всегда один: создаём фрагмент, набиваем его в цикле, а живое дерево трогаем ровно один раз — последней строкой. Внутри цикла ни одного обращения к документу, значит браузеру нечего пересчитывать.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Приятный побочный эффект: ссылки на созданные элементы остаются у вас в руках. Можно навешивать обработчики и складывать узлы в Map ещё до вставки — в отличие от сборки страницы через строку разметки, где элементы приходится потом искать заново.

Фрагмент не спасёт от чтения offsetHeight внутри цикла. Сначала уберите измерения, и только потом сокращайте число вставок — порядок именно такой.

Когда узел сложнее одного тега, собирать его руками утомительно. Тег <template> хранит разметку в спящем виде: содержимое не рисуется, картинки не грузятся, скрипты не выполняются. А свойство content — это ровно наш DocumentFragment, который можно клонировать сколько угодно раз.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Важно клонировать tpl.content, а не вставлять его напрямую: прямая вставка опустошит шаблон, и на второй итерации копировать будет нечего. И обязательно с аргументом true — иначе скопируется пустая оболочка без потомков.

Фрагмент — инструмент против количества, а не ритуал. Для трёх пунктов меню выигрыш неизмерим, а лишняя переменная в коде вполне заметна. Часто задачу закрывает replaceChildren: он и чистит контейнер, и вставляет новое содержимое одной операцией.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
ПриёмПравок живого дереваКогда уместен
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 на каждый город. Живое дерево при этом должно измениться ровно один раз.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Копим карточки в фрагменте, вставляем последней строкой:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Внутри цикла нет ни одного обращения к документу, поэтому браузеру нечего пересчитывать. Число городов может вырасти до тысячи — правка живого дерева всё равно останется одна.

Строки расписания из шаблона

Разметка строки уже лежит в <template>. Склонируйте её для каждой пары, заполните b временем и span названием, накопите в фрагменте и вставьте в список.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Ключевое — cloneNode(true): без копии первая же вставка опустошила бы шаблон, а без аргумента true скопировался бы фрагмент без потомков.