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

Содержание серии

Как сделать бесконечно ленивым: Ультимативный гайд.

Ранее мы обсуждали самые основы ленивой загрузки и то, почему она вообще важна. А в этой статье мы рассмотрим следующее:

  1. Как бандлеры (например, Webpack) анализируют файлы исходного кода, строят деревья зависимостей и генерируют файлы для сборки.

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

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


Эксперименты с Webpack и деревьями зависимостей

Давайте начнём наше глубокое погружение с нескольких мысленных экспериментов. Мы будем использовать pet приложение из Части 1. Сейчас оно состоит из 3 JavaScript-файлов: изначальный (initial chunk) и два лениво загружаемых чанка, каждый из которых отвечает за свою страницу.

Эксперимент №1. Общий компонент

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

Chapter1.tsx
import React from 'react';
import { LargeComponent } from '../../components/large';

export default () => (
  <>
    <section className="page">
      <h2 style={{ margin: 'auto' }}>Chapter 1</h2>
      <LargeComponent />
    </section>

    <Content />
  </>
);

function Content() {
  // 2000 lines of code
}
Chapter2.tsx
import React from 'react';
import { LargeComponent } from '../../components/large';

export default () => (
  <>
    <section className="page">
      <h2 style={{ margin: 'auto' }}>Chapter 2</h2>
      <LargeComponent />
    </section>

    <Content />
  </>
);

function Content() {
  // 2000 lines of code
}

Теперь, вопрос для вас: как вы считаете, сколько файлов будет сгенерировано в сборке нашего сайта и как эти файлы будут скачиваться при открытии страниц Chapter1 and Chapter2?

Я предполагаю, что этот вопрос был довольно простым. Webpack начнет генерировать 4 файла.

А поведение браузера будет следующим. Когда мы попытаемся открыть страницу "Chapter 1", браузер скачает 3 чанка: изначальный, chapter1.chunk.js, a также новосгенерированный чанк. Похожая картина будет наблюдаться при открытии страницы "Chapter 2": изначальный, chapter2.chunk.js, и новосгенерированный чанк.

Как вы заметили, Webpack создал отдельный лениво загружаемый чанк сам по себе. Несмотря на то, что код large.tsx мы импортировали статично, т.е. не использовали никаких динамических импортов. Почему же это произошло?

Webpack проанализировал наше приложение и нашел, что 2 ленивых компонента используют общий компонент. И т.к. размер этого общего компонента достаточно велик, Webpack создал для него отдельный файл. Этот файл стал зависимостью для chapter1.chunk.js и chapter2.chunk.js, и если браузеру требуется скачать любой из этих файлов, новосгенирированный чанк тоже будет скачан, как зависимость.

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

ℹ️ Вам нужно использовать динамический импорт только для тех компонентов, которые сами по себе могут быть отображены лениво, но почти никогда для их общих зависимостей. Webpaсk способен эффективно разбивать сборку на отдельные чанки самостоятельно.

Эксперимент №2. Общий маленький компонент

Я несколько раз упомянул, что в прошлом примере все работает как описано, т.к. общий компонент был достаточно велик. Но что, если мы специально сделаем общий компонент достаточно маленьким? В чем будет разница?

Chapter1.tsx
import React from 'react';
import { TinyComponent } from '../../components/tiny';

export default () => (
  <>
    <section className="page">
      <h2 style={{ margin: 'auto' }}>Chapter 1</h2>
      <TinyComponent />
    </section>

    <Content />
  </>
);

function Content() {
  // 2000 lines of code
}
Chapter2.tsx
import React from 'react';
import { TinyComponent } from '../../components/tiny';

export default () => (
  <>
    <section className="page">
      <h2 style={{ margin: 'auto' }}>Chapter 2</h2>
      <TinyComponent />
    </section>

    <Content />
  </>
);

function Content() {
  // 2000 lines of code
}

Теперь, Webpack снова генерирует 3 файла: основной и по чанку на страницу.

