← Назад к вопросам

Были ли проблемы при Relayout

2.0 Middle🔥 131 комментариев
#Браузер и сетевые технологии#Оптимизация и производительность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Relayout (Reflow) - проблемы и оптимизация

Что такое Relayout/Reflow

Relayout (Reflow) - это процесс пересчета размеров и позиций элементов на странице. Когда браузер изменяет DOM или стили, ему нужно пересчитать макет страницы. Это дорогостоящая операция.

// Каждая из этих операций вызовет Reflow:
element.style.width = '100px';       // Изменение размера
element.style.display = 'none';      // Изменение видимости
const height = element.offsetHeight; // Чтение размера
document.body.appendChild(element);  // Добавление в DOM
window.innerWidth;                   // Чтение размера окна

Проблемы, которые я встречал

Проблема 1: Множественные Reflow при изменении стилей

// ПЛОХО - 3 Reflow
const element = document.getElementById('box');
element.style.width = '100px';   // Reflow 1
element.style.height = '100px';  // Reflow 2
element.style.margin = '10px';   // Reflow 3

// ХОРОШО - 1 Reflow
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';

// ИЛИ ЛУЧШЕ - 1 Reflow
element.className = 'styled'; // Применить класс с несколькими свойствами

Проблема 2: Чередование чтений и записей

// ПЛОХО - множественные Reflow
const boxes = document.querySelectorAll('.box');
for (let i = 0; i < boxes.length; i++) {
  boxes[i].style.width = boxes[0].offsetWidth + 'px'; // Читаем offsetWidth
  // После каждого write идет read -> Reflow!
}

// ХОРОШО - группируем чтения и записи
const width = boxes[0].offsetWidth; // Прочитать один раз
for (let i = 0; i < boxes.length; i++) {
  boxes[i].style.width = width + 'px'; // Использовать для всех
}

Проблема 3: Animation вызывает множественные Reflow

// ОЧЕНЬ ПЛОХО - Reflow на каждом кадре
function animatePosition(element) {
  let position = 0;
  setInterval(() => {
    position += 10;
    element.style.left = position + 'px'; // Reflow каждые 10ms
  }, 10);
}

// ХОРОШО - использовать requestAnimationFrame и transform
function animatePosition(element) {
  let position = 0;
  
  function animate() {
    position += 10;
    // transform не вызывает Reflow, только Repaint
    element.style.transform = `translateX(${position}px)`;
    
    if (position < 1000) {
      requestAnimationFrame(animate);
    }
  }
  
  requestAnimationFrame(animate);
}

Проблема 4: DOM операции в цикле

// ОЧЕНЬ ПЛОХО - Reflow для каждого элемента
const parent = document.getElementById('parent');
for (let i = 0; i < 1000; i++) {
  const element = document.createElement('div');
  element.textContent = `Item ${i}`;
  parent.appendChild(element); // Reflow каждый раз!
}

// ХОРОШО - использовать DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const element = document.createElement('div');
  element.textContent = `Item ${i}`;
  fragment.appendChild(element); // Без Reflow
}
parent.appendChild(fragment); // 1 Reflow в конце

// ИЛИ - использовать innerHTML
let html = '';
for (let i = 0; i < 1000; i++) {
  html += `<div>Item ${i}</div>`;
}
parent.innerHTML = html; // 1 Reflow

Жизненный цикл браузера: Rendering Pipeline

┌─────────────────────────────────────────────────────┐
│ 1. JavaScript выполняется                           │
│    (изменяет DOM/styles)                           │
└────────────────────┬────────────────────────────────┘
                     |
                     v
┌─────────────────────────────────────────────────────┐
│ 2. Reflow/Layout (пересчет размеров и позиций)    │
│    (дорого - O(n))                                 │
└────────────────────┬────────────────────────────────┘
                     |
                     v
┌─────────────────────────────────────────────────────┐
│ 3. Repaint (перерисовка элементов на экране)       │
│    (менее дорого)                                  │
└────────────────────┬────────────────────────────────┘
                     |
                     v
┌─────────────────────────────────────────────────────┐
│ 4. Composite (объединение слоев)                    │
│    (очень быстро на GPU)                           │
└─────────────────────────────────────────────────────┘

Какие операции вызывают Reflow

Чтение свойств (force Reflow):

// Все эти чтения вызовут Reflow перед тем как вернуть значение:
element.offsetHeight;
element.offsetWidth;
element.offsetLeft;
element.offsetTop;
element.clientHeight;
element.clientWidth;
element.clientLeft;
element.clientTop;
element.scrollHeight;
element.scrollWidth;
element.scrollLeft;
element.scrollTop;
window.innerWidth;
window.innerHeight;
window.scrollX;
window.scrollY;
getBoundingClientRect();
getComputedStyle();

Изменения DOM и стилей:

