Статьи

Значительно ускорите свое приложение React Front-End, используя Lazy Loading

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

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

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

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

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

React Loadable — это пакет, который позволяет нам лениво загружать наш JavaScript только тогда, когда это требуется приложением. Конечно, не все веб-сайты используют React, но ради краткости я собираюсь сосредоточиться на реализации React Loadable в серверном приложении, построенном с использованием Webpack. Конечным результатом будет множество файлов JavaScript, автоматически доставляемых в браузер пользователя, когда этот код необходим. Если вы хотите опробовать законченный код, вы можете клонировать исходный код примера из нашего репозитория GitHub .

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

Я возьму пример компонента React, MyComponent . Я предполагаю, что этот компонент состоит из двух файлов, MyComponent/MyComponent.jsx и MyComponent/index.js .

В этих двух файлах я определяю компонент React точно так же, как обычно в MyComponent.jsx . В index.js я импортирую компонент React и реэкспортирую его — на этот раз в функции Loadable . Используя функцию import ECMAScript, я могу указать Webpack, что я ожидаю, что этот файл будет загружен динамически. Этот шаблон позволяет мне легко загружать любой компонент, который я уже написал. Это также позволяет мне разделить логику между отложенной загрузкой и рендерингом. Это может показаться сложным, но вот как это будет выглядеть на практике:

1
2
3
4
5
6
7
// MyComponent/MyComponent.jsx
 
export default () => (
  <div>
    This component will be lazy-loaded!
  </div>
)
1
2
3
4
5
6
7
8
9
// MyComponent/index.js
 
import Loadable from ‘react-loadable’
 
export default Loadable({
  // The import below tells webpack to
  // separate this code into another bundle
  loader: import(‘./MyComponent’)
})

Затем я могу импортировать свой компонент точно так же, как обычно:

1
2
3
4
5
// anotherComponent/index.js
 
import MyComponent from ‘./MyComponent’
 
export default () => <MyComponent />

Я теперь ввел React MyComponent в MyComponent . Я могу добавить больше логики к этому компоненту позже, если я выберу — это может включать введение состояния загрузки или обработчика ошибок для компонента. Благодаря Webpack, когда мы запустим нашу сборку, мне теперь будут предоставлены два отдельных app.min.js JavaScript: app.min.js — это наш обычный пакет приложений, а myComponent.min.js содержит код, который мы только что написали. Я расскажу, как доставить эти пакеты в браузер чуть позже.

Обычно я должен был бы включить две дополнительные опции при передаче объекта в функцию webpack , modules и webpack . Они помогают Webpack определить, какие модули мы должны включать. К счастью, мы можем избавиться от необходимости включать эти два параметра в каждый компонент, используя плагин react-loadable/babel . Это автоматически включает в себя следующие параметры для нас:

1
2
3
4
5
6
7
// input file
 
import Loadable from ‘react-loadable’
 
export default Loadable({
  loader: () => import(‘./MyComponent’)
})
01
02
03
04
05
06
07
08
09
10
// output file
 
import Loadable from ‘react-loadable’
import path from ‘path’
 
export default Loadable({
  loader: () => import(‘./MyComponent’),
  webpack: () => [require.resolveWeak(‘./MyComponent’)],
  modules: [path.join(__dirname, ‘./MyComponent’)]
})

Я могу включить этот плагин, добавив его в мой список плагинов в моем файле .babelrc , например так:

1
2
3
{
  «plugins»: [«react-loadable/babel»]
}

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

В моем серверном приложении у меня есть стандартная конфигурация, которая выглядит примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
// server/index.js
 
app.get(‘/’, (req, res) => {
  const markup = ReactDOMServer.renderToString(
    <MyApp/>
  )
 
  res.send(`
    <html>
      <body>
        <div id=»root»>${markup}</div>
        <script src=»/build/app.min.js»></script>
      </body>
    </html>
  `)
})
 
app.listen(8080, () => {
  console.log(‘Running…’)
})

Первым шагом будет инструктировать React Loadable, что я хочу, чтобы все модули были предварительно загружены. Это позволяет мне решить, какие из них должны быть немедленно загружены на клиенте. Я делаю это, изменяя мой файл server/index.js следующим образом:

1
2
3
4
5
6
7
// server/index.js
 
Loadable.preloadAll().then(() => {
  app.listen(8080, () => {
    console.log(‘Running…’)
  })
})

Следующим шагом будет помещение всех компонентов, которые я хочу визуализировать, в массив, чтобы мы могли позже определить, какие компоненты требуют немедленной загрузки. Это так, чтобы HTML можно было возвращать с правильными пакетами JavaScript, включенными через теги скрипта (подробнее об этом позже) Сейчас я собираюсь изменить файл моего сервера следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// server/index.js
 
import Loadable from ‘react-loadable’
 
app.get(‘/’, (req, res) => {
  const modules = []
  const markup = ReactDOMServer.renderToString(
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
      <MyApp/>
    </Loadable>
  )
 
  res.send(`
    <html>
      <body>
        <div id=»root»>${markup}</div>
        <script src=»/build/app.min.js»></script>
      </body>
    </html>
  `)
})
 
Loadable.preloadAll().then(() => {
  app.listen(8080, () => {
    console.log(‘Running…’)
  })
})

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

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

Итак, теперь я дал указание Webpack создать myComponent.min.js , и я знаю, что MyComponent используется немедленно, поэтому мне нужно загрузить этот пакет в исходной полезной нагрузке HTML, которую мы доставляем пользователю. К счастью, React Loadable также позволяет нам достичь этого. В моем конфигурационном файле клиента Webpack мне нужно включить новый плагин:

1
2
3
4
5
6
7
8
9
// webpack.client.config.js
 
import { ReactLoadablePlugin } from ‘react-loadable/webpack’
 
plugins: [
  new ReactLoadablePlugin({
    filename: ‘./build/loadable-manifest.json’
  })
]

Файл loadable-manifest.json предоставит мне сопоставление между модулями и пакетами, чтобы я мог использовать массив modules я настроил ранее, для загрузки пакетов, которые, как я знаю, мне понадобятся. В моем случае этот файл может выглядеть примерно так:

1
2
3
4
5
// build/loadable-manifest.json
 
{
  «MyComponent»: «/build/myComponent.min.js»
}

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

1
2
3
4
5
6
plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: ‘manifest’,
    minChunks: Infinity
  })
]

Последний шаг при загрузке наших динамических пакетов на сервер — включить их в HTML-код, который мы предоставляем пользователю. Для этого шага я собираюсь объединить вывод шагов 3 и 4. Я могу начать с изменения файла сервера, который я создал выше:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// server/index.js
 
import Loadable from ‘react-loadable’
import { getBundles } from ‘react-loadable/webpack’
import manifest from ‘./build/loadable-manifest.json’
 
app.get(‘/’, (req, res) => {
  const modules = []
  const markup = ReactDOMServer.renderToString(
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
      <MyApp/>
    </Loadable>
  )
   
  const bundles = getBundles(manifest, modules)
 
  // My rendering logic below …
})
 
Loadable.preloadAll().then(() => {
  app.listen(8080, () => {
    console.log(‘Running…’)
  })
})

В этом я импортировал манифест и попросил React Loadable создать массив с отображениями модуля / пакета. Единственное, что мне осталось сделать, это отобразить эти пакеты в строку HTML:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// server/index.js
 
app.get(‘/’, (req, res) => {
  // My App & modules logic
 
  res.send(`
    <html>
      <body>
        <div id=»root»>${markup}</div>
        <script src=»/build/manifest.min.js»></script>
        ${bundles.map(({ file }) =>
          `<script src=»/build/${file}»></script>`
        }).join(‘\n’)}
        <script src=»/build/app.min.js»></script>
      </body>
    </html>
  `)
})
 
Loadable.preloadAll().then(() => {
  app.listen(8080, () => {
    console.log(‘Running…’)
  })
})

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
// client/index.js
 
import React from ‘react’
import { hydrate } from ‘react-dom’
import Loadable from ‘react-loadable’
 
import MyApplication from ‘./MyApplication’
 
Loadable.preloadReady().then(() => {
  hydrate(
    <MyApplication />,
    document.getElementById(‘root’)
  );
});

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