Но почему? Что же случилось с TinyComponent? Правда может шокировать некоторых из вас: Webpack продублировал содержимое файла tiny.tsx и вставил его в оба чанка chapter1.chunk.js и chapther2.chunk.js. И это означает, что если вдруг мы заходим открыть обе страницы Chapter1 и Chapter2, код из tiny.tsx будет скачен дважды.

Это может вызывать у вас опасения, потому что в конечном итоге мы можем дважды загрузить один и тот же код. Но на самом деле это разумная стратегия. В реальных проектах у нас могут быть сотни исходных файлов, используемых в десятках ленивых чанков. Если бы Webpack создавал отдельный чанк для каждого крошечного файла исходного кода, мы могли бы получить сотни или даже тысячи сгенерированных файлов. Можете ли вы представить, что будет если браузер будет загружать сотни файлов для отображения одной страницы? Браузеры испытывают проблемы от большого количества запросов. Если ваш веб-сайт по-прежнему использует HTTP/1.1, каждый дополнительный файл будет существенно влиять на производительность. Но даже если вы используете протокол HTTP/3, у браузера есть собственные ограничения (fetchpriority, connection pool, etc.), которые могут привести к значительным задержкам, если у вас сотни файлов.

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

ℹ️  Для небольших общих зависимостей Webpack не будет генерировать отдельный чанк, а вместо этого скопирует и вставит свой код в другие существующие чанки. И это разумная стратегия. Вам не следует пытаться исправить ее.

Эксперимент №3. Оба общих компонента

Что же случится, если мы будем использовать оба общих компонента в обоих ленивых компонентах?

Chapter1.tsx
import React from 'react';

import { LargeComponent } from '../../components/large';
import { TinyComponent } from '../../components/tiny';

export default () => (
  <>
    <section className="page">
      <h2 style={{ margin: 'auto' }}>Chapter 1</h2>
      <TinyComponent />
      <LargeComponent />
    </section>

    <Content />
  </>
);
Chapter2.tsx
import React from 'react';

import { LargeComponent } from '../../components/large';
import { TinyComponent } from '../../components/tiny';

export default () => (
  <>
    <section className="page">
      <h2 style={{ margin: 'auto' }}>Chapter 2</h2>
      <TinyComponent />
      <LargeComponent />
    </section>

    <Content />
  </>
);

Сгенерированные чанки будут похожи на эксперимент №1.

Однако возникает вопрос, есть ли на этот раз какая-либо разница с TinyComponent? И ответ "да". Теперь код этого компонента будет включен только один раз: в дополнительно сгенерированный чанк - 595.chunk.js.

Так же, как и в примере №1, Webpack удалось создать отдельный чанк для хранения LargeComponent. Но на этот раз, во время анализа графика зависимостей, Webpack понял, что TinyComponent также является общей зависимостью и используется точно в тех же ленивых компонентах, что и LargeComponent. И поэтому Webpack объединил их в один файл.

И что я хотел бы подчеркнуть здесь, так это то, что Webpack достаточно умен, когда дело доходит до ленивой загрузки. Другими словами, мы можем положиться на Webpack в эффективной обработке отложенной загрузки. Но нам нужно немного помочь ему, чтобы повысить его эффективность.


Граф зависимостей

Итак, как это работает? Когда мы говорим о ленивой загрузке, мы должны начать думать о нашем коде с точки зрения графиков зависимостей. Webpack анализирует исходные файлы нашего приложения и генерирует графики зависимостей для кода. Затем он генерирует файлы JavaScript, каждый из которых содержит свой собственный график выполнения (execution dependency graph).

