Производительность веб-приложения: где тормоза, как их найти и как исправить

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

В этой статье разберем:

  1. Что именно мешает приложению быть быстрым (и как это проверить).
  2. Как оптимизировать загрузку (не только CSS/JS, но и сетевые запросы, кэширование, CDN).
  3. Как ускорить рендер (Critical CSS, defer, lazy-loading, виртуальные списки).
  4. Что ломает производительность в продакшене (и как этого избежать).
  5. Как внедрить оптимизацию в процесс разработки (без лишних докеров и сложных конфигов).

Проблема: почему приложение тормозит (и как это проверить)

Обычно проблемы с производительностью проявляются в трех местах:

  • Загрузка ресурсов (HTML, CSS, JS, шрифты, изображения).
  • Рендеринг страницы (время до первого контента, взаимодействия).
  • Взаимодействие (отклик на клики, прокрутку, анимации).

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

1. Lighthouse (Chrome DevTools)

Запустите в браузере F12Lighthouse → выберите "Performance". Получите отчет с оценкой и конкретными проблемами:

# Запуск через CLI (нужен Node.js)
npx lighthouse https://ваш-сайт.ru --chrome-flags="--headless" --output=html --output-path=report.html

Что искать в отчете:

  • First Contentful Paint (FCP) > 1.8с — пользователь видит пустоту.
  • Time to Interactive (TTI) > 3.8с — приложение не откликается на действия.
  • Total Blocking Time (TBT) > 200мс — тормозит при прокрутке/кликах.

2. WebPageTest

Бесплатный сервис с геолокацией тестов. Запустите:

# Пример команды для теста из США
webpagetest --location=US:Seattle --test https://ваш-сайт.ru

Ключевые метрики:

  • Start Render (время до первого визуального контента).
  • Fully Loaded (время до полной загрузки).
  • Filmstrip (визуальная анимация загрузки — покажет, где "подвисания").

3. Real User Monitoring (RUM)

Если у вас уже есть трафик — используйте:

  • Google Analytics 4 (встроенные метрики FCP, LCP).
  • Sentry Performance (для отслеживания JS-тормозов).
  • New Relic (для комплексного мониторинга фронтенда и бэкенда).

Пример конфига для Sentry:

// sentry.init.js
Sentry.init({
  dsn: "ВАШ_DSN",
  integrations: [
    new Sentry.BrowserTracing({
      tracingOrigins: ["https://ваш-сайт.ru"],
    }),
    new Sentry.Replay(),
  ],
  tracesSampleRate: 1.0,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
});

Практика: как ускорить загрузку (и не ломать кэш)

Самая частая ошибка — оптимизировать фронтенд, но не думать о сетевых запросах, кэше и серверной части. Вот проверенные методы:

1. Оптимизация ресурсов

  • Изображения: Используйте WebP вместо JPEG/PNG и сервисную оптимизацию (например, Cloudinary или ImageKit).

    <!-- Пример с lazy-loading и WebP -->
    <img
      src="placeholder.jpg"
      data-src="https://res.cloudinary.com/demo/image/upload/w_800,q_auto:good,f_auto/photo.jpg"
      loading="lazy"
      width="800"
      height="600"
      alt="Описание"
    >
    

    Инструмент для проверки: Squoosh — сжимает изображения без потери качества.

  • Шрифты: Загружайте только нужные символы и используйте font-display: swap:

    @font-face {
      font-family: 'Roboto';
      src: url('roboto.woff2') format('woff2');
      font-display: swap;
    }
    
  • CSS/JS: Используйте defer для JS и preload для критических ресурсов:

    <!-- Critical CSS в head -->
    <link rel="preload" href="critical.css" as="style" onload="this.rel='stylesheet'">
    
    <!-- Некритический JS с defer -->
    <script src="analytics.js" defer></script>
    

2. Кэширование и CDN

  • Настройте Cache-Control на сервере:
    # Пример конфига для Nginx
    location ~* \.(jpg|jpeg|png|gif|webp|svg|ico|woff2?|ttf|eot|otf)$ {
      expires 1y;
      add_header Cache-Control "public, immutable";
      access_log off;
    }
    
  • Используйте CDN (Cloudflare, Fastly, AWS CloudFront) для статики. Пример конфига Cloudflare:
    {
      "rules": [
        {
          "action": "cacheLevel",
          "value": "cacheEverything",
          "conditions": [
            {
              "operator": "matches",
              "key": "url",
              "value": "*.{jpg,png,css,js,webp}"
            }
          ]
        }
      ]
    }
    

3. HTTP/2 и HTTP/3

  • Включите HTTP/2 на сервере (поддерживает мультиплексирование запросов):
    listen 443 ssl http2;
    
  • Для HTTP/3 используйте QUIC (например, через Cloudflare или Caddy):
    # Пример конфига Caddy
    ваш-сайт.ru {
      tls {
        alpn http/3
      }
    }
    

