Сделать React-приложение “прогрессивным” не значит навесить больше технологий. Обычно это значит обратное: сначала убрать лишнюю стоимость интерфейса, а уже потом аккуратно добавить то, что действительно улучшает пользовательский опыт.

Плохая стратегия здесь хорошо знакома: команда сразу думает про SSR, service worker и сложные оптимизации, хотя приложение еще не измерено, рендерит слишком много, грузит слишком большой бандл или ломает базовую доступность. В итоге проблем становится больше, а не меньше.

Ниже разберем набор решений, которые реально двигают React-приложение вперед: измерение, снижение стоимости JavaScript, контроль повторных рендеров, осознанная стратегия рендеринга и аккуратная работа со стилями и доступностью.

Начинай с измерения

Оптимизация без измерения обычно заканчивается красивым объяснением и слабым результатом. Для React минимальный практический набор такой:

  • панель Performance в Chrome DevTools для браузерной картины целиком;
  • React DevTools Profiler для поиска дорогих деревьев компонентов;
  • легкий продакшн-мониторинг через Profiler, если проблема проявляется только у реальных пользователей.

Минимальный пример Profiler:

import { Profiler } from "react";

function onRender(
  id: string,
  phase: "mount" | "update" | "nested-update",
  actualDuration: number,
  baseDuration: number,
) {
  console.log({ id, phase, actualDuration, baseDuration });
}

export function App() {
  return (
    <Profiler id="SearchResults" onRender={onRender}>
      <SearchResults />
    </Profiler>
  );
}

Такой код не решает проблему сам по себе, но сразу показывает, какой участок дерева действительно дорогой.

Сокращай объем JavaScript

Самая частая проблема фронтенд-приложения не в том, что React “медленный”, а в том, что браузеру приходится парсить, загружать и выполнять слишком много JavaScript.

Первое, что стоит сделать, - отделить тяжелые экраны и функции в отдельные чанки:

import { lazy, Suspense } from "react";

const CheckoutPage = lazy(() => import("./CheckoutPage"));

function AppRoutes() {
  return (
    <Suspense fallback={<LoadingPage />}>
      <CheckoutPage />
    </Suspense>
  );
}

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

  • делай route-level code splitting;
  • не тащи тяжелые зависимости в общий initial bundle;
  • откладывай редкие сценарии до момента, когда пользователь реально до них дошел.

Если приложение большое, code splitting обычно дает больше пользы, чем преждевременная микрооптимизация отдельных компонентов.

Убирай лишние повторные рендеры

React-приложение редко упирается только в один медленный компонент. Чаще проблема в каскаде лишних обновлений.

Базовые приемы здесь такие:

  • мемоизировать действительно тяжелые листья;
  • держать состояние ближе к месту использования;
  • не передавать каждый раз новые объектные литералы и функции без причины;
  • виртуализировать длинные списки.

Например, memo уместен там, где компонент действительно дорогой и часто получает одинаковые props:

import { memo } from "react";

type ProductRowProps = {
  product: {
    id: string;
    title: string;
  };
  onSelect(id: string): void;
};

export const ProductRow = memo(function ProductRow({
  product,
  onSelect,
}: ProductRowProps) {
  return (
    <button onClick={() => onSelect(product.id)}>
      {product.title}
    </button>
  );
});

Важно не превращать memo, useMemo и useCallback в религию. Они полезны после измерения, а не вместо него.

Выбирай стратегию рендеринга осознанно

React сам по себе не отвечает на вопрос, как именно страница должна появиться у пользователя. Это архитектурный выбор:

  • CSR подходит для внутренних интерфейсов и приложений, где первый экран не критичен для SEO;
  • SSG хорошо работает для контентных и маркетинговых страниц;
  • SSR нужен там, где важны свежие данные и быстрый первый ответ;
  • streaming SSR полезен, когда приложение большое и UI можно отдавать по частям.

Ошибочный ход здесь - выбирать стратегию по моде. Правильный вопрос другой: что для этого экрана важнее всего?

  • скорость первого показа;
  • SEO;
  • свежесть данных;
  • цена гидрации;
  • сложность инфраструктуры.

Если ответа нет, начни проще. Лишний SSR обходится дороже в поддержке, чем это обычно кажется на старте.

Не ломай доступность и стоимость CSS

Приложение не станет “прогрессивным”, если оно быстро грузится, но плохо читается скринридером, не работает с клавиатуры или мигает без стилей до момента гидрации.

Минимальный набор правил:

  • используй семантический HTML до того, как добавляешь role;
  • проверь таб-цикл, фокус и видимость интерактивных элементов;
  • не делай runtime CSS-in-JS выбором по умолчанию без причины;
  • следи за стоимостью шрифтов, изображений и критического CSS так же внимательно, как за JavaScript.

Если у тебя есть выбор, то для обычного продуктового интерфейса сначала стоит рассмотреть CSS Modules или utility-first подход, а runtime CSS-in-JS оставить для случаев, где действительно нужна динамика или библиотечный API.

Итог

Прогрессивный React - это не набор модных технологий, а порядок принятия решений. Сначала измерение, потом уменьшение стоимости JavaScript, затем контроль re-render и только после этого более дорогие архитектурные шаги вроде сложного SSR или агрессивного кэширования.

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