Давайте взглянем на график для нашего примера: две страницы с ленивой загрузкой и два общих компонента.

  • Всего у нас есть 6 файлов: large.tsx, tiny.tsx, Chapter1.tsx, Chapter2.tsx, Title.tsx и App.tsx. И файлы Chapter1.tsx и Chapter2.tsx загружаются лениво. У нас также есть 2 внешние зависимости: react и react-dom.

  • Webpack запускает анализ нашего приложения из entry файла: App.tsx. Он видит, что App.tsx содержит несколько статических импортов - react, react-dom и Title.tsx, - и включает их все в первоначально загруженный блок: main.js.

  • Затем он создает лениво загружаемые чанки для каждого из динамически импортируемых модулей: chapter1.js и chapter2.js.

  • Затем он анализирует все общие зависимости и видит, что "large.tsx" и "tiny.tsx" используются как в Chapter 1, так и в Chapter 2. Поэтому он объединяет их в один файл "shared.js".

    • Альтернативно, если бы мы использовали только "tiny.tsx", как в эксперименте № 2, содержимое tiny.tsx было бы скопировано и вставлено в оба ленивых чанка.

  • В конце Webpack создает новый график выполнения (execution dependency graph). По графику видно, что браузер может загрузить файл main.js сам по себе, но при этом обязан загружать shared.js каждый раз, когда ему нужно загрузить chapter1.js или chapter2.js

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


Берегись веса статических импортов

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

Давайте расширим наш пример. Представьте, что в Chapter 1 и Chapter 2 используется один и тот же компонент. Изначально этот компонент использовался только в Chapter1, а потому он был объявлен внутри Chapter1.tsx. И чтобы сэкономить время на разработку, мы решили экспортировать этот компонент из файла "Chapter1.tsx" вместо создания нового отдельного файла для хранения общего компонента. Это довольно распространенная практика. По крайней мере, по моему опыту.

Chapter1.tsx
import React from 'react';
import { LargeComponent } from '../../components/large';
import { TinyComponent } from '../../components/tiny';

export const AnotherCommonComponent = () => (
  <div style={{ color: 'blue', fontWeight: 500 }}>
    Another common component
  </div>
);

export default () => (
  <>
    <section className="page">
      <h2 style={{ margin: 'auto' }}>Chapter 1</h2>
      <TinyComponent />
      <LargeComponent />
      <AnotherCommonComponent />
    </section>

    <Content />
  </>
);

function Content() {
  // 2000 lines of code
}
Chapter2.tsx
import React from 'react';
import { AnotherCommonComponent } from '../chapter-1/Chapter1';
import { LargeComponent } from '../../components/large';
import { TinyComponent } from '../../components/tiny';

export default () => (
  <>
    <section className="page">
      <h2 style={{ margin: 'auto' }}>Chapter 2</h2>
      <TinyComponent />
      <LargeComponent />
      <AnotherCommonComponent />
    </section>

    <Content />
  </>
);

function Content() {
  // 2000 lines of code
}

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

Если мы посмотрим на сгенерированные файлы, то увидим, что по какой-то причине теперь генерируется только 3 файла. И по какой-то причине содержимое файла large.tsx включено в Chapter1.chunk.js.

Но почему? Давайте взглянем на дерево зависимостей, чтобы во всем разобраться.

График стал более запутанным. И чем более комплексен график, тем сложнее Webpack'у эффективно с ним работать. Webpack по-прежнему генерирует неизмененный main.js и по-прежнему генерирует файлы chapter1.js и chapter2.js. Однако, что изменилось, так это то, что когда Webpack проанализировал общие зависимости, он увидел, что теперь 3 файла (Chapter1.tsx, large.tsx и tiny.tsx) стали общими зависимостями между Chapter1.tsx и Chapter2.tsx, и объединил их в один файл. И поскольку у Chapter1.tsx уже есть собственный сгенерированный чанк, Webpack включает все эти 3 файла в Chapter1.chunk.js.

Потому график выполнения тоже существенно изменился. Когда пользователь открывает Chapter1, браузер загружает только соответствующий чанк. Однако, когда пользователь открывает Chapter 2, браузер загружает оба файла - Chapter1.chunk.js и Chapter2.chunk.js.

Будьте осторожны! Мы хотели уменьшить время загрузки. Но на самом деле, в случае с Chapter2, мы этого не добились. Браузер вынужден загружать тысячи строк кода из Chapter1.tsx, которые не будут использоваться на странице Chapter2.

