Почему сайт тормозит даже на быстром интернете

Вы открываете сайт, смотрите на счётчик загрузки и думаете: зачем ждать? Интернет быстрый, сервер нормальный, дизайн не перегружен. Но браузер всё равно грузит что-то три секунды. Скорее всего, дело в том, что сайт загружает всё сразу — включая то, что пользователь никогда не увидит.

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

Два инструмента помогают это исправить: lazy loading и code splitting.

Что такое code splitting

Code splitting — это разделение JavaScript-бандла на части. Вместо одного огромного файла браузер получает несколько маленьких и загружает только нужные.

Без code splitting:

bundle.js — 1.2 MB

С code splitting:

main.js — 120 KB (загружается сразу)
page-catalog.js — 340 KB (загружается при переходе в каталог)
page-checkout.js — 180 KB (загружается при оформлении заказа)
admin.js — 560 KB (загружается только для администраторов)

Пользователь, который зашёл на главную и ушёл, скачал 120 KB вместо 1.2 MB — в 10 раз меньше.

Как это работает в Webpack и Vite

В Webpack code splitting включается через динамический импорт:

// Статический импорт — код попадёт в основной бандл
import HeavyComponent from './HeavyComponent';

// Динамический импорт — будет отдельным чанком
const HeavyComponent = () => import('./HeavyComponent');

В React это выглядит так:

import { lazy, Suspense } from 'react';

const AdminPanel = lazy(() => import('./AdminPanel'));

function App() {
  return (
    <Suspense fallback={<div>Загрузка...</div>}>
      <AdminPanel />
    </Suspense>
  );
}

Vite делает code splitting автоматически — каждый динамический импорт становится отдельным чанком. Настраивать почти ничего не нужно.

Route-based splitting — самый простой способ начать

Если у вас SPA (React, Vue, Angular), начните с разделения по маршрутам. Каждая страница — отдельный чанк:

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const Catalog = lazy(() => import('./pages/Catalog'));
const Checkout = lazy(() => import('./pages/Checkout'));

function App() {
  return (
    <Suspense fallback={<Loader />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/catalog" element={<Catalog />} />
        <Route path="/checkout" element={<Checkout />} />
      </Routes>
    </Suspense>
  );
}

Пользователи, которые никогда не доходят до оформления заказа, не скачивают его код.

Что такое lazy loading

Lazy loading — загрузка ресурсов только тогда, когда они нужны. Чаще всего это применяют к изображениям: не грузить картинку, пока она за пределами экрана.

Lazy loading изображений

Нативный способ — атрибут loading="lazy" в HTML:

<img src="photo.jpg" loading="lazy" alt="Описание" width="800" height="600">

Браузер сам решит, когда загрузить картинку — обычно когда до неё остаётся 1000–1500 пикселей прокрутки. Поддержка: Chrome 76+, Firefox 75+, Safari 15.4+.

Важный момент: всегда указывайте width и height. Без них браузер не знает размер картинки до загрузки и не может зарезервировать место — получится Cumulative Layout Shift (прыгающий контент), который портит Core Web Vitals.

Что не нужно грузить лениво:

  • Картинки выше fold — те, что видны сразу при открытии
  • Логотип
  • Hero-изображение

Для них просто не указывайте атрибут или используйте loading="eager".

Lazy loading через Intersection Observer

Если нужно больше контроля:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
}, { rootMargin: '200px' });

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

rootMargin: '200px' — начать загрузку за 200px до того, как элемент появится на экране. Убирает эффект белого прямоугольника при быстрой прокрутке.

Lazy loading компонентов

Та же идея работает для UI-элементов. Модальное окно, которое открывается при клике — зачем грузить его код при загрузке страницы?

// Vue 3
const Modal = defineAsyncComponent(() => import('./Modal.vue'));
// Загружается только когда пользователь нажал кнопку

Особенно актуально для тяжёлых зависимостей. Если в модалке используется редактор кода или библиотека для работы с PDF — их код весит несколько сотен килобайт. Грузить это нужно только тогда, когда пользователь реально открыл окно.

Реальные цифры: что даёт оптимизация

Конкретный пример: интернет-магазин одежды.

До оптимизации:

  • Один бандл: 1.8 MB
  • First Contentful Paint (FCP): 3.2 сек
  • Time to Interactive (TTI): 5.8 сек
  • Lighthouse Performance: 42/100

После code splitting и lazy loading изображений:

  • Начальный бандл: 210 KB
  • FCP: 1.1 сек
  • TTI: 2.4 сек
  • Lighthouse Performance: 78/100

Время до интерактивности упало с почти 6 секунд до 2.4. По данным Google, каждая дополнительная секунда загрузки увеличивает процент отказов на 7–12%. Для среднего магазина это десятки тысяч рублей недополученной выручки в месяц.

Как анализировать бандл

Прежде чем оптимизировать — нужно понять, что именно много весит.

