Руководство по оптимизации производительности загрузки React 16

JavaScript браузер React.js Webpack

По поводу оптимизации загрузки React приложений, подобных статей в интернете на самом деле слишком много, просто поиск их куча, что стало общей проблемой.

Но с выпуском React 16 и Webpack 4.0 многие методы оптимизации в прошлом на самом деле более или менее «устарели», и совсем недавно новые проекты компании перешли на React 16 и Webpack 4.0, сделав многое из этого. оптимизация, поэтому напишите статью, чтобы подвести итоги.

Ноль, основные понятия

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

  1. Когда пользователь открывает страницу, в это время она совершенно пуста;
  2. Затем загружаются html и указанный css, и браузер делаетпервый рендер, Сначала нам нужно загрузить объемный рендеринг ресурса, называемый«Выше сгиба»;
  3. Затем загружаются код реакции, реакции-дома и бизнес-кода, и приложение отображается в первый раз, илипервый рендер контента;
  4. Затем код приложения начинает выполняться, подтягивает данные, выполняет динамический импорт, реагирует на события и т. д. После завершения страница переходит вИнтерактивныйгосударство;
  5. Далее начинает постепенно загружаться мультимедийный контент, такой как лениво загружаемые картинки;
  6. Затем, пока другие ресурсы страницы (например, компоненты отчетов об ошибках, компоненты отчетов RBI и т. д.) не будут загружены, загрузка всей страницы завершена.

Итак, далее мы обсудим моменты, которые стоит оптимизировать на этих этапах.


1. Откройте страницу -> первый экран

Если вы писали React или любой SPA, вы должны знать, что почти все популярные интерфейсные фреймворки (React, Vue, Angular) имеют очень похожие методы запуска приложений:

  1. Укажите корневой узел в html
<div id="root"></div>
  1. Смонтируйте приложение на этом узле
ReactDOM.render(
  <App/>,
  document.getElementById('root')
);

В таком режиме после упаковки вебпаком остается вообще три файла:

  1. Небольшой, бесполезный HTML-код, кроме предоставления корневого узла (Около 1-4 КБ)
  2. Большой JS (Диапазон 50–1000 КБ)
  3. css-файл (конечно, если вы поместите css в js, возможно, нет)

Прямым следствием этого является то, что до того, как пользователь загрузит и выполнит js-файл размером 50-1000 КБ, страницаЗаканчивать! Полный! нулевой! Белый! из!.

То есть на этот раз:

首屏体积(首次渲染需要加载的资源体积) = html + js + css

1.1 Напишите что-нибудь в корневом узле

Мы можем полностью продвинуть время рендеринга первого экрана, например, писать что-то в вашем корневом узле:

<div class="root">Loading...</div>

Это так просто, вы можете увеличить время первого экрана вашего приложения до html и css после загрузки.

В настоящее время:

首屏体积 = html + css

Конечно, строка нестилизованного текста «Загрузка...» может заставить дизайнера захотеть вас побить. Чтобы избежать побоев, мы можем нарисовать содержимое в корневом узле, чтобы оно выглядело лучше:

<div id="root">
    <!-- 这里画一个 SVG -->
</div>

1.2 Используйте html-webpack-plugin для автоматической вставки загрузки

В реальном бизнесе должно быть много-много страниц. Очевидно, что копировать и вставлять такое состояние загрузки вручную для каждой страницы слишком неэлегантно. В настоящее время мы можем рассмотреть возможность использованияhtml-webpack-pluginчтобы помочь нам вставить загрузку автоматически.

var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');

// 读取写好的 loading 态的 html 和 css
var loading = {
    html: fs.readFileSync(path.join(__dirname, './loading.html')),
    css: '<style>' + fs.readFileSync(path.join(__dirname, './loading.css')) + '</style>'
}

var webpackConfig = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'xxxx.html',
      template: 'template.html',
      loading: loading
    })
  ]
};

Затем укажите его в шаблоне:

<!DOCTYPE html>
<html lang="en">
    <head>
        <%= htmlWebpackPlugin.options.loading.css %>
    </head>

    <body>
        <div id="root">
            <%= htmlWebpackPlugin.options.loading.html %>
        </div>
    </body>
</html>

1.3. Используйте prerender-spa-plugin для рендеринга в верхней части сгиба

В некоторых относительно больших проектах Loading может сам быть компонентом React/Vue.Без рендеринга на стороне сервера было бы очень сложно написать компонентную загрузку непосредственно в html-файл, но все же есть решения.Метод.