Разве tree-shaking не помогает с ленивой загрузкой?

У вас может возникнуть вопрос: не должен ли tree-shaking исправить эту проблему? Ответ на этот вопрос прост: "нет, не должен". Когда мы работаем с ленивой загрузкой, tree-shaking следует рассматривать как отдельный механизм. Его цель не в разбивании кода на отдельные чанки, и происходит он еще до процесса создания чанков. С помощью tree-shaking бандлеры могут удалять неиспользуемые экспорты из файла. Например, если бы в Chapter 1 экспортировались другие функции, которые бы нигде не использовались, tree-shaking удалил бы их. Но поскольку оба AnotherSharedComponent и дефолтный экспорт Chapter1.tsx использовались, tree-shaking ничего не сделал.

ℹ️ Webpack оперирует файлами и не может рассматривать экспорт файла как отдельную зависимость.

А потому:

? Не полагайтесь на tree-shaking. Разделяйте файлы вручную, чтобы обеспечить эффективную ленивую загрузку.


Реальный пример плохого графа зависимостей

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

Этот график не отображет действительность на 100% Однако он подчеркивает реально существующие проблемы:

  • Компоненты из “Группы компонентов 1” по замыслу должны были использоваться только на страницах “Page1” и “Page2”.

    • При этом, Page1 и Page2 содержали некоторые уникальные компоненты, но хранились они в общем пространстве.

  • Компоненты из “Группы компонентов 2” и некоторые связанные с ними пакеты NPM должны были использоваться только на страницах “Page3”, “Page4” и “Page5”.

  • И каждая из страниц должна была быть максимально независимой.

Однако в реальности, когда загружалась любая из этих страниц, браузеру приходилось загружать почти все сгенерированные файлы. Даже если компонент из “группы компонентов 2” или “npm-пакет-1” не использовался на “Page1”, их код все равно загружался, когда пользователи открывали эту страницу. И эти проблемы существовали исключительно из-за того, что архитектура проекта негативно влияла на дерево зависимостей.


Держите ваш граф зависимостей в тонусе

Это был пример плохого графика зависимостей. Ленивая загрузка была использована , но это не уменьшило время загрузки настолько, насколько могло бы, потому что браузеру приходилось загружать и читать/компилировать много файлов. Но как мы можем избежать таких ситуаций? Честно говоря, учитывая, сколько файлов может быть в реальном проекте, представление реального графика зависимостей не поможет. Но мы можем формализовать несколько правил, которые бы помогли нам улучшить его.

Правило №1. Прими дефолты

Вы заметили, что "Page1.tsx" импортировал что-то из "Page4.tsx", как в одном из мысленных экспериментов? Чтобы отобразить страницу Page1, браузер обязан загрузить файл Page2.tsx и все его зависимости. И чтобы избежать этого, мы должны следовать этому правилу:

? Каждый динамически импортируемый файл должен содержать только 1 экспортируемый объект, например, только экспорт по умолчанию.

Если бы мы следовали этому правилу, мы не смогли бы импортировать что-либо еще из "Page4.tsx", кроме кода для отображения этой страницы.

Правило №2. Децентрализируй и избегай плохих паттернов

Далее на графике вы можете увидеть файлы, которые я назвал "facade". Что это за файлы такие? Под facade файлами я понимаю файлы, которые импортирует множество сущностей и оборачивают их в объекты и/или методы. В моем конкретном случае facade файлы использовались для хранения методов рендеринга для десятков компонентов в одном объекте:

import { First } from './component-1/first';
import { Second } from './component-1/second';
import { Third } from './component-1/third';
// other imports

export const renderComponentsGroup1 = {
  renderFirst: (prop1: string) => <First prop1={prop1} />,
  renderSecond: (prop2: string[]) => <Second prop2={prop2} />,
  renderThird: (prop3: number[]) => <Third prop3={prop3} />,
  // other dozens of methods
};

