[Перевод] Профилирование производительности React-приложений

Сегодня поговорим об измерении производительности рендеринга React-компонентов с использованием API React Profiler. Ещё мы будем оценивать взаимодействия с компонентом, применяя новый экспериментальный API Interaction Tracing. Кроме того, мы воспользуемся API User Timing для проведения собственных измерений.

В качестве площадки для экспериментов воспользуемся приложением React Movies Queue.

5ef45251ae6a761a3fec98e14c4eefa1.jpg


Приложение React Movies Queue

API React Profiler


API React Profiler предназначен для оценки скорости работы рендеринга и помогает выявлять узкие места производительности приложений.

import React, { Fragment, unstable_Profiler as Profiler} from "react";


Компонент Profiler принимает коллбэк onRender в виде свойства. Он вызывается каждый раз, когда компонент в профилируемом дереве фиксирует обновление.

const Movies = ({ movies, addToQueue }) => (
  
    


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

10a9d03ece4b7636876ab9b72998dea1.jpg


Приложение React Movies Queue и исследование Movies с помощью инструментов разработчика React

Коллбэк onRender принимает параметры, которые описывают то, что рендерится, и время, необходимое на рендеринг. Сюда входит следующее:

  • id: Свойство id из дерева компонента Profiler, для которого было зафиксировано изменение.
  • phase: или mount (если дерево было смонтировано), или update (если дерево было повторно отрендерено).
  • actualDuration: время, затраченное на рендеринг зафиксированного обновления.
  • baseDuration: предполагаемое время рендеринга всего поддерева без кеширования.
  • startTime: время, когда React начал рендерить это обновление.
  • commitTime: время, когда когда React зафиксировал это обновление.
  • interactions: множество «взаимодействий» для данного обновления.
const callback = (id, phase, actualTime, baseTime, startTime, commitTime) => {
    console.log(`${id}'s ${phase} phase:`);
    console.log(`Actual time: ${actualTime}`);
    console.log(`Base time: ${baseTime}`);
    console.log(`Start time: ${startTime}`);
    console.log(`Commit time: ${commitTime}`);
}


Загрузим страницу и перейдём в консоль инструментов разработчика Chrome. Там мы должны увидеть следующее.

cc8baf9c9500419f587874bae69acf96.jpg


Результаты профилирования в инструментах разработчика

Мы, кроме того, можем открыть инструменты разработчика React, перейти на закладку Profiler и визуализировать сведения о времени рендеринга компонентов. Ниже показана визуализация этого времени в виде flame-графика.

c56138d6154866df33b45014966aa2cb.jpg


Работа с результатами профилирования в инструментах разработчика React

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

2243241b6de115dbbd7c0a94270d3868.jpg


Просмотр результатов профилирования в режиме Ranked

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

import React, { Fragment, unstable_Profiler as Profiler} from "react";

render(
  
    
      
                         );


А как проанализировать взаимодействия пользователей с компонентами?

API Interaction Tracing


Хорошо бы иметь возможность отслеживать взаимодействия пользователя с интерфейсом приложения (вроде щелчков по элементам). Это позволит находить ответы на интересные вопросы вроде такого: «Сколько времени понадобилось на обновление DOM после щелчка по этой кнопке?». К нашему счастью, в React есть экспериментальная поддержка анализа взаимодействий пользователя с приложением с использованием API Interaction Tracing из нового пакета scheduler. Документацию по нему можно почитать здесь.

Сведения о взаимодействиях пользователя и приложения снабжают описаниями (например — «Пользователь щёлкнул по кнопке Добавить в корзину») и отметками времени. Кроме того, при настройке анализа взаимодействий используются коллбэки, в которых выполняются действия, соответствующие тому или иному взаимодействию.

В нашем приложении есть кнопка Add Movie To Queue, на которой выводится значок +. Она служит для добавления фильмов в очередь просмотра.

6f775caf25ad484e73ae70354752b879.jpg


Кнопка для добавления фильма в очередь просмотра

Вот пример кода, отслеживающего обновления состояния для этого взаимодействия пользователя с приложением:

import { unstable_Profiler as Profiler } from "react";
import { render } from "react-dom";
import { unstable_trace as trace } from "scheduler/tracing";

class MyComponent extends Component {
  addMovieButtonClick = event => {
    trace("Add To Movies Queue click", performance.now(), () => {
      this.setState({ itemAddedToQueue: true });
    });
  };
}


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

d93fc1ea00661d926e2f9a183b32ebb7.jpg


Анализ взаимодействия пользователя с элементом приложения

С помощью API Interaction Tracing можно, кроме того, собрать сведения о первом рендеринге компонента:

import { unstable_trace as trace } from "scheduler/tracing";

trace("initial render", performance.now(), () => {
   ReactDom.render(, document.getElementById("app"));
});
b3c4ab310fd105fc83697e4a7b5a61fb.jpg


Анализ первого рендеринга компонента

Автор API приводит и другие примеры его использования. Например, иллюстрирующие профилирование асинхронных задач.

Puppeteer


Для автоматизации тестирования взаимодействия пользователя с элементами приложения интересным может показаться применение Puppeteer. Это — Node.js-библиотека, которая даёт доступ к высокоуровневому API, предназначенному для управления браузером Chrome без пользовательского интерфейса с использованием протокола DevTools.

При использовании Puppeteer в распоряжении разработчика оказываются вспомогательные методы tracing.start() и tracing.stop(), предназначенные для сбора показателей производительности DevTools. Ниже показан пример использования этих механизмов для сбора данных о том, что происходит при щелчке по интересующей нас кнопке.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  const navigationPromise = page.waitForNavigation();
  await page.goto('https://react-movies-queue.glitch.me/')
  await page.setViewport({ width: 1276, height: 689 });
  await navigationPromise;

  const addMovieToQueueBtn = 'li:nth-child(3) > .card > .card__info > div > .button';
  await page.waitForSelector(addMovieToQueueBtn);

  // Запуск профилирования...
  await page.tracing.start({ path: 'profile.json' });
  // Щелчок по кнопке
  await page.click(addMovieToQueueBtn);
  // Остановка профилирования
  await page.tracing.stop();

  await browser.close();
})()


