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

Как решить проблему с зависанием страницы?

2.0 Middle🔥 121 комментариев
#Soft Skills и рабочие процессы

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

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

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

Решение проблемы зависания (freezing) страницы

Зависание страницы - это когда пользователь видит, что страница не реагирует на клики, скролл или ввод текста. Это одна из самых раздражающих проблем в веб-разработке. Давайте разберёмся, почему это происходит и как это исправить.

1. Основная причина - Main Thread блокирован

Вся JavaScript логика в браузере работает в одном потоке (Main Thread). Если код долго выполняется, страница зависает:

// ПЛОХО - зависает на 5 секунд
function processLargeData() {
  let result = 0;
  for (let i = 0; i < 1000000000; i++) {
    result += Math.sqrt(i);  // Тяжёлые вычисления
  }
  return result;
}

// Пользователь кликает на кнопку -> ничего не происходит
// Скролит страницу -> не скролится
// Это потому что main thread занят вычислениями

Иллюстрация работы main thread:

Время:      0s          1s          2s          3s          4s          5s
            |-----------|-----------|-----------|-----------|-----------|  
Main Thread: [████████████████ processLargeData() ██████████████████]
             (невозможно обрабатывать другие события)

2. Диагностика проблемы с Chrome DevTools Performance

Нужно найти, что зависает:

// Шаг 1: Открой Chrome DevTools -> Performance tab
// Шаг 2: Нажми запись, выполни действие, которое зависает
// Шаг 3: Посмотри на диаграмму:

const analysis = {
  'Длинная жёлтая полоса': 'Скрипт работает долго',
  'Красный треугольник внизу': 'Janky frame - потеряны кадры',
  'Frame rate упал': 'С 60fps упал до 10fps'
};

// Нажми на жёлтую полоску и видишь, какой код работает

3. Решение 1 - Разбить работу на части (Chunking)

Вместо одного долгого выполнения, разбей на маленькие кусочки:

// ПЛОХО - зависает
function processItems(items) {
  items.forEach(item => {
    expensiveOperation(item);  // Для каждого элемента
  });
}
processItems(millionItems);

// ХОРОШО - разбиваем на батчи
function processItemsInChunks(items, batchSize = 100) {
  let index = 0;
  
  function processBatch() {
    const end = Math.min(index + batchSize, items.length);
    
    for (let i = index; i < end; i++) {
      expensiveOperation(items[i]);
    }
    
    index = end;
    
    if (index < items.length) {
      // Дай браузеру время обновить UI
      requestAnimationFrame(processBatch);
    }
  }
  
  processBatch();
}

processItemsInChunks(millionItems, 100);

// Результат: страница остаётся отзывчивой

Визуальная разница:

БЕЗ chunking:                    С chunking:
[████████████████] 5 сек         [██] [██] [██] 5 сек
 (зависла на 5 сек)              (отзывчива 95% времени)

4. Решение 2 - Web Workers для вычислений

Для тяжёлых вычислений используй отдельный поток:

// main.js
const worker = new Worker('heavy-computation.js');

// Отправь данные на обработку
worker.postMessage({ data: millionItems });

// Получи результат (когда worker закончит)
worker.onmessage = (event) => {
  const result = event.data;
  updateUI(result);
};

// main thread остаётся свободным!
// heavy-computation.js (worker)
self.onmessage = (event) => {
  const items = event.data.data;
  
  // Долгие вычисления в отдельном потоке
  const result = items.map(item => expensiveOperation(item));
  
  // Отправляем результат обратно
  self.postMessage({ result });
};

Преимущество: вычисления не блокируют UI.

5. Решение 3 - setTimeout и debounce/throttle

Для событий, которые срабатывают много раз:

// ПЛОХО - срабатывает при каждом движении мыши (сотни раз в сек)
window.addEventListener('mousemove', (e) => {
  expensiveCalculation(e.x, e.y);  // Зависает!
});

// ХОРОШО - используй throttle
function throttle(func, delay) {
  let lastRun = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastRun >= delay) {
      func(...args);
      lastRun = now;
    }
  };
}

const throttledCalc = throttle((e) => {
  expensiveCalculation(e.x, e.y);
}, 100);  // Максимум 10 раз в сек

window.addEventListener('mousemove', throttledCalc);

// Или с debounce - дождись, пока пользователь остановится
function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func(...args), delay);
  };
}

const debouncedSearch = debounce((query) => {
  expensiveSearch(query);
}, 300);

input.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

6. Решение 4 - Виртуализация для больших списков

Если отрисовываешь 1000+ элементов списка - зависает:

// ПЛОХО - 1000 элементов в DOM
function renderList(items) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

// ХОРОШО - используй react-window или react-virtualized
import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={35}
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