element.style.width = '...';        // Reflow
element.style.display = '...';      // Reflow
dom.appendChild(element);            // Reflow
dom.removeChild(element);            // Reflow
document.body.innerHTML = '...';     // Reflow

Практические примеры оптимизации

Пример 1: Обновление множества элементов

// БЫЛО (медленно)
function updateBoxes(boxes, newWidth) {
  boxes.forEach(box => {
    box.style.width = newWidth + 'px';
  });
}

// СТАЛО (быстро)
function updateBoxes(boxes, newWidth) {
  // Способ 1: cssText
  boxes.forEach(box => {
    box.style.cssText = `width: ${newWidth}px`;
  });
  
  // Способ 2: className
  boxes.forEach(box => {
    box.className = 'new-width';
  });
}

/* CSS */
.new-width {
  width: 200px;
}

Пример 2: Чтение offsetHeight для всех элементов

// БЫЛО (Reflow для каждого элемента)
const elements = document.querySelectorAll('.item');
const heights = [];
for (let i = 0; i < elements.length; i++) {
  // Каждое чтение offsetHeight вызывает Reflow!
  heights.push(elements[i].offsetHeight);
  elements[i].style.height = heights[i] + 'px';
}

// СТАЛО (оптимизировано)
const elements = document.querySelectorAll('.item');
const heights = elements.map(el => el.offsetHeight); // Все чтения сразу
for (let i = 0; i < elements.length; i++) {
  elements[i].style.height = heights[i] + 'px'; // Потом все записи
}

Пример 3: Анимация со смещением

// БЫЛО (вызывает Reflow каждый кадр)
function slideIn(element) {
  let position = 0;
  const animate = () => {
    if (position >= 100) return;
    position++;
    element.style.left = position + 'px'; // Reflow!
    requestAnimationFrame(animate);
  };
  requestAnimationFrame(animate);
}

// СТАЛО (только Repaint)
function slideIn(element) {
  let position = 0;
  const animate = () => {
    if (position >= 100) return;
    position++;
    // transform не вызывает Reflow, только Composite
    element.style.transform = `translateX(${position}px)`;
    requestAnimationFrame(animate);
  };
  requestAnimationFrame(animate);
}

Пример 4: Массовое добавление элементов в React

// БЫЛО
function ItemList({ items }) {
  return (
    <div>
      {items.map((item, index) => (
        // Каждый render может вызвать Reflow
        <Item key={index} data={item} />
      ))}
    </div>
  );
}

// СТАЛО (обычно React это оптимизирует автоматически)
// Но можно помочь с помощью key и virtualizing
import { FixedSizeList } from 'react-window';

function ItemList({ items }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <Item style={style} data={items[index]} />
      )}
    </FixedSizeList>
  );
}

Debug: DevTools для анализа Reflow

Chrome DevTools:

// 1. Открыть DevTools -> Performance
// 2. Нажать Record
// 3. Выполнить операции
// 4. Нажать Stop

// Смотрим:
// - фиолетовые полосы = Reflow (Layout)
// - зеленые полосы = Paint
// - серые полосы = Composite

// Можно также использовать консоль:
const start = performance.now();
element.style.width = '100px';
const width = element.offsetWidth; // Force Reflow
const end = performance.now();
console.log(`Reflow took ${end - start}ms`);

Реальный случай из практики

Проблема: список 10,000 элементов обновляется медленно

// БЫЛО (200ms per update)
function updateList(items) {
  const container = document.getElementById('list');
  const html = items.map(item => 
    `<div class="item" style="left: ${item.position}px">${item.name}</div>`
  ).join('');
  container.innerHTML = html; // 1 Reflow для всех
}

// Но когда обновляем в цикле:
for (let i = 0; i < items.length; i++) {
  items[i].position += 10;
  updateList(items); // 10,000 Reflow!
}

// СТАЛО (10ms per update)
function updateList(items) {
  const container = document.getElementById('list');
  // Использовать виртуальный список
  container.innerHTML = items
    .slice(0, 50) // Показать только 50
    .map(item => 
      `<div class="item" style="transform: translateX(${item.position}px)">${item.name}</div>`
    )
    .join('');
}

// Transform - нет Reflow, только Composite
for (let i = 0; i < items.length; i++) {
  items[i].position += 10;
  updateList(items); // 1 Reflow + множество Composite
}

Заключение

Проблемы Relayout:

  1. Дорого - O(n), может быть медленнее всего в браузере
  2. Неявно - трудно заметить проблему
  3. Часто - множественные Reflow в цикле

Как избежать:

  • Группировать чтения и записи
  • Использовать transform вместо left/top
  • Использовать cssText для множественных стилей
  • Использовать DocumentFragment для добавления элементов
  • Использовать виртуальные списки для больших данных
  • Измерять в DevTools Performance

Правило 80/20:

  • 80% проблем Reflow решаются использованием transform
  • 20% требуют глубокого анализа и оптимизации