React, пожалуй, самый популярный фреймворк Javascript за последнее время. Он предоставляет инженеру очень простой API для разработки совместно используемых компонентов, а также одностраничных приложений. Объединение контейнера, такого как Redux, или концепции потока данных Flux делает React еще более мощным.

Мне было интересно (а иногда и сложно) когда дело дошло до оптимизации производительности React.

В общем, есть несколько углов, которые мы могли бы решить. Некоторые из оптимизаций, вероятно, предназначены для веб-приложения в целом (прогрессивное веб-приложение).

  1. Начальная загрузка
  2. Последующая загрузка страницы
  3. React update
  4. Анимация и частота кадров
  5. Общая оптимизация Javascript
  6. Общая оптимизация рендеринга CSS / HTML

Этот пост будет посвящен ускорению начальной загрузки вашего приложения React.

Если вы не знакомы с тем, как браузер обрабатывает страницу, я настоятельно рекомендую прочитать замечательную статью Ильи Григорика о критическом пути отрисовки. [1]

  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