Facade файлы могут также оперировать служебными методами, классами и даже использовать различные NPM библиотеки. И они используются гораздо чаще, чем вы думаете. В принципе, любой Java-like паттерн проектирования, например, Factory, Orchestrator, Builder, Module, Service Layer, Repository и т.д., - все они могут быть примерами "фасадных" файлов. И проблема с такими файлами заключается в том, что мы должны быть крайне осторожны при их использовании.

Давайте возьмем renderComponentsGroup1 из нашего примера и используем только один его метод renderFirst на странице Page 1. Как было сказано ранее, Webpack оперирует файлами, и не способен анализировать разные экспорты в отдельности, и тем более он не способен анализировать методы одного объекта по отдельности. А потому, хоть мы и не используем renderSecond и renderThird, т.к. они являются методами renderComponentsGroup1, они тоже будут загружены при открыти страницы Page 1. Более того, все зависимости facade файла, включая Second.tsx, Third.tsx, другие компоненты, которые используются внутри renderComponentsGroup1, а также все зависимости Second.tsx, Third.tsx, и т.д., тоже станут зависимостью страницы Page 1. И даже если First.tsx сам по себе является крошечным файлом, поскольку мы отрисовали его с помощью renderComponentsGroup1, браузер будет обязан загрузить все дерево зависимостей facade файла.

И проблема здесь на самом деле не столько в использовании объектов, сколько в централизации вашей логики в одном файле. Потому что чем больше файл и/или его дерево зависимостей, тем хуже ситуация. Может быть 2 сценария такой централизации:

  1. файл, содержащий тысячи строк кода;

  2. файл, имеющий десятки зависимостей, каждая из которых имеет десятки своих зависимостей, и т.д.

И чтобы избегать обоих этих сценариев, старайтесь придерживаться следующего правила:

? Делайте ваши файлы как можно более атомарными и экспортируйте как можно меньше зависимостей.

Кроме того, я упомянул Java-style patterns, потому что они могут привести ко второму сценарию. Зачастую Front-End разработчики могут следовать таким шаблонам вслепую, без реальной цели и только потому, что "умные люди сказали, что так будет лучше". Я не говорю, что их использование всегда плохо. Но для Front-end разработки они не идеальны. Мы всегда должны дважды спросить себя, действительно ли использование такого шаблона вносит порядок в нашу кодовую базу, прежде чем применять их.

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

? Старайтесь избегать Java-style pattern'ов и других шаблонов, которые позволяют централизовать логику из нескольких файлов в одном файле.

Правило №3. Разбивай файлы

Я уже оговорился, что Webpack не способен анализировать экспорты одного файла как разные сущности, и что нам нужно самостоятельно разбивать файлы для построения эффективного графика зависимостей. Я также говорил, что идеальный способ создать чистое дерево зависимостей - сделать каждый исходный файл как можно более атомарным. В идеале, вы должны экспортировать только 1 объект/метод/константу из файла. Хотя это не абсолютно удобно с точки зрения DX. Поэтому я сомневаюсь, что какой-либо разработчик захочет создавать отдельный файл каждый раз, когда ему понадобится общая константа или служебный метод. Итак, чтобы сбалансировать трудозатраты с эффективной ленивой загрузкой, вот простое руководство: не экспортируйте сущности разных типов из одного и того же файла. И под типами я понимаю саму суть сущности: константа, объект, класс, метод, компонент, и т.п.

Вы по-прежнему можете объявлять служебные методы, константы, классы и компоненты в одном файле. Например, мы могли бы хранить некоторые служебные методы и константы внутри файла Chapter1.tsx. Но когда нам нужно экспортировать сущности разных типов, нам необходимо создавать отдельные файлы: utility файлы для функций, constant файлы для констант и так далее. Что касается компонентов, то лучше стараться, чтобы их было как можно меньше в каждом файле. И если какой-либо из компонентов должен стать общим, мы не должны полениться создать для него отдельный файл.

Вот пример того, как я разделяю файлы в своем проекте:

