Значительное ускорение вашего фронтенд-приложения на React с помощью ленивой загрузки
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 | }); |
Заключение
Следуя этому процессу, я могу разбить свой бандл приложения на столько маленьких бандлов, сколько мне требуется. Таким образом, мое приложение отправляет меньше данных пользователю и только тогда, когда они ему нужны. Я уменьшил количество кода, который нужно отправить, чтобы его можно было отправить быстрее. Это может значительно повысить производительность для более крупных приложений. Это также подходит для небольших приложений до больших в случае необходимости.