prerender-spa-pluginЭто плагин веб-пакета, который может помочь вам сгенерировать первый экран html страницы во время создания.Принцип примерно следующий:

  1. Укажите каталог dist и путь для рендеринга
  2. Плагин запускает статический сервер в каталоге dist и использует безголовый браузер (puppeteer), чтобы получить доступ к соответствующему пути, выполнить JS и получить HTML-код соответствующего пути.
  3. Запишите захваченный контент в html, чтобы даже без рендеринга на стороне сервера добиться почти такого же эффекта, как рендеринг на стороне сервера (без учета динамических данных)

Как его использовать, см.эта статья

plugins: [
  new PrerenderSpaPlugin(
    path.join(__dirname, 'dist'),
    [ '/', '/products/1', '/products/2', '/products/3']
  )
]

1.4. Удалить внешний css

До сих пор наш первый экран volume = html + css, еще есть место для оптимизации, то есть убрать css внешней цепочки, чтобы браузер мог отрисовывать первый экран при загрузке html.

На самом деле у webpack по умолчанию нет внешнего css, ничего делать не нужно. Конечно, если ваш проект был настроен ранееextract-text-webpack-pluginилиmini-css-extract-pluginСоздание отдельного файла css можно удалить напрямую.

Некоторые люди могут задаться вопросом, что помещение css в пакет js потеряет многие преимущества кэширования браузера (например, если вы измените только код js, содержимое сконструированного js изменится, но css будет перезагружен вместе), так оно того действительно стоит?

Это правда, что css нельзя кэшировать, но на самом деле для зрелых интерфейсных приложений кэширование не должно быть дифференцировано по размеру js/css, а должно быть дифференцировано по «компонентам», то есть компонентам кэширования с динамическим Импортировать.

Как вы увидите дальше, преимущества CSS в режиме js намного перевешивают такой небольшой недостаток.


2. Первый экран -> первый рендеринг контента

Во время этого процесса браузер в основном загружает и запускает JS-код, поэтому ключом к оптимизации стало повышение производительности загрузки и выполнения JS-кода.

Почти весь бизнес-код JS можно условно разделить на следующие большие блоки:

  1. базовая структура, такие как React, Vue и т. д. Код этих базовых фреймворков не меняется, если только фреймворк не обновляется;
  2. Polyfill, для проектов, использующих синтаксис ES2015+, для совместимости необходимы полифиллы;
  3. Библиотека бизнес-базы, некоторые общие базовые коды бизнеса, которые не относятся к фреймворку, но используются большинством бизнесов;
  4. Бизнес-код, который характеризуется логическим кодом самого конкретного бизнеса.

Оптимизировать производительность этого временного промежутка, то есть оптимизировать скорость загрузки вышеуказанных четырех ресурсов.


2.1 Инфраструктура кэширования

Характеристики базового кода фреймворка:обязательныйипостоянный, который хорошо подходит для кэширования.

Итак, все, что нам нужно сделать, этоУстановите максимально возможное время кеша для базового кода фреймворка., чтобы браузер пользователя пытался загрузить эти ресурсы через кеш.

Приложение: Сводка ресурсов кэша HTTP

HTTP предоставляет нам несколько решений для кэширования.Подведем итоги:

1. expires

expires: Thu, 16 May 2019 03:05:59 GMT

Установите время истечения в заголовке http. До этого времени истечения запрос браузера не будет отправлен, но файл будет автоматически считан из кеша, если только кеш не будет очищен или принудительно обновлен. Недостатком является то, что могут быть несоответствия между временем сервера и временем клиента, поэтому добавлен HTTP/1.1.cache-controlголову, чтобы улучшить этот вопрос.

2. cache-control

cache-control: max-age=31536000

Установите время истечения срока действия (в секундах), в пределах этого временного диапазона запросы браузера будут напрямую читать кеш. когдаexpiresиcache-controlкогда оба существуют,cache-controlболее высокий приоритет.

3. last-modified / if-modified-since

Это набор заголовков запроса/ответа.

Заголовок ответа:

last-modified: Wed, 16 May 2018 02:57:16 GMT

Заголовок запроса:

if-modified-since: Wed, 16 May 2018 05:55:38 GMT

Когда сервер возвращает ресурс, если заголовокlast-modified, то при следующем запросе ресурса значение будет добавлено в заголовок запросаif-modified-since, сервер может сравнить это значение, чтобы определить, изменился ли ресурс, и вернуть 304, если изменений нет.

4. etag / if-none-match

Это также набор заголовков запроса/ответа.

Заголовок ответа:

etag: "D5FC8B85A045FF720547BC36FC872550"

Заголовок запроса:

if-none-match: "D5FC8B85A045FF720547BC36FC872550"

