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
}); 

Заключение

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