// Результат: в DOM только видимые элементы (~20-30)
// Остальные вставляются/удаляются при скролле

7. Решение 5 - Code Splitting и Lazy Loading

Если зависает при загрузке страницы:

// ПЛОХО - весь код загружается сразу (500KB)
import HeavyComponent from './HeavyComponent';

// ХОРОШО - загружается только когда нужен
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

Результат: страница загружается быстро, тяжёлые компоненты загружаются по требованию.

8. Решение 6 - Optimized Rendering

Иногда проблема в том, как React отрисовывает компоненты:

// ПЛОХО - каждый render пересчитывает всё
function Parent() {
  const [filter, setFilter] = useState('');
  
  return (
    <>
      <Input onChange={setFilter} />
      <HugeList items={allItems} />  {/* Пересчитывается при каждом изменении фильтра */}
    </>
  );
}

// ХОРОШО - используй useMemo и React.memo
const HugeListMemo = React.memo(HugeList);

function Parent() {
  const [filter, setFilter] = useState('');
  const filteredItems = useMemo(
    () => allItems.filter(item => item.name.includes(filter)),
    [filter]
  );
  
  return (
    <>
      <Input onChange={setFilter} />
      <HugeListMemo items={filteredItems} />
    </>
  );
}

// Или используй useCallback для стабильных функций
const handleClick = useCallback(() => {
  // ...
}, [dependency]);

9. Решение 7 - Оптимизация анимаций

Анимации могут вызвать зависание:

/* ПЛОХО - анимирует width (вызывает reflow) */
.box {
  animation: slideWidth 1s;
}
@keyframes slideWidth {
  from { width: 0; }
  to { width: 100px; }
}

/* ХОРОШО - анимирует transform (не вызывает reflow) */
.box {
  animation: slideTransform 1s;
}
@keyframes slideTransform {
  from { transform: translateX(0); }
  to { transform: translateX(100px); }
}

Transform анимируется на GPU, не зависает.

10. Полная диагностика зависания

Когда страница зависает, следуй этому алгоритму:

const diagnosis = [
  '1. Открой Chrome DevTools -> Performance',
  '2. Запиши что-то (5-10 секунд)',
  '3. Нажми жёлтый значок (Show what happened during each frame)',
  '4. Ищи frames, где FPS < 60 (обычно < 30)',
  '5. Кликни на frame и видишь что работало',
  '6. Посмотри:
     - Если жёлтое (JavaScript) - проблема в скрипте
     - Если фиолетовое (Rendering) - проблема в Layout/Paint
     - Если зелёное (Painting) - проблема в стилях',
  '7. Возьми наибольший блок и кликни на него',
  '8. В открывшемся окне видишь точный вызов функции',
  '9. Фиксишь эту функцию'
];

// Пример результата:
// expensiveSearch() - 3.5 сек
// renderItems() - 1.2 сек
// updateUI() - 0.8 сек
// -> Фокусируешься на expensiveSearch()

11. Практический пример - зависание при поиске

// ДО (зависает)
function SearchComponent() {
  const [query, setQuery] = useState('');
  
  const handleChange = (e) => {
    setQuery(e.target.value);
    
    // Фильтруем 1 миллион элементов при каждом вводе
    const filtered = millionItems.filter(item =>
      item.name.toLowerCase().includes(e.target.value.toLowerCase())
    );
    
    setResults(filtered);
  };
  
  return (
    <>
      <input onChange={handleChange} />
      <ul>
        {results.map(item => <li key={item.id}>{item.name}</li>)}
      </ul>
    </>
  );
}

// ПОСЛЕ (оптимизировано)
function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  // Дебаунсим поиск - ждём 300ms после последнего ввода
  const debouncedSearch = useCallback(
    debounce((searchQuery) => {
      // Это вычисляется не при каждом вводе, а только когда пользователь остановился
      const filtered = millionItems.filter(item =>
        item.name.toLowerCase().includes(searchQuery.toLowerCase())
      );
      setResults(filtered);
    }, 300),
    []
  );
  
  const handleChange = (e) => {
    setQuery(e.target.value);
    debouncedSearch(e.target.value);
  };
  
  return (
    <>
      <input onChange={handleChange} value={query} />
      <VirtualizedList items={results} /> {/* Только видимые элементы в DOM */}
    </>
  );
}

Итог

Зависание страницы - это когда main thread блокирован долгим кодом. Решение: разбивай работу на части (chunking), используй Web Workers, применяй throttle/debounce для событий, виртуализируй большие списки, оптимизируй анимации с помощью transform, используй code splitting. Главное правило: не давай JavaScript работать больше 50ms подряд - это даст браузеру время обновить UI и остаться на 60fps.