Принцип аналогичен, когда сервер возвращает ресурс, если заголовокetag, то при следующем запросе ресурса значение будет добавлено в заголовок запросаif-none-match, сервер может сравнить это значение, чтобы определить, изменился ли ресурс, и вернуть 304, если изменений нет.

Приоритет четырех вышеперечисленных кешей: cache-control > expires > etag > last-modified


2.2 Использование динамических полифиллов

Полифилл характеризуетсяне требуетсяипостоянный, т.к. для мобильного телефона, какие полифиллы требуются фиксировано, конечно, полифиллы могут вообще не понадобиться.

Сейчас для совместимости браузеров мы часто вводим различные полифиллы, но есть некоторые проблемы со статическим введением полифилов при построении.Например, для пользователей с относительно новыми моделями и версиями браузеров полифилы вообще не нужны, а полифилы вводятся Это избыточно для этой части пользователей, что приводит к увеличению размера и потере производительности.

Например, код React 16 опирается на объекты ES6 Map/Set. При их использовании вам нужно добавлять полифиллы самостоятельно. Однако текущие полифиллы нескольких полных Map/Sets относительно велики, и их упаковка увеличит объем.

Другим примером является объект Promise, который на самом деле основан наcaniuse.comЧто касается мобильных устройств, почти 94% пользовательских браузеров в Китае изначально поддерживают промисы и не нуждаются в полифиллах. Но на самом деле, когда мы упаковываем, мы все равно упаковываем полифилл Promise, то естьМы увеличили размер загрузки для 94% пользователей для совместимости пользователей с 6%.

Таким образом, решение здесь состоит в том, чтобы удалить статический полифилл в сборке и использовать вместо негоpolyfill.ioТакой динамический полифилл-сервис,Убедитесь, что полифиллы вводятся только при необходимости.

Конкретный метод использования очень прост, нужно только связать js:

<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>

Конечно, это для загрузки всех полифилов, на самом деле вам может не понадобиться столько, например, если вам нужны только Map/Set:

<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Map,Set"></script>

Принцип динамического полифилла

Если вы перейдете по этой ссылке в последней версии браузера Chrome:CDN.poly fill.IO/V2/poly fill…, вы обнаружите, что содержимое почти пусто:

Если вы откроете консоль, смоделируете Safari для iOS и зайдете снова, вы обнаружите, что в ней появляются некоторые полифиллы (полифиллы для объектов URL):

Этоpolyfill.ioПо принципу заголовка UA вашего браузера он определит, поддерживаете ли вы те или иные функции, и вернет вам соответствующий полифилл. Для последней версии браузера Chrome полифилл не требуется, поэтому возвращаемый контент пуст. Для iOS Safari требуется полифилл объекта URL, поэтому возвращается соответствующий ресурс.


2.3. Используйте SplitChunksPlugin для автоматического разделения базовой бизнес-библиотеки

Webpack 4 отказался от оригиналаCommonChunksPlugin, замененный более совершеннымSplitChunksPlugin, для извлечения общего кода.

Разница между ними в том, что CommonChunksPlugin может найти большинство общих модулей и извлечь их (common.js), а это значит, что если вы загружаете common.js, то может быть какой-то текущий модуль не нуждается в вещах.

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

Ниже приведен простой пример Предположим, у нас есть 4 чанка, которые зависят от следующих модулей:

chunk Зависимые модули
chunk-a react, react-dom, componentA, utils
chunk-b react, react-dom, componentB, utils
chunk-c angular, componentC, utils
chunk-d angular, componentD, utils

Если это предыдущий CommonChunksPlugin, то конфигурация по умолчанию упаковывает их следующим образом:

Имена пакетов Включены модули
common utils
chunk-a react, react-dom, componentA
chunk-b react, react-dom, componentB
chunk-c angular, componentC
chunk-d angular, componentD

Очевидно, что общие модули, такие как react, react-dom и angular, не были выделены в независимые пакеты, и есть место для дальнейшей оптимизации.

Теперь новый плагин SplitChunksPlugin упакует их в следующие пакеты:

Имена пакетов Включенные модули
chunk-a~chunk-b~chunk-c~chunk-d utils
chunk-a~chunk-b react, react-dom
chunk-c~chunk-d angular
chunk-a componentA
chunk-b componentB
chunk-c componentC
chunk-d componentD

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

Подробнее о том, как настроить SplitChunksPlugin, см.официальная документация по веб-пакету.

Примечание. Ямы, которые в настоящее время существуют с использованием SplitChunksPlugin