За таким разделением стоит простая логика. Файл, который экспортирует только константы, с большей вероятностью будет повторно использоваться другими файлами, но с меньшей вероятностью будет иметь свои собственные зависимости. Поскольку обычно файлы с константами могут импортировать другие константы, но ничего больше. Служебные методы тоже часто используются повторно, и они обычно импортируют только константы и другие служебные методы. И, что касается компонентов, лучше использовать их как можно меньше в каждом файле, потому что они будут использовать любые другие файлы.

? Не экспортируйте объекты разных типов из одного файла. Экспортируйте служебные методы только из utility файлов. Аналогично для констант. Что касается компонентов, старайтесь, чтобы их было как можно меньше на файл.

Правило №4. Избегай петли

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

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

? Избегайте циклических импортов в вашем приложении.


Re-export'ы не влияют на ленивую загрузку. Пока не...

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

export * from './component-1/first';
export * from './component-1/second';
export * from './component-1/third';

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

Однако это не так. Webpack использует файлы конечных деклараций при анализе дерева зависимостей. Если мы импортируем объект из файла реэкспорта, Webpack будет использовать фактический файл, в котором этот объект был объявлен.

// So if you import an entity like this
import { First } from '@components';

// Webpack will treat it as
import { First } from '../../../components/component-1/first.tsx';

Повторный экспорт файлов не приведет к нарушению нашего дерева зависимостей, даже если вы выполните barrel import. Потому что, если мы используем barrel импорт как обычный импорт, Webpack все равно сможет построить правильное дерево зависимостей.

import * as components from '@components';

// Only `First` will be a dependency of this file
export default () => (
  <components.First {...} />
);

Но как только мы используем barrel импорт как обычный объект, мы обречены. Такой объект просто становится фасадным объектом, чего нам следует избегать. И с таким объектом мы в конечном итоге загружаем все экспортируемые сущности из barrel import объекта.

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

import * as components from '@components';
import * as icons from 'react-icons/md';

// Using this component will lead to downloading all the icons
//  from react-icons/md, even if only 1 icon is used.
export Icon = ({ name }: { name: keyof typeof icons }) => {
  const IconElement = icons[name];
  return <IconElement />;
};

// Using this component will lead to downloading all the components
//  from @components, even if only 1 components is used.
export Component = ({ name }: { name: keyof typeof components }) => {
  const ComponentElement = components[name];
  return <ComponentElement />;
};

? Старайтесь избегать использования barrel импортов и особенно их использования в качестве обычных объектов, за исключением случаев, когда это действительно необходимо.


Заключение

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

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

  • ? Не полагайтесь на tree-shaking. Разделяйте файлы вручную, чтобы обеспечить эффективную ленивую загрузку.

  • ? Каждый динамически импортируемый файл должен содержать только 1 экспортируемый объект, например, только экспорт по умолчанию.

  • ? Делайте ваши файлы как можно более атомарными и экспортируйте как можно меньше зависимостей.

  • ? Старайтесь избегать Java-style pattern'ов и других шаблонов, которые позволяют централизовать логику из нескольких файлов в одном файле.

  • ? Не экспортируйте объекты разных типов из одного файла. Экспортируйте служебные методы только из utility файлов. Аналогично для констант. Что касается компонентов, старайтесь, чтобы их было как можно меньше на файл.

  • ? Избегайте циклических импортов в вашем приложении.

  • ? Старайтесь избегать использования barrel импортов и особенно их использования в качестве обычных объектов, за исключением случаев, когда это действительно необходимо.

Мы на половине пути. Оставайтесь с нами, чтобы погрузиться еще глубже:
Как сделать сайт бесконечно ленивым. Часть 3: Вендоры и кэш

Вот мои соц. сети: LinkedInTelegramGitHub. Свидимся! ✌️

Комментарии (1)


  1. OlegZH
    10.11.2025 08:39

    Как же было здорово во времена настольных приложений! Скольких проблем не существовало в природе! Неужели никто не объяснит, почему сейчас нельзя продолжать делать такие же настольные приложения, просто указывая удалённый сервер как источник данных?