Webpack Bundle Analyzer

npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [new BundleAnalyzerPlugin()]
};

Открывается интерактивная карта, где видно, какие модули занимают место. Часто оказывается, что 40% бандла — одна библиотека, которую можно заменить более лёгкой альтернативой.

Vite — rollup-plugin-visualizer

npm install --save-dev rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [visualizer({ open: true })]
};

Chrome DevTools Network

Самый быстрый способ: DevTools → Network → перезагрузить страницу → отсортировать по размеру. Сразу видно, что грузится и сколько весит. Смотрите на колонку Waterfall — там видно, что грузится параллельно, а что блокирует рендеринг.

Распространённые ошибки

Дробить слишком мелко

Code splitting создаёт HTTP-запросы. Если разбить приложение на 200 чанков по 5 KB — это хуже, чем один файл 1 MB. HTTP/2 позволяет параллельные запросы, но overhead всё равно есть. Оптимально: чанки от 30–50 KB и выше.

Не кэшировать правильно

Смысл code splitting частично в том, что чанки, которые не менялись, браузер берёт из кэша. Это работает только если в именах чанков есть хэш:

vendor.a1b2c3d4.js  ← хорошо, хэш меняется только при изменении контента
vendor.js           ← плохо, браузер не знает, изменился ли файл

В Webpack настраивается через output.filename, в Vite работает из коробки.

Lazy loading для всего подряд

Не каждый компонент нужно грузить лениво. Кнопка «Добавить в корзину» с задержкой загрузки — это хуже, чем если бы она просто была в бандле. Ленивая загрузка нужна для:

  • Тяжёлых компонентов: редакторы, графики, карты
  • Компонентов, которые видит меньшинство пользователей
  • Функционала за авторизацией
  • Модалок и сайдбаров

Забыть про prefetching

После загрузки основной страницы можно в фоне начать тянуть следующую:

// Webpack magic comment
const Catalog = lazy(() => import(/* webpackPrefetch: true */ './pages/Catalog'));

Браузер скачает файл с низким приоритетом. Когда пользователь кликнет — страница откроется мгновенно.

Разница: prefetch — вероятно понадобится позже, низкий приоритет; preload — нужно прямо сейчас, высокий приоритет.

CSS и шрифты тоже оптимизируются

CSS splitting

В больших проектах стили тоже разделяют по маршрутам. MiniCssExtractPlugin в Webpack или встроенные возможности Vite позволяют подключать CSS только для текущей страницы.

Шрифты

Подключение Google Fonts через <link> блокирует рендеринг. Используйте font-display: swap:

@font-face {
  font-family: 'MyFont';
  src: url('font.woff2') format('woff2');
  font-display: swap;
}

С swap браузер сначала показывает текст системным шрифтом, потом заменяет на загруженный. Пользователь видит контент сразу.

Изображения: форматы

Lazy loading решает вопрос «когда грузить». Отдельно стоит разобраться с «что грузить».

WebP весит в среднем на 25–35% меньше JPEG при том же качестве. AVIF ещё легче, но поддержка чуть хуже.

<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="Описание" loading="lazy" width="800" height="600">
</picture>

Браузер возьмёт первый поддерживаемый формат.

Когда это реально нужно

Всё вышесказанное не нужно для сайта-визитки на 5 страниц с парой картинок. Там особо нечего оптимизировать.

Code splitting и lazy loading актуальны когда:

  • SPA с множеством маршрутов
  • Много тяжёлых зависимостей: редакторы, графики, карты
  • Интернет-магазин с каталогом и сотнями изображений
  • Часть функционала доступна только авторизованным пользователям
  • Lighthouse Performance ниже 70

Для обычного корпоративного сайта на Next.js code splitting уже встроен, lazy loading изображений делается одним атрибутом. Базовая оптимизация занимает пару часов.

Для сложного React-приложения — это полноценная задача с анализом бандла, настройкой чанков и тестированием. Если заказываете разработку, уточните: делает ли студия code splitting по умолчанию или это нужно просить отдельно. В REEXY оптимизация загрузки входит в стандартный процесс — при разработке интернет-магазина от 10 000 ₽ изображения подключаются правильно, бандл не превращается в один монолитный файл.

Чек-лист быстрой проверки

Проверьте свой сайт прямо сейчас:

  1. Откройте Chrome DevTools → Network → перезагрузите страницу
  2. Посмотрите на общий размер загруженных ресурсов (строка внизу)
  3. Отфильтруйте по JS — если один файл больше 500 KB, нужен code splitting
  4. Отфильтруйте по Img — если все картинки грузятся сразу, нужен lazy loading
  5. Проверьте Lighthouse: F12 → вкладка Lighthouse → Analyze page load
  6. Проверьте наличие loading="lazy" у изображений ниже fold через вкладку Elements

Если Performance ниже 70 — есть конкретные точки для улучшения. Большинство из них поправляется за несколько часов работы.