React, пожалуй, самый популярный фреймворк Javascript за последнее время. Он предоставляет инженеру очень простой API для разработки совместно используемых компонентов, а также одностраничных приложений. Объединение контейнера, такого как Redux, или концепции потока данных Flux делает React еще более мощным.
Мне было интересно (а иногда и сложно) когда дело дошло до оптимизации производительности React.
В общем, есть несколько углов, которые мы могли бы решить. Некоторые из оптимизаций, вероятно, предназначены для веб-приложения в целом (прогрессивное веб-приложение).
- Начальная загрузка
- Последующая загрузка страницы
- React update
- Анимация и частота кадров
- Общая оптимизация Javascript
- Общая оптимизация рендеринга CSS / HTML
Этот пост будет посвящен ускорению начальной загрузки вашего приложения React.
Если вы не знакомы с тем, как браузер обрабатывает страницу, я настоятельно рекомендую прочитать замечательную статью Ильи Григорика о критическом пути отрисовки. [1]
- Приоритет сети: разные ресурсы имеют разный приоритет загрузки. Некоторые ресурсы с высоким приоритетом могут блокировать рендеринг.
Допустим, у вас есть внешний файл Javascript.
<script src="javascript.js"></script>
Это заблокирует рендеринг браузера до тех пор, пока ресурс не вернется. Ваша страница будет работать еще медленнее, если у вас большой файл Javascript и медленная сеть (например, мобильная сеть 3G).
К счастью, у браузера есть несколько подходов для более элегантной обработки блокирующих ресурсов.
async vs.s. отложить
Распространенным подходом является использование атрибутов HTML5 async
и defer
.
Оба атрибута сделают файл Javascript неблокирующим активом, но работают они по-разному.
async
указывает, что это неблокирующие ресурсы, и они должны выполняться асинхронно.
<script src="javascript.js" async></script>
defer
сообщит браузеру, что этот скрипт должен выполняться только после анализа DOM (но до события DOMContentLoaded)
<script src="javascript.js" defer></script>
Если у вас несколько сценариев, атрибуты defer
по-прежнему будут гарантировать последовательность выполнения, а async
- нет.
«предварительная нагрузка» против. «Предварительная выборка»
Использование таких приемов, как preload
и prefetch
, поможет браузеру получить критический ресурс еще раньше.
Адди Османи написал [2] блестящий пост о preload
и prefetch
, который я настоятельно рекомендую проверить.
«программная отсрочка активов»
Еще одна распространенная практика - отложить загрузку Javascript после события DOMContentLoaded
или load
.
<script>
const loadscript = link => { var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.charset = 'utf-8'; script.async = false; script.src = link;
head.appendChild(script); }
document.addEventListener("DOMContentLoaded", function(event) { loadscript('yourscript'); });
</script>
2. Начальная полезная нагрузка
Чтобы предотвратить циклические обходы вашего веб-приложения, нам неизбежно придется уменьшить размер вашего пакета.
Один из подходов - уменьшить размер кода фреймворка, ища альтернативу React. Например, Preact
и Inferno
все обеспечивают замену React, что значительно уменьшит размер пакета.
Другой подход - уменьшить размер собственного пакета. В качестве примера я буду использовать Webpack.
CommonsChunkPlugin помогает переместить общую библиотеку Javascript в отдельный файл, чтобы лучше оптимизировать кеширование браузера для последующей загрузки страницы.
Хороший подход - перечислить те внешние зависимости, которые могут не меняться так часто, как «реагировать», «lodash», «immutablejs»… на общий фрагмент поставщика и обновлять хеш-значение при внесении изменений или обновлении. Затем браузер может загрузить фрагмент продавца из кеша.
Ленивая загрузка (динамический импорт) и разделение кода
Динамический импорт в настоящее время находится на стадии 3.
Вы можете отложить загрузку ресурса, используя require.ensure()
.
require.ensure([/* dependencies */], require => { const Foo = require('foo'); }, error => { // handle error }, 'custom-bundle-name');
Если вы предпочитаете функцию на основе обещаний, вы также можете использовать import
с babel. Импортируйте использование Promise для внутреннего использования, поэтому вам понадобятся полифиллы для старых версий браузера.
Вам также понадобится плагин babel, пока он еще находится на этапе 3.
import(/*webpackChunkName: "
custom-bundle-name"*/
'foo').then(Foo => {});npm install --save-dev babel-core babel-loader babel-plugin-syntax-dynamic-import babel-preset-es2015
module.exports = { module: { rules: [{ test: /\.js$/, exclude: /(node_modules)/, use: [{ loader: 'babel-loader', options: { presets: [['es2015', { modules: false }]], plugins: ['syntax-dynamic-import'] } }] }] } };
Чтобы узнать больше о разделении кода, вы можете проверить здесь [3].
Практический пример - реализация разделения кода по маршруту. Я буду использовать пример, сочетающий response-router и react-loadable.
Перед оптимизацией Webpack объединит ваши ресурсы в один файл, который загрузит все ресурсы заранее для начальной загрузки страницы.
import React from 'react'; import { BrowserRouter as Router, Route } from 'react-router-dom';
import Home from './Home'; import About from './About';
const App = () => ( <Router> <h1>My App</h1>
<div> <Route exact path="/" component={Home}/> <Route path="/about" component={About}/> </div> </Router> );
Используя динамический импорт, мы потенциально можем выделить некритические ресурсы из первоначального пакета.
import React from 'react' import { BrowserRouter as Router, Route } from 'react-router-dom';
import Loadable from 'react-loadable';import Home from './Home';
const About =
Loadable({ loader: () => import('./About'), LoadingComponent: null // or your loading component });const App = () => ( <Router> <h1>My App</h1>
<div> <Route exact path="/" component={Home}/> <Route path="/about" component={About}/> </div> </Router> );
В этом примере компонент About не будет загружен, пока вы не получите доступ к маршруту / about. Однако, поскольку ресурс загружается асинхронно, ваша страница может видеть вспышку после загрузки ресурса. react-loadable
предоставляет компонент-заполнитель LoadingComponent, который будет отображаться перед загрузкой фрагмента. Вы также можете использовать функцию preload, предоставляемую react-loadable
, для предварительной загрузки ресурса, если вы уверены, что ваш пользователь посетит страницу.
function preloadNextPage(LoadableComponent) { LoadableComponent.preload(); }
Поскольку Webpack теперь разделяет все пакеты с отложенной загрузкой, вы можете обнаружить, что некоторые часто используемые библиотеки дублируются в нескольких дочерних блоках. Снова используя CommonsChunkPlugin, вы сможете переместить этот общий кусок вверх в родительский и уменьшить общее дублирование.
new webpack.optimize.CommonsChunkPlugin({ children: true, minChunks: 3 })
Подробный пост [5] от Sean T. Larkin поможет вам понять волшебство плагина Webpack.
По моему опыту, мы наблюдаем снижение времени загрузки примерно на 45% после принятия предварительной загрузки / предварительной выборки и разделения кода для нашего приложения React.
Следующим шагом будут некоторые практики, которые помогут сократить время загрузки подпоследовательности.
[1] https://developers.google.com/web/fundamentals/performance/critical-rendering-path/
[2] https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf
[3] https://webpack.js.org/guides/code-splitting-async/
[4] https://developers.google.com/web/fundamentals/performance/prpl-pattern/
[5] https://medium.com/webpack/webpack-bits-getting-the-most-out-of-the-commonschunkplugin-ab389e5f318