Назад
Назад к заметкам
· 4 мин чтения

Архитектура компонентного дерева без каскадных ререндеров

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

reactperformancearchitecturestate-management

Если ваш бандл уже превысил 1 МБ, а страницы “залипают” при первом открытии, обычный import уже не спасет ситуацию. Ленивая загрузка компонентов позволяет отдавать только нужные куски кода. В статье разберем, как мы внедрили эти механизмы в прод-приложении, какие подводные камни встретились и какие практики стоит добавить в арсенал.

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


Ленивая загрузка компонентов: зачем и как

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

// HeavyComponent.tsx большой блок с графикой и зависимостями
const HeavyComponent = React.lazy(() => import("./HeavyComponent"));

export default function Page() {
  return (
    <div>
      <h1>Главная страница</h1>
      <HeavyComponent />
    </div>
  );
}

Плюсы:

  • Меньший первоначальный бандл загружаются лишь критические зависимости.
  • Отложенный парсинг браузер начинает работать, пока остальные модули скачиваются.
  • Гибкая стратегия можно комбинировать с dynamic import в роутере.

Подробнее о разделении кода см. в статье о виртуализации списка.


Suspense как оболочка: UI-запаска и нюансы

React.lazy сам по себе не показывает индикатор загрузки. За это отвечает Suspense. Его задача отобразить запасной UI, пока асинхронный модуль скачивается.

import React, { Suspense } from "react";
import Spinner from "./Spinner"; // простой индикатор

const Dashboard = React.lazy(() => import("./Dashboard"));

export default function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Dashboard />
    </Suspense>
  );
}

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

Что часто упускают:

  1. Стилизация fallback не ограничиваться простой строкой, добавить анимацию.
  2. Таймауты при сетевых проблемах пользователь может ждать бесконечно. Можно написать собственный useTimeout-хук, меняющий fallback после N секунд.

Динамический импорт в роутинге: пример с React Router v6

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

import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Suspense } from "react";
import Loader from "./Loader";

const Home = React.lazy(() => import("./pages/Home"));
const Settings = React.lazy(() => import("./pages/Settings"));
const Profile = React.lazy(() => import("./pages/Profile"));

export default function Router() {
  return (
    <BrowserRouter>
      <Suspense fallback={<Loader />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/settings" element={<Settings />} />
          <Route path="/profile" element={<Profile />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}
РоутРазмер бандла (KB)Время первой отрисовки
/45120 ms
/settings78 (ленивая)85 ms после перехода
/profile62 (ленивая)90 ms после перехода

Отдельный lazy-модуль экономит более 30 KB на первом рендере. Для более тонкой работы с кешем смотрите материал о мемоизации компонентов.


Типичные проблемы и их обходные пути

При внедрении ленивой загрузки в большой код-базе сразу всплывают несколько “ловушек”.

  1. Ошибка “Component not found” часто появляется, когда импорт возвращает не default. Проверьте экспорт модуля.
  2. Серверный рендеринг React.lazy не работает в SSR без полифила. Мы перешли на loadable-components только для сервера.
  3. Потеря состояния при переключении между lazy-компонентами состояние может сбрасываться, если компонент размонтируется. Решение хранить глобальное состояние в контексте или использовать Redux.
ПроблемаПричинаВыходное решение
Component not foundНеправильный export defaultДобавить export default в целевом файле
SSR-неподдержкаReact.lazy работает только в клиентеИспользовать loadable-components или next/dynamic
Потеря UI-состоянияУдаление компонента из дереваПеренести состояние в Redux/Context

Предзагрузка и предзапросы

Иногда хочется избавиться от задержки даже при fallback. Для этого используют link rel="preload" или динамический импорт в useEffect.

import { useEffect } from "react";

export default function PreloadDashboard() {
  useEffect(() => {
    // Начинаем подгрузку заранее, пока пользователь читает главную страницу
    import("./Dashboard");
  }, []);
  return null;
}

Такой “потихуюшный” импорт гарантирует, что к моменту перехода на /dashboard файл уже в кэше, а fallback почти не виден. Комбинация preload и React.lazy стандартный паттерн в наших прод-проектах.


FAQ / чек-лист

  • Не забывайте про fallback без него пользователь увидит пустой экран.
  • Проверяйте, что модуль экспортирует default.
  • Тестируйте в прод-режиме минификация может менять пути и влиять на lazy-импорты.
  • Добавляйте предзагрузку для “горячих” роутов, чтобы убрать паузы.
  • Не вкладывайте Suspense без необходимости: каждый вложенный fallback усложняет UX.

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

Готов начать?

Выбери удобный способ связи — напиши напрямую или оставь заявку

Написать в Telegram

Отвечу в течение пары часов