Перевод статьи о том, что означает ленивая загрузка в трех наиболее часто используемым фронтенд-фреймворках: Angular, React и Vue.js. Далее текст от лица автора.

Нетерпеливая загрузка против ленивой

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

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

О типовых проектах для примера

Примеры приложений, созданных во всех трех фреймворках, очень похожи. Каждый из них показывает следующие две вещи:

  • как использовать ленивую загрузку компонента внутри страницы;
  • как использовать ленивую загрузку компонента с помощью маршрутизации.

Чтобы эффективно визуализировать ленивую загрузку, демо-приложение (компонент) будет вычислять 42-е число Фибоначчи. Математические операции считаются блокирующими — это означает, что наша программа не может продвинуться дальше; она должна вернуть результат вычисления. Эта стратегия используется только для имитации того, что происходит, если существует фрагмент кода, выполнение которого занимает много времени, и какое влияние он оказывает на общий опыт использования приложения.

Чтобы получить доступ к проекту, пожалуйста, посетите репозиторий GitHub.

Angular

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

Компонент Фибоначчи

Вот как наш компонент выглядит в Angular:

fibonacci: number;

 ngOnInit(): void {
  const fibonacci = num => {
   if (num <= 1) return 1;
   return fibonacci(num - 1) + fibonacci(num - 2);
 };
 this.fibonacci = fibonacci(42);
}

Поскольку это отдельный компонент и он уже принадлежит корневому компоненту Angular, мы можем загрузить его на страницу.

Загрузка компонента на странице

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

<div>
    <p>I am the <strong>home</strong> component.</p>
    <button (click)="showMe()">{{ showFibonacci ? 'Hide' : 'Show' }}</button>
    <div *ngIf="showFibonacci">
        <app-fibonacci-one></app-fibonacci-one>
    </div>
</div>

Вместе со следующим TypeScript-кодом:

export class AppComponents {
 showFibonacci:Boolean = false;
 showMe() {
  this.showFibonacci = !this.showFibonacci
 }
}

Что действительно интересно в этой ситуации, так это то, что компонент Fibonacci будет загружаться только в том случае, если значение showFibonacci равно true. Это означает, что управлять ленивой загрузкой можно только с помощью директивы ngIf. Это происходит потому, что Angular не просто показывает или скрывает компонент в DOM — он добавляет или удаляет его на основе указанного условия.

Ленивая загрузка или роутинг

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

Создать функциональный модуль в нашем приложении вместе со вторым компонентом можно с помощью Angular CLI: ng g m fibonacci && ng g c —module=fibonacci fibonacci.

После создания модуля мы можем назначить ему компонент, а затем добавить его в основной модуль маршрутизации (app-routing.module.ts):

const routes: Routes = [{
 path: 'fibonacci', loadChildren: () =>
  import('./fibonacci/fibonacci.module').then(m => m.FibonacciModule)
}];

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

Сравните код выше с этим:

import { FibonacciComponent } from './fibonacci/fibonacci.component';

const routes: Routes = [{
 path: 'fibonacci', component: FibonacciComponent
}];

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

Тут можно прочитать больше про ленивую загрузку в Angular.

Vue

Теперь давайте рассмотрим, как добиться ленивой загрузки при разработке с помощью фреймворка Vue.js. Давайте создадим Vue-приложение с помощью интерфейса командной строки Vue CLI и добавим новый компонент. Взгляните на то, как будет выглядеть часть компонента <script>:

const fibonacci = num => {
  if (num <= 1) return 1;
  return fibonacci(num - 1) + fibonacci(num - 2);
};
const myFibonacci = fibonacci(42);
 
export default {
  name: 'Fibonacci',
  data: () => ({
   fibonacci: 0
  }),
  created() {
   this.fibonacci =myFibonacci;
  }
};

Обратите внимание: причина, по которой нам нужно выполнить вычисление вне блока export default {}, в том, что иначе мы не сможем имитировать операцию блокировки. Естественно, Vue.js имеет как свойство mounted, так и свойство method, доступные для компонентов, что позволит вызывать код только при создании компонента.

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

В Vue.js мы можем использовать директиву v-if для добавления или удаления элемента из DOM, и так лениво загружать компонент. Однако есть еще много вещей, которые нам нужно сделать, когда речь заходит о сравнении Vue.js и Angular. Взгляните на следующий код:

<div v-if="showFibonacci">
 <Fibonacci />
</div>
<script>
import Fibonacci from './Fibonacci.vue';
export default {
 name: 'Home',
 data: () => ({
  showFibonacci: false
 }),
 methods: {
  showMe: function() {
   this.showFibonacci = !this.showFibonacci;
  }
 },
 components: {
  Fibonacci
 }
};
</script>

Это может показаться логичным способом сделать ленивую загрузку, однако при открытии страницы становится очевидным, что начальное время загрузки действительно велико. Это происходит потому, что компонент загружается сразу независимо от условия v-if. Другими словами, мы говорим Vue загрузить все компоненты независимо от их добавления в DOM.

Производительность загрузки существенно изменится, если мы внесем следующие изменения в элемент <script>:

// import Fibonacci from './Fibonacci.vue';
export default {
 name: 'Home',
 data: () => ({
  showFibonacci: false
 }),
 method: {
  showMe: function() {
   this.showFibonacci = !this.showFibonacci;
  }
 },
 components: {
  // Fibonacci
  Fibonacci: () => import('./Fibonacci.vue')
 }
};

Добавляя выражение import в инлайн-стиле, как часть свойства компонента, мы включаем ленивую загрузку для компонента Fibonacci. Теперь обновление приложения будет означать, что главная страница загружается действительно быстро. Только когда компонент Fibonacci отображается на главной странице, есть некоторая задержка.

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

