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



