1. Code
  2. Coding Fundamentals
  3. Performance

Значительное ускорение вашего фронтенд-приложения на React с помощью ленивой загрузки

Scroll to top

Russian (Pусский) translation by Alexey Pyltsyn (you can also view the original English article)

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

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

Что значит быстрый сайт?

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

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

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

React Loadable

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

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

1. Добавить React Loadable к вашему компоненту

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

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

1
// MyComponent/MyComponent.jsx

2
3
export default () => (
4
  <div>
5
    This component will be lazy-loaded!
6
  </div>

7
)
1
// MyComponent/index.js

2
3
import Loadable from 'react-loadable'
4
5
export default Loadable({
6
  // The import below tells webpack to 

7
  // separate this code into another bundle

8
  loader: import('./MyComponent')
9
})

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

1
// anotherComponent/index.js

2
3
import MyComponent from './MyComponent'
4
5
export default () => <MyComponent />

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

2. Упрощение установки с помощью Babel

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

1
// input file

2
3
import Loadable from 'react-loadable'
4
5
export default Loadable({
6
  loader: () => import('./MyComponent')
7
})
1
// output file 

2
3
import Loadable from 'react-loadable'
4
import path from 'path'
5
6
export default Loadable({
7
  loader: () => import('./MyComponent'),
8
  webpack: () => [require.resolveWeak('./MyComponent')],
9
  modules: [path.join(__dirname, './MyComponent')]
10
})

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

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

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

3. Отрисовка компонентов на сервере

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

1
// server/index.js

2
3
app.get('/', (req, res) => {
4
  const markup = ReactDOMServer.renderToString(
5
    <MyApp/>
6
  )
7
8
  res.send(`

9
    <html>

10
      <body>

11
        <div id="root">${markup}</div>

12
        <script src="/build/app.min.js"></script>

13
      </body>

14
    </html>

15
  `)
16
})
17
18
app.listen(8080, () => {
19
  console.log('Running...')
20
})

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

1
// server/index.js 

2
3
Loadable.preloadAll().then(() => {
4
  app.listen(8080, () => {
5
    console.log('Running...')
6
  })
7
})

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

1
// server/index.js

2
3
import Loadable from 'react-loadable'
4
5
app.get('/', (req, res) => {
6
  const modules = []
7
  const markup = ReactDOMServer.renderToString(
8
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
9
      <MyApp/>
10
    </Loadable>

11
  )
12
13
  res.send(`

14
    <html>

15
      <body>

16
        <div id="root">${markup}</div>

17
        <script src="/build/app.min.js"></script>

18
      </body>

19
    </html>

20
  `)
21
})
22
23
Loadable.preloadAll().then(() => {
24
  app.listen(8080, () => {
25
    console.log('Running...')
26
  })
27
})

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

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

4. Сопоставление пакетов Webpack с модулями

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

1
// webpack.client.config.js

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

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

1
// build/loadable-manifest.json

2
3
{
4
  "MyComponent": "/build/myComponent.min.js"
5
}

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

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

5. Включение бандлов в HTML

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

1
// server/index.js

2
3
import Loadable from 'react-loadable'
4
import { getBundles } from 'react-loadable/webpack'
5
import manifest from './build/loadable-manifest.json'
6
7
app.get('/', (req, res) => {
8
  const modules = []
9
  const markup = ReactDOMServer.renderToString(
10
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
11
      <MyApp/>
12
    </Loadable>

13
  )
14
  
15
  const bundles = getBundles(manifest, modules)
16
17
  // My rendering logic below ...

18
})
19
20
Loadable.preloadAll().then(() => {
21
  app.listen(8080, () => {
22
    console.log('Running...')
23
  })
24
})

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

1
// server/index.js

2
3
app.get('/', (req, res) => {
4
  // My App & modules logic

5
6
  res.send(`

7
    <html>

8
      <body>

9
        <div id="root">${markup}</div>

10
        <script src="/build/manifest.min.js"></script>

11
        ${bundles.map(({ file }) =>
12
          `<script src="/build/${file}"></script>`
13
        }).join('\n')}

14
        <script src="/build/app.min.js"></script>

15
      </body>

16
    </html>

17
  `)
18
})
19
20
Loadable.preloadAll().then(() => {
21
  app.listen(8080, () => {
22
    console.log('Running...')
23
  })
24
})

6. Загрузка отрисовываемых на сервере бандлов на клиенте

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

1
// client/index.js

2
3
import React from 'react'
4
import { hydrate } from 'react-dom'
5
import Loadable from 'react-loadable'
6
7
import MyApplication from './MyApplication'
8
9
Loadable.preloadReady().then(() => {
10
  hydrate(
11
    <MyApplication />,
12
    document.getElementById('root')
13
  );
14
});

Заключение

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