React course
  • Компоненты и коллекции
  • TypeScript
  • Стилизация
  • События и состояния
  • Формы
  • Жизненный цикл
  • Функциональные vs классовые компоненты
  • Основы Redux
  • Redux Toolkit
  • Асинхронный Redux
  • Селекторы
  • React Router
  • Code splitting
  • Паттерны и контекст
  • Анимация
Powered by GitBook
On this page
  • 1. Code splitting
  • 1.1. Динамический импорт в ESM
  • 2. Route-based и Component-based splitting
  • 2.1. asyncComponent HOC
  • 2.2. Имена чанков
  • 3. React Loadable
  • 4. React.lazy версии 16.6+
  • 5. Дополнительные материалы

Was this helpful?

Code splitting

PreviousReact RouterNextПаттерны и контекст

Last updated 4 years ago

Was this helpful?

1. Code splitting

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

Секция приложения может импортировать большое количество компонентов, которые не нужны при первой загрузке. Необходимо разбивать бандл на отдельные файлы (chunks, чанки) и загружать их динамически, только когда это необходимо.

1.1. Динамический импорт в ESM

// Статический импорт, всегда в начале файла
import moduleA from './path/to/module-a';

/*
 *  Динамический импорт, где угодно
 * Путь к модулю должен быть полный, без выражений
 */
const loadModuleB = () => import('./path/to/module-b');

/*
 * Можно использовать в любом месте, к примеру при событии
 * import() возвращает обещание
 */
loadModuleB().then(module => {
  // Объект модуля с полем default - дефолтный экспорт
  console.log(module);
});

Create React App поддерживает разделение кода и позволяет динамически импортировать части приложения используя import().

2. Route-based и Component-based splitting

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

Но есть еще один вариант. Раут это просто компонент. Поэтому можно разделять код на уровне компонентов, а не маршрутов. Какая в этом выгода?

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

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

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

2.1. asyncComponent HOC

  • Функция asyncComponent принимает объект настроек из двух свойств: loader - функция, которая при вызове будет динамически импортировать компонент, loading - компонент который будем показывать пока идет HTTP-запрос.

  • В componentDidMount мы просто вызываем функцию loader, и сохраняем динамически загруженный компонент в состояние.

  • После чего делаем рендер по условию, где рендерим загруженный компонент, если он уже загрузился. В противном случае рендерим компонент Loading.

// asyncComponent.js

const asyncComponent = ({ loader, loading: Loading }) => {
  return class AsyncComponent extends Component {
    state = {
      component: null,
    };

    async componentDidMount() {
      const { default: component } = await loader();

      this.setState({ component });
    }

    render() {
      const { component: LoadedComponent } = this.state;

      return LoadedComponent ? (
        <LoadedComponent {...this.props} />
      ) : (
        <Loading />
      );
    }
  };
};

Теперь используем эту функцию для динамической загрузки страниц.

const AsyncHome = asyncComponent({
  loader: () => import('./pages/Home'),
  loading: Loader,
});

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

Может показаться странным, что мы передаем функцию. Почему бы просто не передать строку './pages/Home', а затем выполнить динамический импорт внутри asyncComponent?

Это связано с тем, что мы хотим явно указать компонент, который мы динамически импортируем. Основываясь на этом, Webpack разбивает наше приложение. Он смотрит на эти импорты и генерирует необходимые чанки (куски, chunks).

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

<Route path="/" exact component={AsyncHome} />

Каждый из файлов .chunk.js - это вызовы import(), асинхронно загружаемые компоненты.

2.2. Имена чанков

Если необходимо дать чанкам вменяемые имена, делается это так:

const AsyncHome = asyncComponent({
  loader: () => import('./pages/Home' /* webpackChunkName: "home-page" */),
  loading: Loader,
});

3. React Loadable

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

React Loadable - небольшая библиотека, которая упрощает разделение компонентно-ориентированного кода.

Loadable - компонент высшего порядка, который позволяет легко разделить код на уровне компонентов.

Добавим пакет в проект.

npm install react-loadable

Достаточно заменить asyncComponent на Loadable и все продолжит работать как и раньше.

Одно но, можно улучшить компонент Loading, потому что react-loadable прокидывает ему несколько очень полезных свойств.

const Loading = ({ error, timedOut, pastDelay, retry }) => {
  if (error) {
    return (
      <div>
        Error! <button onClick={retry}>Retry</button>
      </div>
    );
  }

  if (timedOut) {
    return (
      <div>
        Taking a long time... <button onClick={retry}>Retry</button>
      </div>
    );
  }

  if (pastDelay) {
    return <div>Loading...</div>;
  }

  return null;
};

4. React.lazy версии 16.6+

Официальный API который позволяет рендерить динамически загружаемый компонент. Заменит react-loadable.

React.lazy(loader);

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

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

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

const MyComponent = () => (
  <Suspense fallback={<div>Loading...</div>}>
    <AsyncComponent />
  </Suspense>
);

Если во время ренедера MyComponent, компонент AsyncComponent еще не загружен, необходимо показать фолбек. Для этого используется компонент Suspense. Проп fallback принимает любой React-элемент. Susupense можно поместить в любом месте над асинхронным компонентом, даже оборачивая целую группу.

5. Дополнительные материалы

image

Dynamic import()
proposal-dynamic-import
code-splitting-specify-chunk-name
Репозиторий react-loadable
Документация React.lazy
Code Splitting in React Using React Loadable
Optimizing Performance
JavaScript Start-up Optimization