Теперь, загрузив файл profile.json в панель Performance инструментов разработчика, мы можем видеть то, вызов каких функций инициировало нажатие на кнопку.

3ed42f62350db7d110d4ba53c80973b6.jpg


Анализ последствий нажатия на кнопку

Если вам интересна тема анализа производительности компонентов — взгляните на этот материал.

API User Timing


API User Timing позволяет разработчику создавать собственные метрики производительности, используя высокоточные отметки времени. Метод window.performance.mark() создаёт отметку времени, которой назначается имя. Метод window.performance.measure() позволяет узнать время, прошедшее между двумя измерениями.

// Запись времени перед началом задачи
performance.mark('Movies:updateStart');
// Решение задачи

// Запись времени после окончания задачи
performance.mark('Movies:updateEnd');

// Нахождение разницы между временем начала и окончания задачи
performance.measure('moviesRender', 'Movies:updateStart', 'Movies:updateEnd');


Профилируя React-приложением с использованием закладки Performance инструментов разработчика Chrome, можно найти раздел Timing, заполненный временными показателями, касающимися React-компонентов. React, во время рендеринга, умеет публиковать эти сведения с использованием API User Timing.

ed0f07ebb5939839c088bc5d56e4b330.jpg


Закладка Performance инструментов разработчика Chrome

Обратите внимание на то, что из DEV-сборок React убирают API User Timings, заменяя его на API React Profiler, который предоставляет более точные отметки времени. Возможно, в будущем поддержку этого API вернут, сделав это для тех браузеров, которые поддерживают спецификацию User Timing Level 3. 

В интернете вам могут попасться React-сайты, которые используют API User Timing для определения собственных метрик. Сюда, например, входит метрика Reddit Time to first post title visible и метрика Spotify Time to playback ready.

18a28ac19bf3b14050cde0731cf09e71.jpg


Особые метрики, используемые на React-сайтах

Показатели метрик, созданных средствами API User Timing, удобно выводятся в панели Lighthouse инструментов разработчика Chrome.

9b1b979616527fd88511ecbadcd0a260.jpg


Метрики в панели Lighthouse

Например, в состав свежих версий Next.js включены пользовательские метрики и механизмы измерения множества различных событий. В том числе — следующих:

  • Next.js-hydration: время, необходимое для приведения заранее отрендеренной разметки в рабочее состояние.
  • Next.js-nav-to-render: время от начала навигации до начала рендеринга.


Все эти измерения выводятся в области Timings.

2488c5ee9d6bd6ccf9da8e6c36436c26.jpg


Анализ метрик Next.js

Инструменты разработчика и Lighthouse


Напоминаю, что Lighthouse и панель Performance инструментов разработчика Chrome можно использовать для глубокого анализа процесса загрузки и производительности React-приложений. Здесь можно найти метрики, особенно сильно влияющие на восприятие страниц пользователями.

ce25c72eedd5c79edd5dbc1a42685b10.jpg


Анализ производительности страницы

Тем, кто работает с React, может понравиться то, что в их распоряжении окажутся новые метрики — вроде показателя Total Blocking Time (TBT), который даёт сведения о том, сколько времени страница пребывает в неинтерактивном режиме до того момента, когда она сможет надёжно работать в интерактивном режиме (Time to Interactive). Вот — показатели TBT («до» и «после») для приложения, в котором применяется экспериментальный конкурентный режим, использование которого помогает приложению адаптироваться к особенностям среды, в которой оно выполняется.

9b707496d500da0883027da3a571a030.jpg


Изменение TBT

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

98b3ac8031d84cdfb9765fde2f399a70.jpg


Анализ приложения

Lighthouse, кроме того, даёт React-разработчикам массу специфических советов по множеству вопросов. Ниже показан результат анализа в Lighthouse 6.0. Здесь открыт раздел Remove unused JavaScript, в котором сообщается о неиспользуемом JavaScript-коде, загружаемом в приложение, который может быть импортирован с использованием React.lazy().

1e39b2f3a323a74744ee16e68550c6c2.jpg


Анализ приложения в Lighthouse

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

Уважаемые читатели! Как вы исследуете производительность своих React-приложений?

iqfib45pgphfrxv--zfemt0qnmw.jpeg

© Habrahabr.ru