Главная / Статьи

Что оптимизирует компилятор React и что оставляет на вашей ответственности

Поймите, какие задачи по оптимизации автоматизирует React Compiler, почему работа с медленными API и большими пакетами остается вашей ответственностью, и как безопасно внедрить его в существующую базу кода React.

Издатель: REACTAPP.TOP6 мин чтения1421 слов

Источники: React Compiler Won’t Fix a Slow React App

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

От списка шагов мемоизации к автоматической мемоизации

Долгое время советы по улучшению производительности React сводились к короткому списку инструментов:

useMemo()
useCallback()
React.memo()

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

Компилятор React меняет этот стандарт. Версия 1.0 считается стабильной, и команда React рекомендует новым разработчикам позволять компилятору самостоятельно заниматься мемоизацией, вместо того чтобы вручную использовать useMemo и useCallback. Для получения последних рекомендаций по применению обратитесь к актуальной документации React.

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

Что на самом деле делает компилятор

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

function ProductList({ products }) {
  const visible = products.filter(
    product => product.inStock
  );

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

  return <List items={visible} />;
}

До появления компилятора разработчик, ориентированный на производительность, часто явно кэшировал фильтр, чтобы List получала тот же самый указатель на массив до момента изменения переменной products:

const visible = useMemo(
  () => products.filter(
    product => product.inStock
  ),
  [products]
);

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

Однако обратите внимание на контекст этого описания: каждый элемент списка является конструкцией React.

Ментальная модель: оптимизатор отрисовки

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

React Compiler
                      │
          ┌───────────┴───────────┐
          ▼                       ▼
    Component work          Hook calculations
          │                       │
          ▼                       ▼
     memoization             memoization
          │                       │
          └───────────┬───────────┘
                      ▼
              fewer unnecessary
                 React updates

Уменьшение количества ненужных перерисовок может действительно принести пользу. Однако важной частью диаграммы является то, что она не показывает.

Медленные бэкенды находятся вне его досягаемости

Рассмотрим асинхронный компонент, который ожидает ответ от API перед отображением панели управления:

async function Dashboard() {
  const data = await fetch(
    "https://api.example.com/dashboard"
  );

Как только данные поступают, они передаются в интерфейс пользователя:

  return <DashboardUI data={data} />;
}

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

Browser
   │
   ▼
React
   │
   ▼
API request ──────────► 800ms
   │
   ▼
Response
   │
   ▼
React render

Мемоизация используется практически исключительно на последнем этапе. Если большая часть времени уходит на ожидание сети, вы оптимизируете не тот уровень; решениями могут стать более быстрые запросы, кэширование, меньший объем ответов или параллельное загрузчение данных. Чтобы отделить время сетевых операций от времени работы клиента, ознакомьтесь с Диагностика проблем с производительностью React за пределами времени ответа API.

JavaScript, который не следовало включать в пакет

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

Initial page
     │
     ├── React
     ├── UI
     ├── Chart library
     ├── Editor
     └── Analytics SDK

Компилятор не удаляет зависимости; каждый байт все равно загружается, парсится и выполняется. Помогает разделение кода, например загрузка экрана аналитики по требованию:

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

Теперь код загружается только тогда, когда функция действительно отрисовывается, а окружающие его границы Suspense показывают запасной вариант во время загрузки. Это решение, связанное с архитектурой пакетов, и никакая мемоизация не может заменить его.

Где возникают недопонимания

Частая ситуация: приложение работает медленно, кто-то включает компилятор, и команда ожидает именно такого результата:

Slow app
   │
   ▼
React Compiler
   │
   ▼
Fast app

Это слишком упрощено. Медленная работа может иметь несколько причин, и компилятор устраняет лишь одну из них:

SLOW APP
                       │
       ┌───────────────┼───────────────┐
       ▼               ▼               ▼
    React work      Network          Browser
       │               │               │
       ▼               ▼               ▼
   Compiler        API/cache       JS/rendering
       │               │               │
       ▼               ▼               ▼
   Optimized       Engineer         Engineer

Компилятор — это лишь один инструмент из набора средств для улучшения производительности, а не сам набор средств. Для решения проблем с задержками в сети и затратами браузера, такими как обработка скриптов и формирование лейаута, всё равно требуется участие человека.

Стоит ли удалять существующие вызовы useMemo?

Не автоматически. В новом коде официальным стандартом является использование мемоизации компилятором. Что касается существующего кода, React рекомендует оставить текущую мемоизацию без изменений или удалять её только после тщательных тестов. Причина заключается в поведении кода, а не только в производительности: значение, помеченное как мемоизированное и используемое в качестве зависимости Effect, определяет момент повторной обработки этого Effect, поэтому изменение способа мемоизации может повлиять на время выполнения побочных эффектов.

Поэтому сдерживайте желание проводить такую полную очистку кодовой базы:

useMemo      → DELETE
useCallback  → DELETE
React.memo   → DELETE

Более безопасный подход — постепенные изменения с измерением результатов на каждом этапе:

Existing app
     │
     ▼
Enable compiler
     │
     ▼
Run tests
     │
     ▼
Profile
     │
     ▼
Remove unnecessary
manual memoization
carefully

Компилятор предназначен для того, чтобы в будущем сделать ручную мемоизацию менее необходимой, а не для того, чтобы сразу объявить все существующие оптимизации неверными.

Компилятор как инструмент проверки качества кода

Ещё одно преимущество, которому уделяется мало внимания: компилятор включает этапы проверки, основанные на правилах React, а его интеграция с инструментами проверки кода позволяет выявлять подобные проблемы:

set-state-in-render
set-state-in-effect
unsafe ref access

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

Сначала проанализируйте профиль, затем выберите решение

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

SLOW UI
                 │
                 ▼
               PROFILE
                 │
        ┌────────┼────────┐
        ▼        ▼        ▼
      React    Network   Browser
       work      │        │
        │        │        │
        ▼        ▼        ▼
    Compiler   API/cache  JS/render
        │
        ▼
   Targeted fix

Затем подберите решение в соответствии с тем, что показал профилятор:

  • Ненужные перерисовки компонентов React: здесь может помочь React Compiler.
  • Медленный API: улучшите конечную точку или стратегию получения данных.
  • Чрезмерно большой объем данных: сокращайте количество JavaScript-кода путем его разделения и уменьшения количества зависимостей.
  • Дорогостоящие обновления DOM: пересмотрите способ отрисовки, например, с помощью виртуализации длинных списков.
  • Тяжелый скрипт от стороннего разработчика: удалите его, отложите выполнение или изолируйте.
  • Основные выводы

    В старой модели требовалось указывать React, что следует сохранять в кэше; в новой модели вы можете писать корректный код на React и доверять компилятору большую часть работы по отрисовке. Это повышает ценность одного навыка — умения понимать, куда на самом деле тратится время.

    • Компилятор оптимизирует ту работу, которую он понимает, а именно отрисовку в React и вычисления хуков.
    • Он не может уменьшить размер ответа от API, ускорить запрос к базе данных, удалить скрипт от стороннего разработчика или сжать изображение размером 5 МБ.
    • Он не может спасти архитектуру, которая создает ненужную работу везде.
    • В существующем коде включите его, протестируйте, проанализируйте производительность, и только затем удаляйте ручную мемоизацию.
    • Рассматривайте его диагностическую информацию как бесплатные отчёты об ошибках, связанных с нарушениями правил React.

    Не спрашивайте, включён ли компилятор, а где приложение тратит время; именно для оптимизации этого времени и существует компилятор.