Хотя плагин SplitChunksPlugin, предоставляемый webpack 4.0, очень полезен, на момент написания этой статьи (май 2018 г.)html-webpack-plugin еще не полностью поддерживает SplitChunksPlugin, сгенерированный пакет общедоступного модуля нельзя автоматически внедрить в html.

Вы можете обратиться к следующему выпуску или PR:

2.4. Правильное использование Tree Shake для уменьшения размера бизнес-кода

Tree Shaking — это функция веб-пакета, которая существует очень-очень давно, и это клише, но на самом деле не все люди (особенно те, кто плохо знает веб-пакет) используют ее правильно, поэтому я расскажу о это здесь сегодня Напишите это снова.

Например, у нас есть следующий модуль, использующий стандарт модуля ES:

// math.js
export function square(x) {
  return x * x
}

export function cube(x) {
  return x * x * x
}

Затем вы ссылаетесь на него в другом модуле:

// index.js
import { cube } from './math'
cube(123)

После упаковки webpack,math.jsстанет следующим:

/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__["a"] = cube;
function square(x) {
  return x * x;
}

function cube(x) {
  return x * x * x;
}

обратите внимание здесьsquareФункция все еще существует, но с дополнительной строкой магического комментария:unused harmony export square

Последующий uglifyJS сжатого кода распознает эту строку магического комментария и поместитsquareфункция сбрасывается.

Но будь осторожен! ! !Webpack 2.0 начал изначально поддерживать ES-модуль, а это значит, что Babel не нужно конвертировать ES-модуль в прежний модуль commonjs,Чтобы использовать Tree Shaking, обязательно отключите экранирование модуля Babel по умолчанию:

{
  "presets": [
    ["env", {
      "modules": false
      }
    }]
  ]
}

Кроме того, начиная с Webpack 4.0, Tree Shaking будет работать и для тех модулей, которые не имеют побочных эффектов.

Если один из ваших модулей находится вpackage.jsonЭтот модуль не имеет побочных эффектов (то есть выполнение кода в нем не окажет никакого влияния на окружение, вроде просто объявления некоторых функций и констант):

{
  "name": "your-module",
  "sideEffects": false
}

Затем, когда этот модуль вводится, но не используется, webpack автоматически отбрасывает его Tree Shaking:

import yourModule from 'your-module'
// 下面没有用到 yourModule

Это особенно важно для библиотек инструментов, таких как lodash и underscore.После включения этой функции вы можете написать это без какой-либо психологической нагрузки:

import { capitalize } from 'lodash-es';
document.write(capitalize('yo'));

3. Первый рендеринг контента -> интерактивный

Во время этого процесса главное, что делает браузер, этоЗагрузите и инициализируйте компоненты

3.1. Code Splitting

Что делают большинство сборщиков (таких как webpack, rollup, browserify), так это объединяют код вашей страницы в один большой «пакет», где будет находиться весь код. Однако с увеличением сложности приложения размер бандла также будет увеличиваться, а также будет увеличиваться время загрузки бандла, что оказывает большое негативное влияние на пользовательский опыт в процессе загрузки.

Чтобы избежать слишком больших пакетов, все, что нам нужно сделать, это разделить код, т.е.Code Splitting, почти все упаковщики в настоящее время изначально поддерживают эту функцию.

Разделение кода может помочь вам "лениво загрузить" код, чтобы улучшить загрузку пользователя. Если вы не можете напрямую уменьшить размер приложения, вы можете попытаться разделить приложение из одного пакета на один пакет + несколько копий динамического код.

Например, мы можем принять следующую форму:

import { add } from './math';
console.log(add(16, 26));

Перепишите его в виде динамического импорта, чтобы он не загружался при первой загрузкеmathмодуль, тем самым уменьшив размер первого загруженного ресурса.

import("./math").then(math => {
  console.log(math.add(16, 26));
});

React LoadableЭто высокоуровневый компонент React, специально используемый для динамического импорта. Вы можете переписать любой компонент для поддержки динамического импорта.

import Loadable from 'react-loadable';
import Loading from './loading-component';

const LoadableComponent = Loadable({
  loader: () => import('./my-component'),
  loading: Loading,
});

export default class App extends React.Component {
  render() {
    return <LoadableComponent/>;
  }
}

Когда приведенный выше код загружается в первый раз, он сначала отобразитloading-component, затем динамически загружайтеmy-componentПосле загрузки кода компонента он будет замененloading-component.

Вот конкретный пример:

Взяв в качестве примера эту домашнюю страницу пользователя, есть по крайней мере три компонента, которые не нужно загружать в первый раз, а используют динамическую загрузку: строка заголовка, панель вкладок и список. На самом деле первой загрузке нужно загрузить только аватар пользователя, псевдоним и идентификатор в центральной области. После разделения размер js в верхней части страницы уменьшается с 40 КБ до 20 КБ.