Примеры: как оптимизировать рендер

Рендер — это когда браузер начинает отрисовывать страницу. Если он тормозит, пользователь видит "серый экран смерти". Вот как это исправить:

1. Critical CSS

Выделите CSS, необходимый для отображения выше складки (above-the-fold). Инструменты:

  • Critical (Node.js):
    npx critical https://ваш-сайт.ru > critical.css
    
  • Penthouse (для SASS/LESS).

Пример встроенного Critical CSS:

<style>
  /* Сгенерированный Critical CSS */
  body { font-family: Roboto; }
  .hero { background: #fff; }
  /* ... */
</style>
<link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'">

2. Lazy-loading и виртуальные списки

  • Для изображений и iframe:
    <img src="image.jpg" loading="lazy" alt="...">
    
  • Для длинных списков (например, таблиц данных) используйте виртуальную прокрутку:
    // Пример с react-window
    import { FixedSizeList as List } from 'react-window';
    
    const Row = ({ index, style }) => (
      <div style={style}>Элемент {index}</div>
    );
    
    const VirtualizedList = () => (
      <List
        height={500}
        itemCount={10000}
        itemSize={50}
        width="100%"
      >
        {Row}
      </List>
    );
    

3. Code Splitting и Dynamic Imports

Разбивайте код на чанки и загружайте их по требованию:

// В React
import React, { Suspense, lazy } from 'react';

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

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

Ошибки: что убивает производительность (и как этого избежать)

Ошибка Причина Как исправить
Блокирующий JavaScript Скрипты загружаются в <head> Использовать defer или async
Неоптимизированные изображения JPEG/PNG без сжатия, большие размеры Конвертировать в WebP, использовать CDN
Избыточные ререндеры Частые изменения DOM/state Использовать React.memo, useMemo
Без кэша для статики Cache-Control: no-cache Настроить кэш на год для статики
Слишком много HTTP-запросов Необдуманные API-вызовы Объединять запросы, использовать GraphQL
Блокирующий CSS Внешние стили без preload Выносить критический CSS в <head>
Неиспользуемые полифилы Подключение старых библиотек Удалять ненужные зависимости (npm prune)

Вывод: как внедрить оптимизацию в процесс

Производительность — это не разовая задача, а часть DevOps-процесса. Вот как сделать это рабочим:

  1. Автоматизируйте тесты Добавьте Lighthouse в CI/CD (например, GitHub Actions):

    # .github/workflows/performance.yml
    name: Performance Check
    on: push
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
          - run: npx lighthouse https://ваш-сайт.ru --chrome-flags="--headless" --output=json
          - run: |
              result=$(npx lighthouse https://ваш-сайт.ru --chrome-flags="--headless" --output=json)
              performanceScore=$(echo $result | jq '.categories.performance.score')
              if [ $(echo "$performanceScore < 90" | bc) -eq 1 ]; then
                echo "Performance score is too low: $performanceScore"
                exit 1
              fi
    
  2. Настройте бюджет веса Используйте Webpack Bundle Analyzer:

    npm install --save-dev webpack-bundle-analyzer
    
    // webpack.config.js
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    
    module.exports = {
      plugins: [
        new BundleAnalyzerPlugin({
          analyzerMode: 'static',
          reportFilename: './dist/bundle-report.html',
        }),
      ],
    };
    

    Цель: Держать общий вес пакета < 500 КБ (для мобильных).

  3. Объедините фронтенд и бэкенд-метрики

    • Отслеживайте TTFB (Time to First Byte) с серверной стороны (например, через New Relic).
    • Сравнивайте FCP (фронтенд) и TTFB (бэкенд) — если TTFB > 300мс, проблема на сервере.
  4. Обучение команды

    • Добавьте проверку производительности в PR-ревью (например, "Добавил ли ты loading="lazy" к изображениям?").
    • Организуйте внутренний "Performance Hackathon" с наградами за лучшие оптимизации.

Итог: три ключевых правила

  1. Измеряй всё. Без метрик оптимизация — это гадание.
  2. Начинай с сетевых запросов. 80% проблем — в том, как и что загружается.
  3. Не жертвуй UX ради краткосрочных оптимизаций. Если пользователь видит "скелетон-скрин" 2 секунды — это нормально, если после этого всё грузится быстро.

Практическое задание:

  1. Запустите Lighthouse для своего приложения.
  2. Найдите одну метрику, которая хуже средней (например, CLS > 0.1).
  3. Исправьте её за один день (например, добавьте loading="lazy" к изображениям или оптимизируйте шрифты).
  4. Повторите тест — результат должен улучшиться на 30%+.

Производительность — это не фича, а основа. Если вы её игнорируете, пользователи уйдут, а конкуренты обгонят вас на старте.