- Измерение производительности на уровне компонентов
- Измерение производительности на уровне приложения
Измерение производительности на уровне компонентов
Если попробовать обобщить концепцию алгоритма согласования (reconciliation) React или «virtual DOM»
в одном предложении, то это будет звучать следующим образом: React предпринимает шаги для проведения различия между более новым деревом DOM и старым деревом, чтобы выяснить, что необходимо обновлять в пользовательском интерфейсе, когда данные изменяются внутри компонента. Это значительно снижает нагрузку на производительность, нежели повторный рендеринг всего приложения при каждом изменении состояния (разница между O(N3) и O(N)).
В статье Дена Абрамова «React as UI Runtime» довольно хорошо объясняется согласование (reconciliation).
Даже с учетом этих оптимизаций, встроенных во внутренние компоненты React, ты всегда можешь столкнуться с проблемой повторного рендеринга компонентов в приложении. Это может быть незаметно для небольших приложений, но может значительно снизить производительность, если на странице отображаются сотни компонентов.
Существует ряд причин по которым компоненты рендерятся, когда не должны: функции внутри компонента могут быть не настолько эффективны, как они могли бы быть, или, может быть так что весь список компонентов перерисовывается, когда в него добавляется только один компонент. Существуют инструменты, которые ты можешь использовать для определения того, какие деревья компонентов отрисовываются слишком долго, например:
- Chrome DevTools Performance панель;
- React DevTools Profiler.
Анализ производительности с помощью панели Chrome DevTools Performance
React использует User Timing API для измерения времени, которое компонент занимает на каждом этапе своего жизненного цикла. Ты можешь запустить трассировку производительности с помощью Chrome DevTools
, чтобы проанализировать насколько эффективно твои компоненты собираются, отображаются и отключаются во время взаимодействия страниц или перезагрузок.
У Бена Шварца есть отличный учебник, в котором подробно об этом рассказывается - Profiling React performance with React 16 and Chrome Devtools.
User Timing API
используется только во время разработки и отключен в production
режиме. Одной из причин создания более нового Profiler API
стала необходимость в более быстром его внедрении и возможности использовать в продакш версии приложения без существенного снижения производительности.
Анализ производительности при помощи React DevTools Profiler
C выходом react-dom 16.5
стала доступна более новая панель Profiler
в React DevTools
, которая помогает отслеживать насколько хорошо React компоненты воспроизводятся с точки зрения производительности. Это достигается при помощи Profiler API для сбора информации о времени для каждого компонента, который выполняет ре-рендер.
Панель Profiler
находится в отдельной вкладке React DevTools. Подобно панели Performance
в Chrome DevTools, ты можешь записывать взаимодействия пользователя и перезагрузки страниц, чтобы проанализировать насколько хорошо работают твои компоненты.
Другие виды диаграмм (ranked, component) также доступны для просмотра в Profiler. Чтобы получить более подробную информацию об этом, необходимо прочитать статью Introducing the React Profiler в документации React.
Минимизируй ненужные повторные рендеры
Существует несколько способов свести к минимуму или удалить ненужные повторные рендеры в приложении React:
- Переопределить shouldComponentUpdate для
class
компонентов
shouldComponentUpdate(nextProps, nextState) {
// возвращает true, только если выполнено определенное условие.
}
- Использовать PureComponent для
class
компонентов
import React, { PureComponent } from "react";
class MyComponent extends PureComponent {}
- Использовать memo для функциональных компонентов
import React, { memo } from "react";
const MyComponent = memo((props) => {});
- Memoize селекторов Redux (например с reselect)
- Отображение длинных списков (например, при помощи react-window)
Есть два видео Brian Vaughn, которые стоит посмотреть, если есть желание узнать больше об использовании Profiler
:
Измерение производительности на уровне приложения
Помимо специфических мутаций DOM и повторного рендеринга компонентов, существуют и другие проблемы более высокого уровня, на которые стоит обратить внимание. Lighthouse позволяет легко анализировать и оценивать работу конкретного сайта.
Существует три способа запуска тестов Lighthouse
на веб-странице:
- Node CLI;
- Chrome Extension;
- непосредственно через
Chrome DevTools
в панели Lighthouse (Audits).
Lighthouse
обычно занимает немного времени, собирая все необходимые данные со страницы, а затем проверяя их по ряду проверок. После этого он генерирует окончательной отчет со всей информацией.
Разделение JavaScript пакета
Один из способов разделения кода - использовать динамический импорт:
import("lodash.sortby")
.then((module) => module.default)
.then((module) => doSomething(module));
Синтаксис импорта может выглядеть как вызов функции, но он позволяет асинхронно импортировать любой модуль, когда возвращается Promise
. В этом примере импортируется метод sortby
из lodash
; затем запускается doSomething
.
Динамический импорт является относительно новым синтаксисом и в настоящее время находится на стадии 4 процесса TC39. Он будет доступен в es2020. Синтаксис уже поддерживается в Chrome и Safari, а также в пакетах модулей, таких как Webpack, Rollup и Parcel.
В React были созданы абстракции, которые помогают сделать процесс разделения на уровне компонентов проще. React.lazy является одним из них:
import React, { lazy } from "react";
const MyComponent = lazy(() => import("./MyComponent"));
Вот здесь ты можешь ознакомится с небольшим обзором о code-splitting
.
Одной из основных проблем асинхронной загрузки различных частей приложения является обработка задержки, с которой может столкнуться пользователь. Для этого можем использовать компонент Suspense для «приостановки» рендеринга определенного дерева компонентов. Используя его вместе с React.lazy
, индикатор загрузки может отображаться как запасной вариант (fallback), пока компонент ещё грузится:
import React, { lazy, Suspense } from "react";
import LoadingComponent from "./LoadingComponent";
const MyComponent = lazy(() => import("./MyComponent"));
const PageComponent = () => (
<Suspense fallback={LoadingComponent}>
<MyComponent />
</Suspense>
);
Suspense
ещё не работает, если UI собирается на сервере. Если ты хочешь разделить код в приложении React на стороне сервера - используй библиотеку, например loadable-components, как это предлагается в документации React.
import React from "react";
import loadable from "@loadable/component";
const OtherComponent = loadable(() => import("./OtherComponent"));
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}
Чтобы loadable-components
работали с SSR
тебе нужно настроить несколько вещей.
С чего начать code splitting
?
Самый простой способ начать c React документации. Она объясняет, как code splitting
может работать с React Router
и Suspense
.
Кэшируй вещи, которые стоит кешировать
Service worker
- это веб-работник, который работает в фоновом режиме твоего браузера при просмотре веб-страницы.
Идея service-workers
заключается в том, чтобы включить отдельные функции в отдельный поток, который может улучшить пользовательский опыт. Он включает в себя кэширование важных файлов. Так что когда пользователи совершают повторное посещение, браузер может отправлять запрос к service-workers
, а не на сервер, что повышает скорость загрузки страницы при повторных посещениях.
Workbox - это набор библиотек, которые могут упростить включение service-workers
, фактически не создавая его самому с нуля.
Streaming SSR
Основная идея рендеринга на стороне сервера (SSR) в клиентском приложении заключается в отправке HTML-документа с сервера, который клиент в конечном итоге создал бы. Это позволяет отображать контент пользователям гораздо раньше, чем если бы они ожидали завершения сборки на клиенте.
Чтобы убедиться, что все работает хорошо, тебе нужно чтоб браузер повторно использует DOM, отображаемый на сервере, вместо того, чтобы заново создавать разметку, например с помощью hydrate(). Он делает так, как будто страница загружается быстрее, но может вызвать задержку во времени, необходимую для того, чтобы страница стала интерактивной.
С React 16 ты можешь использовать потоковую передачу (streaming) при рендеринге компонентов на стороне сервера. Вместо использования renderToString
для возврата строки HTML, ты можешь использовать renderToNodeStream
для возврата Readable
потока байтов. Потоковая передача с сервера позволяет клиенту получать и обрабатывать различные части документа HTML вместо всех сразу.
Если используешь React для создания статического сайта - используй renderToStaticNodeStream.
Предварительный рендеринг
Определение SSR немного размыто. Существует много разных способов, с помощью которых серверы могут отправлять часть статического контента, необходимого для определенного URL-адреса, и то, как клиент может его обрабатывать. Предварительный рендеринг (или статический рендеринг) является промежуточным звеном между полноценным рендерингом страниц на стороне сервера и рендерингом на стороне клиента. Этот подход обычно включает создание HTML-страниц для каждого маршрута во время сборки и передачу его пользователю, пока пакет JavaScript завершает компиляцию.
Если ты хочешь внедрить предварительный рендеринг в свое приложение, такие библиотеки как react-snap, использующие Puppeteer, могут упростить тебе задачу.
У Jason Miller и Addy Osmani есть отличная статья, в которой рассматриваются различные способы предоставления контента на стороне сервера пользователям: Rendering on the Web.
Извлечь критические стилей CSS-in-JS
Многие разработчики в сообществе React используют библиотеки CSS-in-JS
, такие как emotion и styled-components, по множеству причин (стили в компонентной области, автоматически сгенерированные свойства селекторов, основанные на свойствах и т.д.). Если ты не осторожен, можешь столкнуться с проблемой вычисления всех стилей во время выполнения. Это означает, что стили применяются только после того, как завершается выполнение пакета JavaScript, что может вызвать отображение содержимого без стилей. Как и большинство проблем с производительностью, это является примером того, что у твоего пользователя слабое мобильное устройство или более слабое сетевое соединение.
Если ты уже включил в свое приложение рендеринг на стороне сервера, можешь исправить это, извлек критические стили. И emotion
, и styled-components
поддерживают это, где ты можешь извлечь свои стили в поток Readable
узла. Glamour имеет отдельную библиотеку, которая позволяет делать то же самое.
Извлечение критических стилей может значительно улучшить ситуацию для пользователей с более слабыми устройствами или сетевыми подключениями.
Доступность
Термин «прогрессивный» обычно означает, что была предпринята попытка обеспечить всем пользователям возможность доступа по крайней мере если не ко всему то хотя бы к некоторому контенту сайта. Если ты не уверен, что твой сайт доступен для пользователей с ограниченными возможностями, он не будет прогрессивным.
Lighthouse
- это хороший первый шаг для выявления проблем с доступностью на веб-странице. Чтобы найти проблемы, более свойственные элементам React, может помочь react-axe.
Atomic CSS
Идея Atomic стилей заключается в том, чтобы определить одноразовые классы, которые являются сфокусированными, небольшими и узнаваемыми. Например, один класс будет использоваться для добавления синего фона к кнопке:
<button class="bg-blue"> Click Me </button>
Отдельный класс будет отвечать за предоставление ему определенного количества отступов:
<button class="bg-blue pa2"> Click Me </button>
И так далее. Хотя это добавит намного больше значений к атрибуту класса для большинства твоих узлов DOM, ты получишь дополнительное преимущество, заключающееся в том, что ты не пишете столько же CSS, сколько обычно, если бы использовал библиотеку, которая настраивает все селекторы. Tachyons является одним из примеров такой библиотеки.
Определение стилей для компонентов, использующих atomic классы, стало распространенным шаблоном для многих разработчиков. Библиотеки, такие как tachyons-components, даже позволяют применять эти стили с помощью API типа styled-components
:
import styled from 'tachyons-components'
const Button = styled('button')`
bg-blue pa2
`
<Button>Click Me</Button>
Hooks
Хуки позволяют делать кучу вещей с функциональными (не классовыми) компонентами, которые ранее можно было сделать только с компонентами классов.
import { useState } from "react";
function MyComponent() {
const [name, setName] = useState("Apple");
return (
<>
<div>
<p>This is a picture of {name}</p>
<img src="avatar.png" />
</div>
<button onClick={() => setName("a banana")}>Fix name</button>
</>
);
}
Хуки могут быть полезны для:
- включения состояния в компонент без использования класса (useState);
- яключения побочных эффектов без использования класса (useEffect);
- повторного использование логики между компонентами путем создания собственных хуков.
Всё это позволяет включать логику в твои компоненты таким образом, чтобы её можно было повторно использовать в других местах твоего приложения. До хуков, для получения некоторых из этих возможностей, обычно использовалось recompose решение.
Первые несколько выступлений в React Conf подробно объясняют концепцию Hooks.
Ты можешь прочесть мой небольшой обзор о hooks
здесь.