3.2. Компиляция в ES2015+ для повышения эффективности кода

Статьи по Теме:«Развертывание кода ES2015+ в производстве сегодня»

Сегодня большинство проектов пишут код ES2015+ и компилируют его в ES5 во время сборки.

Например, очень лаконичный синтаксис класса:

class Foo extends Bar {
    constructor(x) {
        super()
        this.x = x;
    }
}

будет компилироваться так:

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var Foo = function (_Bar) {
  _inherits(Foo, _Bar);

  function Foo(x) {
    _classCallCheck(this, Foo);

    var _this = _possibleConstructorReturn(this, (Foo.__proto__ || Object.getPrototypeOf(Foo)).call(this));

    _this.x = x;
    return _this;
  }

  return Foo;
}(Bar);

Но на самом деле большинство современных браузеров уже изначально поддерживают синтаксис классов. Например, iOS Safari поддерживает его с версии iOS 9.0 в 2015 году.

То же самое верно и для других функций ES2015.

Другими словами, в 2018 году для большинства пользователей вообще не нужно компилировать код в ES5, он не только громоздкий, но и медленно работает.Все, что нам нужно сделать, это скомпилировать код в ES2015+ и сохранить стандартный запас ES5 для нескольких пользователей со старыми браузерами.

Конкретное решение<script type="module">Этикетка.

служба поддержки<script type="module">Браузеры должны поддерживать следующие функции:

  • async/await
  • Promise
  • Class
  • Функции со стрелками, карта/установка, выборка и т. д.

не поддерживается<script type="module">Старые браузеры не будут загружать код ES2015+, потому что они не распознают этот тег. Кроме того, старые браузеры не распознают его.nomoduleЗнакомый, он будет автоматически проигнорирован, тем самым загружая стандартный код ES5.

Это просто резюмируется в виде следующего рисунка:

в соответствии сэта статья, объем упаковки и эффективность работы были значительно улучшены:

4. Интерактивный -> Контент загружен

Этот этап очень прост, в основном это загрузка различного мультимедийного контента.

4.1. LazyLoad

Про ленивую загрузку, собственно, и говорить нечего, на данный момент есть несколько относительно зрелых компонентов, и реализовать их самостоятельно не составляет особого труда:

Конечно, вы также можете добиться опыта загрузки, подобного Medium (кажется, это уже имеет место в Zhihu), то есть сначала загрузить низкопиксельное размытое изображение, а затем заменить его после загрузки реального изображения.

По сути, почти все компоненты lazyload в настоящее время представляют собой не более чем следующие два принципа:

  • Слушайте событие прокрутки оконного объекта или родительского объекта и инициируйте загрузку;
  • использоватьIntersection Observer APIчтобы получить видимость элемента.

4.2. placeholder

Когда мы загружаем текст и картинки, часто возникает ситуация "заставки". Например, картинка или текст еще не загружены. В это время соответствующая позиция на странице еще полностью пуста. После загрузки содержимое внезапно откроет страницу. , что приведет к появлению «экрана-заставки», что приведет к неприятному опыту.

Чтобы избежать этого внезапного открытия, все, что нам нужно сделать, это заранее установить элемент-заполнитель, то есть заполнитель:

Уже доступны некоторые готовые сторонние компоненты:

Также ознакомьтесь с этой статьей в Facebook:«Как работает заполнитель контента Facebook»

V. Резюме

В этой статье мы упомянули следующие моменты для оптимизации загрузки:

  1. Реализовать состояние загрузки или скелетный экран в HTML;
  2. удалить внешний css;
  3. Базовая структура кэша;
  4. Используйте динамические полифиллы;
  5. Используйте SplitChunksPlugin для разделения общего кода;
  6. Правильное использование Tree Shaking Webpack 4.0;
  7. Используйте динамический импорт, чтобы разделить код страницы и уменьшить размер JS в верхней части страницы;
  8. Скомпилируйте в ES2015+, повысьте эффективность выполнения кода и уменьшите размер;
  9. Используйте отложенную загрузку и заполнитель, чтобы улучшить процесс загрузки.

На самом деле пунктов, которые можно оптимизировать, гораздо больше, чем перечисленные ниже.

Надеюсь, эта статья поможет сохранить ваши KPI во второй половине года :)


Технологический еженедельник IVWEBШок в сети, обратите внимание на публичный номер: сообщество IVWEB, регулярно каждую неделю публикуйте качественные статьи.

  • Сборник статей еженедельника:weekly
  • Командные проекты с открытым исходным кодом:Feflow