DocumentFragment и производительность DOM
DocumentFragment и производительность
Проблема: многократные вставки в DOM
Каждая операция вставки в DOM потенциально вызывает reflow (пересчёт раскладки):
const list = document.querySelector('ul')
// МЕДЛЕННО: 1000 reflow
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li')
li.textContent = `Элемент ${i}`
list.appendChild(li) // reflow при каждой вставке
}
DocumentFragment — батчинг вставок
const list = document.querySelector('ul')
// БЫСТРО: 1 reflow
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li')
li.textContent = `Элемент ${i}`
fragment.appendChild(li) // вставка во fragment — нет reflow!
}
list.appendChild(fragment) // ОДИН reflow для всех элементов
DocumentFragment — это «виртуальный» контейнер, который не является частью DOM. Добавление элементов в fragment не вызывает reflow. При вставке fragment в DOM все его дети переносятся в DOM за одну операцию.Что вызывает reflow и repaint
// Чтение геометрических свойств вызывает forced reflow:
const height = el.offsetHeight // reflow!
const width = el.getBoundingClientRect().width // reflow!
const scrollTop = el.scrollTop // reflow!
// Layout thrashing — чередование чтения и записи:
// ПЛОХО:
for (const el of elements) {
const height = el.offsetHeight // reflow (чтение)
el.style.top = height + 'px' // invalidate layout (запись)
}
// ХОРОШО: сначала все чтения, потом все записи
const heights = elements.map(el => el.offsetHeight) // все чтения
elements.forEach((el, i) => {
el.style.top = heights[i] + 'px' // все записи
})
requestAnimationFrame для батчинга
// Все изменения в одном rAF происходят в одном кадре
function updatePositions(elements, positions) {
requestAnimationFrame(() => {
elements.forEach((el, i) => {
el.style.transform = `translateX(${positions[i]}px)`
})
})
}
// Накапливаем изменения, применяем в rAF
class DOMBatcher {
constructor() {
this.pending = []
this.scheduled = false
}
write(fn) {
this.pending.push(fn)
if (!this.scheduled) {
this.scheduled = true
requestAnimationFrame(() => {
this.pending.forEach(f => f())
this.pending = []
this.scheduled = false
})
}
}
}
const batcher = new DOMBatcher()
batcher.write(() => div.style.left = '100px')
batcher.write(() => div.style.top = '50px')
// Оба изменения применятся в одном кадре
React, Vue и другие фреймворки используют «виртуальный DOM» (vDOM) для минимизации реальных DOM-операций:
- Создают JS-объект, описывающий желаемое состояние DOM
- После изменений состояния создают новый vDOM
- Сравнивают (diffing) старый и новый vDOM
- Находят минимальный набор изменений
- Применяют только эти изменения к реальному DOM
// Упрощённый пример vDOM
const vdom = {
tag: 'div',
props: { className: 'container' },
children: [
{ tag: 'h1', props: {}, children: ['Заголовок'] },
{ tag: 'p', props: {}, children: ['Текст'] }
]
}
// При обновлении — только измененные узлы
DocumentFragment — это более простое решение той же проблемы: собрать все изменения, потом применить одним разом.