Ленивая загрузка компонентов в Vue.js следует аналогичной схеме, которую мы обсуждали ранее. Посмотрите на роутер:

// excerpt
import Home from '../views/Home.vue';
import Fibonacci from '../views/Fibonacci.vue';

Vue.use(VueRouter);

const routes = [
 {
   path: '/',
   name: 'Home';
   components: Home
 },
 {
   path: '/fibonacci',
   name: 'Fibonacci',
   components: Fibonacci
  }
];

Такой маршрутизатор вы, возможно, использовали или видели раньше в приложениях Vue. Несмотря на то что он функциональный, вы можете наблюдать следующую проблему. Если у нас есть блокирующая операция в компоненте Fibonacci, она будет блокировать загрузку компонента Home.

Чтобы устранить эту проблему, мы можем прибегнуть к привычному паттерну и импортировать компонент в определение маршрута:

import Home from '../views/Home.vue';
// import Fibonacci from '../views/Fibonacci.vue';

Vue.use(VueRouter);

const routes = [
 {
  path: '/',
  name: 'Home',
  components: Home
 },
 {
  path: '/fibonacci',
  name: 'Fibonacci',
  components: () => import('../views/Fibonacci.vue')
 }
];

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

Тут больше информации про ленивую загрузку в Vue.js.

React

И последнее, но не менее важное: давайте рассмотрим, как добиться ленивой загрузки в React. Приложение было создано с помощью CLI create-react-app и, как и в предыдущих примерах, у нас есть компонент с некоторой блокирующей операцией:

import React from 'react';

const fibonacci = num => {
 if (num <= 1) return 1;
 return fibonacci(num - 1) + fibonacci(num - 2);
};

const fib = fibonacci(42);

const Fibonacci = () => (
 <>
  <p>Hello, this is the <strong>Fibonacci</strong> component.
  For fun I calculated the 42nd Fibonacci number which is: { fib }.</p>
 </>
);

export default Fibonacci;

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

По умолчанию, как и в предыдущих примерах с использованием других фреймворков, импорт компонентов будет означать нетерпеливую загрузку:

import Fibonacci from './Fibonacci';
const Home = () => {
 const [showFibonacci, setShowFibonacci] = useState(false)
 const showMe = () => setShowFibonacci(!showFibonacci);
 return (
 <>
  <p>I am the <strong>home</strong> component.</p>
  <button onClick={showMe}>{ showFibonacci ? 'Hide' : 'Show' }</button>

  { showFibonacci ? <Fibonacci /> : '' }
 </>
)};

В приведенном выше примере, даже если компонент Fibonacci не отображается, загрузка главной страницы приложения все равно занимает много времени. Чтобы исправить это, нужно сказать React о ленивой загрузке компонента после знака вопроса. В React есть несколько вспомогательных инструментов, таких как компонент Suspense для отображения плейсхолдера во время загрузки компонента и метод lazy (), который загружает компонент лениво:

import React, { Suspense, useState, lazy } from 'react';
const Fibonacci = lazy (() => import('./Fibonacci'));
//...
{ showFibonacci ? <Suspense fallback={<div>Loading...</div>}>
<Fibonacci /></Suspense>: '' }

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

Ленивая загрузка или роутинг

Тот же подход применим и к ленивой загрузке компонента с помощью маршрутизации, включая использование Suspense и lazy():

import Fibonacci from './components/Fibonacci';
<Router>
 <nav>
  <ul>
   <li>
    <Link to="/">Home</Link>
   </li>
   <li>
    <Link to="/fibonacci">Fibonacci</Link>
   </li>
  </ul>
 </nav>
 <Suspense fallback={<div>Loading...</div>}>
  <Switch>
   <Route exact path="/" component={Home}/>
   <Route path="/fibonacci" component={Fibonacci}/>
  </Switch>
 </Suspence>

</Router>

Учитывая вышеприведенный маршрутизатор, в сочетании с оператором import это означает, что компонент Fibonacci будет загружен сразу. Теперь, надеюсь, понятно, почему это не идеально. Чтобы включить ленивую загрузку компонентов через маршрутизацию, нужно изменить код, чтобы использовать вышеупомянутый компонент Suspense и метод lazy ():

// import Fibonacci from './components/Fibonacci';
const Fibonacci = lazy (() => import('./components/Fibonacci'));

//...
<Suspense fallback={<div>Loading...</div>}>
 <Switch>
  <Route exact path="/" components={Home}/>
  <Route path="/fibonacci" components={Fibonacci}/>
 </Switch>
</Suspense>

Тут больше о ленивой загрузке в React.

Проверки через Инструменты разработчика

Чтобы увидеть, что происходит под капотом, мы можем использовать панель DevTools браузера.

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

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

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

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

Еще одна вещь

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

Существуют определенные стратегии, которые помогут преодолеть эту проблему. Со всеми фреймворками мы можем использовать волшебные комментарии через Webpack для динамического добавления prefetch (или preload) через тег <link rel=»prefetch» /> на страницу. Просто поместите волшебные комментарии перед именем компонента, внутри импорта:

const Fibonacci = lazy(() => import(
 /* webpackMode: "lazy" */
 /* webpackPrefetch: true */
 /* webpackPreload: true */
 /* webpackChunkName: "fibonacci" */ './components/Fibonacci'));

Это добавит в DOM тег <link rel=»prefetch» as=»script» href=»/static/js/fibonacci.chunk.js»>.

Больше о волшебных комментариях и параметрах preload/prefetch в Webpack.

Заключение

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

Оригинал статьи на Habr.com.