← Браузер/Reflow, Repaint и производительность#177 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: async и сетьТермин: Event LoopТермин: Core Web Vitals

Reflow, Repaint и производительность

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

Что такое Reflow (Layout)

Reflow — это пересчёт геометрии всех элементов страницы. Браузер вычисляет, где должен находиться каждый элемент, какой он ширины и высоты, как он влияет на соседей.

Reflow запускается при любом изменении, которое влияет на геометрию:

  • Изменение размеров: width, height, padding, margin, border
  • Изменение структуры DOM: добавление/удаление элементов
  • Изменение содержимого: текст, картинки другого размера
  • Чтение геометрических свойств: offsetWidth, getBoundingClientRect(), scrollTop
  • Reflow — дорогая операция: браузер должен пересчитать всё дерево элементов (или большую его часть), а не только изменившийся элемент.

    Что такое Repaint

    Repaint — это перерисовка пикселей без изменения геометрии. Меняется только внешний вид: цвет, фон, тень, border-radius, видимость.

    Repaint дешевле Reflow, но тоже затратен: нужно перерисовать пиксели элемента и все перекрывающиеся с ним слои.

    Операции только Repaint (без Reflow): color, background-color, box-shadow, border-color, outline, visibility.

    Layout Thrashing — принудительный синхронный Layout

    Самая распространённая ошибка производительности — чтение геометрических свойств сразу после записи стилей в цикле. Браузер вынужден завершить layout до следующего чтения, что приводит к сотням layout-операций за один кадр.

    // ПЛОХО: layout thrashing
    for (let i = 0; i < items.length; i++) {
      items[i].style.width = container.offsetWidth + 'px'  // чтение → запись → reflow!
    }
    
    // ХОРОШО: сначала читаем, потом пишем
    const width = container.offsetWidth  // одно чтение
    for (let i = 0; i < items.length; i++) {
      items[i].style.width = width + 'px'  // только запись
    }

    GPU-составные анимации

    Некоторые CSS-свойства браузер выносит на отдельный GPU-слой и анимирует без участия CPU-рендеринга. Это transform и opacity. GPU-анимации не вызывают ни Reflow, ни Repaint — они работают в 60fps даже на слабых устройствах.

    Плохо: анимировать left, top, width, margin — каждый кадр Reflow.

    Хорошо: анимировать transform: translateX(), transform: scale(), opacity — только Composite.

    Пакетные обновления DOM с DocumentFragment

    Когда нужно добавить много элементов, вставляй их через DocumentFragment. Fragment — это виртуальный контейнер в памяти, который не является частью DOM. При добавлении элементов в Fragment Reflow не происходит. Один Reflow случается только при финальной вставке Fragment в DOM.

    requestAnimationFrame

    requestAnimationFrame(callback) запускает callback перед следующей перерисовкой браузера, обычно ~16.7мс при 60fps. Это правильное место для любых визуальных обновлений: анимации согласуются с ритмом браузера и не вызывают лишних перерисовок.

    Примеры

    Layout thrashing vs пакетное обновление — сравнение производительности

    // --- МЕДЛЕННЫЙ СПОСОБ: layout thrashing ---
    function slowBuildList(container, count) {
      const start = performance.now()
    
      for (let i = 0; i < count; i++) {
        const item = document.createElement('div')
        item.className = 'item'
        item.textContent = `Элемент ${i + 1}`
        container.appendChild(item)  // каждое добавление — Reflow!
    
        // Чтение после записи — принудительный синхронный Reflow
        const height = container.scrollHeight  // браузер обязан пересчитать
        item.style.marginTop = (height > 100) ? '2px' : '4px'
      }
    
      console.log(`Медленный способ: ${(performance.now() - start).toFixed(1)} мс`)
    }
    
    // --- БЫСТРЫЙ СПОСОБ: DocumentFragment + пакетное обновление ---
    function fastBuildList(container, count) {
      const start = performance.now()
    
      // Сначала читаем нужные данные
      const useSmallMargin = container.scrollHeight > 100
      const margin = useSmallMargin ? '2px' : '4px'
    
      // Строим всё в DocumentFragment — без Reflow
      const fragment = document.createDocumentFragment()
    
      for (let i = 0; i < count; i++) {
        const item = document.createElement('div')
        item.className = 'item'
        item.textContent = `Элемент ${i + 1}`
        item.style.marginTop = margin
        fragment.appendChild(item)  // нет Reflow — Fragment не в DOM
      }
    
      // Один Reflow при финальной вставке
      container.appendChild(fragment)
    
      console.log(`Быстрый способ: ${(performance.now() - start).toFixed(1)} мс`)
    }
    
    // GPU-анимация через requestAnimationFrame
    function animateBox(box) {
      let position = 0
      function frame() {
        position += 2
        // transform не вызывает Reflow/Repaint — только Composite
        box.style.transform = `translateX(${position}px)`
        if (position < 300) requestAnimationFrame(frame)
      }
      requestAnimationFrame(frame)
    }

    Reflow, Repaint и производительность

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

    Что такое Reflow (Layout)

    Reflow — это пересчёт геометрии всех элементов страницы. Браузер вычисляет, где должен находиться каждый элемент, какой он ширины и высоты, как он влияет на соседей.

    Reflow запускается при любом изменении, которое влияет на геометрию:

  • Изменение размеров: width, height, padding, margin, border
  • Изменение структуры DOM: добавление/удаление элементов
  • Изменение содержимого: текст, картинки другого размера
  • Чтение геометрических свойств: offsetWidth, getBoundingClientRect(), scrollTop
  • Reflow — дорогая операция: браузер должен пересчитать всё дерево элементов (или большую его часть), а не только изменившийся элемент.

    Что такое Repaint

    Repaint — это перерисовка пикселей без изменения геометрии. Меняется только внешний вид: цвет, фон, тень, border-radius, видимость.

    Repaint дешевле Reflow, но тоже затратен: нужно перерисовать пиксели элемента и все перекрывающиеся с ним слои.

    Операции только Repaint (без Reflow): color, background-color, box-shadow, border-color, outline, visibility.

    Layout Thrashing — принудительный синхронный Layout

    Самая распространённая ошибка производительности — чтение геометрических свойств сразу после записи стилей в цикле. Браузер вынужден завершить layout до следующего чтения, что приводит к сотням layout-операций за один кадр.

    // ПЛОХО: layout thrashing
    for (let i = 0; i < items.length; i++) {
      items[i].style.width = container.offsetWidth + 'px'  // чтение → запись → reflow!
    }
    
    // ХОРОШО: сначала читаем, потом пишем
    const width = container.offsetWidth  // одно чтение
    for (let i = 0; i < items.length; i++) {
      items[i].style.width = width + 'px'  // только запись
    }

    GPU-составные анимации

    Некоторые CSS-свойства браузер выносит на отдельный GPU-слой и анимирует без участия CPU-рендеринга. Это transform и opacity. GPU-анимации не вызывают ни Reflow, ни Repaint — они работают в 60fps даже на слабых устройствах.

    Плохо: анимировать left, top, width, margin — каждый кадр Reflow.

    Хорошо: анимировать transform: translateX(), transform: scale(), opacity — только Composite.

    Пакетные обновления DOM с DocumentFragment

    Когда нужно добавить много элементов, вставляй их через DocumentFragment. Fragment — это виртуальный контейнер в памяти, который не является частью DOM. При добавлении элементов в Fragment Reflow не происходит. Один Reflow случается только при финальной вставке Fragment в DOM.

    requestAnimationFrame

    requestAnimationFrame(callback) запускает callback перед следующей перерисовкой браузера, обычно ~16.7мс при 60fps. Это правильное место для любых визуальных обновлений: анимации согласуются с ритмом браузера и не вызывают лишних перерисовок.

    Примеры

    Layout thrashing vs пакетное обновление — сравнение производительности

    // --- МЕДЛЕННЫЙ СПОСОБ: layout thrashing ---
    function slowBuildList(container, count) {
      const start = performance.now()
    
      for (let i = 0; i < count; i++) {
        const item = document.createElement('div')
        item.className = 'item'
        item.textContent = `Элемент ${i + 1}`
        container.appendChild(item)  // каждое добавление — Reflow!
    
        // Чтение после записи — принудительный синхронный Reflow
        const height = container.scrollHeight  // браузер обязан пересчитать
        item.style.marginTop = (height > 100) ? '2px' : '4px'
      }
    
      console.log(`Медленный способ: ${(performance.now() - start).toFixed(1)} мс`)
    }
    
    // --- БЫСТРЫЙ СПОСОБ: DocumentFragment + пакетное обновление ---
    function fastBuildList(container, count) {
      const start = performance.now()
    
      // Сначала читаем нужные данные
      const useSmallMargin = container.scrollHeight > 100
      const margin = useSmallMargin ? '2px' : '4px'
    
      // Строим всё в DocumentFragment — без Reflow
      const fragment = document.createDocumentFragment()
    
      for (let i = 0; i < count; i++) {
        const item = document.createElement('div')
        item.className = 'item'
        item.textContent = `Элемент ${i + 1}`
        item.style.marginTop = margin
        fragment.appendChild(item)  // нет Reflow — Fragment не в DOM
      }
    
      // Один Reflow при финальной вставке
      container.appendChild(fragment)
    
      console.log(`Быстрый способ: ${(performance.now() - start).toFixed(1)} мс`)
    }
    
    // GPU-анимация через requestAnimationFrame
    function animateBox(box) {
      let position = 0
      function frame() {
        position += 2
        // transform не вызывает Reflow/Repaint — только Composite
        box.style.transform = `translateX(${position}px)`
        if (position < 300) requestAnimationFrame(frame)
      }
      requestAnimationFrame(frame)
    }

    Задание

    Оптимизируй функцию buildList: вместо прямой вставки в DOM используй DocumentFragment. Сравни время выполнения медленной и быстрой версии с помощью performance.now(). Добавь в контейнер 500 элементов.

    Подсказка

    Создай fragment через document.createDocumentFragment(). Добавляй div-элементы в fragment (не в container). После цикла вставь fragment в container одной операцией appendChild.

    Загружаем среду выполнения...
    Загружаем AI-помощника...