Анализ и оптимизация React-приложений

Автор оригинала: Houssein Djirdeh
  • Перевод
Люди, вроде меня, которые борются за высокую производительность сайтов, часто тратят на это много времени. Поэтому сейчас я собираюсь раз и навсегда решить проблему низкого быстродействий веб-ресурсов, интерфейс которых написан на React. А именно, я предлагаю всем, кто это читает, сегодня же прекратить пользоваться React.



Автор материала, перевод которого мы сегодня публикуем, конечно же, шутит. Здесь речь пойдёт о том, как оптимизировать производительность React-приложений. Кстати, прежде чем начать, подумаем о том, зачем вообще нужна оптимизация сайтов. Пожалуй, можно сказать, что нужна она для того, чтобы сайтом могло бы пользоваться больше людей, чем до оптимизации.

Введение


Как оптимизировать сайт? Как можно оценить выгоду от оптимизации для пользователей сайта? И почему вообще нужно задумываться о подобных показателях?

Попытаемся ответить на эти вопросы, взглянув на самый простой способ создания React-приложений — на использование средства create-react-app (CRA). У нового проекта, созданного с помощью этого инструмента, имеются следующие зависимости:

  • Основная библиотека react, которая позволяет работать с компонентами React: 2.5 Кб.
  • Библиотека react-dom, позволяющая выводить компоненты на страницу, превращая их в структуры, подходящие для вставки в дерево DOM: 30.7 Кб.
  • Небольшой объём кода, в который входит и шаблон первого компонента: около 3 Кб.

Эти данные получены для React 16.6.3.

Для того чтобы узнать о том, сколько времени займёт загрузка нового CRA-приложения на телефоне Moto G4, можно воспользоваться сервисом WebPageTest.


Время загрузки сайта на телефоне Moto G4 с использованием разных сетей

Это приложение, разновидность «Hello, World», размещено на хостинге Firebase, исследуется его загрузка в браузере Chrome с использованием трёх типов соединений:

  • 4G (9 Мбит/с)
  • 3G (1.6 Мбит/с)
  • Медленное 3G-соединение (400 Кбит/с)

Тут нужно учитывать и сетевые задержки.

Почему я, для эксперимента, использовал Moto G4? Это — простой недорогой телефон, похожий на те телефоны, которыми, в виде основных устройств, пользуются многие люди в развивающихся странах. В 4G-сети приложение загрузилось за 2 секунды. В медленной 3G-сети для того, чтобы страница вышла в интерактивный режим работы, понадобилось более 4 секунд.

Хотя эти показатели и довольно интересны, они не особенно полезны в том случае, если вы не знаете о том, кем являются ваши пользователи. То, что вы определяете как «медленно», может полностью отличаться от того, что «медленным» считаю я или кто-то ещё. И ваше восприятие «быстрой» загрузки сайта может быть искажено тем, каким устройством и каким сетевым соединением вы пользуетесь. Если включить в этот эксперимент настольный компьютер, подключённый к интернету по проводному соединению, можно увидеть, как велико может быть различие между «быстрой» и «медленной» загрузкой сайта.


Время загрузки сайта на настольном компьютере и на Moto G4

Для того чтобы повысить производительность React-приложений, при построении которых используется библиотека React, что называется, «из коробки», намечены улучшения React DOM, которые нацелены на упрощение некоторых вещей. Так, система событий содержит множество полифиллов, которые, для многих новых браузеров, не нужны, и команда разработчиков рассматривает варианты их удаления или упрощения в тех случаях, если это возможно. Понаблюдать за этим можно здесь.

Можно ли измерить текущий уровень производительности веб-сайтов?


Типичное React-приложение может содержать множество компонентов и библиотек сторонних разработчиков. Это означает, что производительность «Hello, World»-приложения не даёт нам особенно ценных сведений о том, как загружаются настоящие приложения. Есть ли способ узнать о том, насколько высокопроизводительным является большинство сайтов, построенных с использованием некоей технологии (вроде React)?

Ответить на этот вопрос нам, возможно, может помочь ресурс HTTP Archive. Это — опенсорсная платформа, которая ориентирована на наблюдение за тем, как построен веб. Делается это путём ежемесячного обхода миллионов сайтов, анализа их с помощью WebPageTest и записи сведений о них. В эти сведения входит число запросов, метрики, касающиеся загрузки данных, размеры передаваемых данных и другие показатели.

Вот ещё один интересный инструмент — расширение для Chrome, которое называется Library-Detector-for-Chrome. Оно позволяет выяснять то, какие JavaScript-библиотеки используются на странице. Оно было недавно включено, в качестве инструмента аудита страниц, в Lighthouse. Это говорит о том, что информация, которую даёт это расширение, может быть получена для множества сайтов, сведения о которых хранятся в HTTP Archive. Это может помочь тем, кто хочет проанализировать результаты испытания тысяч сайтов, на которых используется конкретная JavaScript-библиотека (механизм определения React находится здесь).

Полный набор данных HTTP Archive общедоступен, найти его можно на BigQuery. После исследования 140000 сайтов, использующих React, на предмет их загрузки в искусственно смоделированной мобильной среде (набор данных 2019_01_01), удалось выяснить следующее:

  • Медиана показателя First Meaningful Paint (первое значимое отображение, время отрисовки важных элементов) составила 6.9 с.
  • Медиана показателя Time to Interactive (время до интерактивности, время загрузки элементов взаимодействия) составила 19.7 с.

Вы можете исследовать эти данные и самостоятельно.

Почти 20 секунд нужно на то, чтобы пользователь смог бы начать взаимодействовать с сайтом. И это, в общем-то, происходит, так сказать, здесь и сейчас, хотя и может выглядеть неправдоподобно. Такое можно видеть на крупных сайтах при работе с ними со слабых устройств на медленных линиях связи. Кроме того, сейчас мне хотелось бы озвучить несколько причин, по которым эти данные стоит рассматривать с долей скептицизма.

  • На производительность сайтов влияет множество факторов. Среди них — объём JavaScript-кода, отправляемого пользователю, число изображений и других материалов, выводимых на странице, и так далее. Некорректно будет сравнивать производительность любых сайтов, основанных на React, с производительностью страницы с надписью «Hello, World» в том случае, если во внимание не принимаются другие факторы.
  • Если попытаться получить данные, заменив в запросе React на название какой-нибудь другой библиотеки, будут получены очень похожие цифры.

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

Рост объёмов JavaScript-кода


Общая проблема современных веб-сайтов, не привязанная к конкретной библиотеке, связана с объёмом JavaScript-кода, которое обычно приходится загружать клиенту при просмотре страниц. На ресурсе HTTP Archive уже имеется хороший отчёт об этом. Если в двух словах, то вот как выглядят медианные значения объёмов JavaScript, загружаемых с веб-сайтов в разные годы:

  • 74.7 Кб — мобильные веб-страницы, 15 декабря 2011 года.
  • 384.4 Кб — мобильные веб-страницы, 15 декабря 2018 года.

Тут нужно учитывать, что эти данные получены после обработки миллионов страниц. Вероятно, существуют тысячи нетипичных сайтов, которые искажают данный показатель. Это жизнеспособная идея. Поэтому попытаемся узнать о том, как выглядит этот показатель для первых 10000 сайтов из рейтинга Alexa:

  • 381.5 Кб — мобильные веб-страницы, 15 декабря 2018 года (вот запрос).

Всё это позволяет сделать вывод о том, что в наши дни создаются веб-сайты, которые включают в себя больше JS-кода, чем сайты, которые создавались несколько лет назад. Это — важное наблюдение. Сайты стали больше, они стали более интерактивными и более сложными, и объём JS-кода этих сайтов постепенно, с каждым годом, растёт. Вероятно, вы уже об этом слышали, но чем больше JavaScript-кода вы отправляете в браузер — тем больше времени нужно на то, чтобы его распарсить, скомпилировать и выполнить. Это, в результате, замедляет сайт.

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

Профилирование и анализ страниц


Профилирование и анализ React-приложений может рассматриваться с двух точек зрения:

  • Во-первых, речь идёт об оценке производительности компонентов. Это влияет на то, как пользователи взаимодействуют с сайтом. Например, если при щелчке по кнопке выводится список, это должно выполняться быстро, но, в том случае, если в ходе выполнения этой операции выполняется повторный рендеринг сотен компонентов, при том, что в нём нет необходимости, эта операция будет восприниматься как медленная.
  • Во-вторых, речь идёт о том, как скоро приложение приводится в рабочее состояние. То есть о том, через сколько времени после начала загрузки сайта пользователь сможет с ним взаимодействовать. Объём кода, отправляемый пользователю в ходе загрузки первой страницы сайта — это пример фактора, влияющего на то, как быстро пользователь сможет начать работу с приложением.

Оценка производительности и оптимизация компонентов


Попытаемся выразить в одном предложении смысл алгоритма реконсиляции React, или суть того, что называют «виртуальной DOM». Это будет выглядеть так: «React предпринимает действия для нахождения различий между новым деревом DOM и старым деревом для того, чтобы понять, что именно в пользовательском интерфейсе должно быть обновлено при изменении данных в компоненте». Это создаёт гораздо меньшую нагрузку на систему, чем повторный рендеринг всего приложения при каждом изменении состояния или свойств (тут можно почитать о разнице между O(n3) и O(n)). Вот статья Дэна Абрамова, где можно найти пояснения по поводу реконсиляции.

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

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

  • Панель Performance инструментов разработчика Chrome.
  • Профилировщик инструментов разработчика React.

▍Анализ производительностью с использованием панели Performance инструментов разработчика Chrome


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


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

Вот хороший материал на эту тему, посвящённый исследованию производительности приложений, написанных с использованием React 16, с применением инструментов разработчика Chrome.

API User Timing используется лишь в ходе разработки. Оно, в продакшне, отключается. В таких условиях могут использоваться более быстрые реализации подобных механизмов, не оказывающие серьёзного влияния на производительность. Именно необходимость в таких механизмах и стала одной из причин разработки более нового API Profiler.

▍Анализ производительности с помощью профилировщика из инструментов разработчика React


С выходом библиотеки react-dom 16.5 в инструментах разработчика React можно пользоваться новой панелью, называемой Profiler. Она позволяет анализировать производительность рендеринга компонентов. Делается это с помощью API Profiler, средствами которого собирается информация о времени выполнения операций для всех компонентов, подвергаемых повторному рендерингу.

Панель Profiler представляет собой самостоятельную вкладку в инструментах разработчика React. Тут, как и в случае с панелью Performance инструментов разработчика Chrome, можно записывать сведения о действиях пользователя и о перезагрузках страниц для того, чтобы собрать данные для анализа производительности компонентов.


Сбор данных с помощью инструментов разработчика React

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


Пламенный график профилировщика

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


Просмотр сведений о коммитах в профилировщике

Представленные здесь скриншоты представляют данные, полученные в результате записи действий пользователя, выполняемых в простом приложении. Приложение выполняет загрузку списка трендовых GitHub-репозиториев при щелчке по кнопке. Как видите, здесь имеется всего два коммита:

  • Один — для индикатора загрузки, который выводится в ходе загрузки списка элементов.
  • Ещё один представляет момент, когда вызов к API завершён и список выводится в DOM.

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


Метаданные

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

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


Увеличение числа коммитов при усложнении схемы работы с приложением

Более поздние коммиты отличаются более длинными графиками, в них больше жёлтого цвета. Это означает, что время, необходимое всем компонентам для завершения рендеринга, растёт по мере роста размера списка элементов на странице. Это происходит из-за того, что каждый компонент в списке претерпевает повторный рендеринг с каждым новым обращением к API. Это помогает выявить проблему, которую можно довольно просто решить. Решение заключается в том, что существующие в списке элементы не нужно рендерить повторно при добавлении в список новых элементов.

▍Минимизация ненужных операций повторного рендеринга компонентов


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

  • Можно воспользоваться методом жизненного цикла компонента shouldComponentUpdate():

    shouldComponentUpdate(nextProps, nextState) {
      // true нужно возвратить только при соблюдении определённых условий
    }
  • Можно, для конструирования компонентов, основанных на классах, использовать PureComponent:

    import React, { PureComponent } from 'react';
    
    class AvatarComponent extends PureComponent {
    
    }
  • Для функциональных компонентов можно использовать memo:

    import React, { memo } from 'react';
    
    const AvatarComponent = memo(props => {
    
    });
  • Можно произвести мемоизацию селекторов Redux (например, с помощь reselect).
  • Можно оптимизировать вывод очень длинных списков, применив виртуализацию (например — с помощью react-window).

Вот и вот — пара полезных видео, где рассматривается применение профилировщика React для поиска узких мест в приложениях.

Оценка производительности и оптимизация приложений


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

Протестировать веб-страницу с помощью Lighthouse можно тремя способами:

  • Используя интерфейс командной строки Node.js.
  • Используя расширение Chrome.
  • С помощью панели Audits инструментов разработчика Chrome.

Вот как выглядит Lighthouse на панели Audits.


Lighthouse на панели Audits

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

Для того чтобы понять, что загрузка страницы в браузер предусматривает и загрузку слишком большого объёма JavaScript-кода, и сделать вывод о том, что объём этого кода стоит уменьшить, нужно обратить внимание на следующие фразы из отчёта:

  • Eliminate render-blocking resources
  • JavaScript boot-up time is too high
  • Avoid enormous network payloads

Если Lighthouse сообщает об этих проблемах из-за того, что страница использует JS-бандл слишком большого размера, самым первым, что стоит рассмотреть в качестве способа исправления проблемы, является разделение бандла. Дело в том, что если код можно разбить на фрагменты, некоторые из которых нужны только для работы с определёнными страницами сайта, то у нас нет причин этой возможностью не воспользоваться.

▍Разделение JS-бандлов


Один из способов разделения кода на части является использование динамического импорта:

  import('lodash.sortby')
    .then(module => module.default)
    .then(module => doSomethingCool(module))

Синтаксис импорта может выглядеть как вызов функции, но он позволяет импортировать любой модуль асинхронно, с использованием механизма промисов. В данном примере сначала импортируется метод sortby из библиотеки lodash, а затем выполняется метод doSomethingCool().


Приложение для сортировки чисел

В этом примере происходит следующее:

  1. Пользователь щёлкает по кнопке для того, чтобы отсортировать три числа.
  2. Импортируется lodash.sortby.
  3. Вызывается метод doSomethingCool(), который сортирует числа и выводит их в новом узле DOM.

Это — предельно простой, искусственный пример, так как если кому-то понадобится сортировать числа на веб-странице, то он, вероятно, просто воспользуется методом Array.prototype.sort(). Но хочется надеяться, что этот пример помог мне показать то, почему использование динамического импорта при выполнении пользователем неких действий может оказаться полезным.

Синтаксис динамического импорта является сравнительно новым, он в настоящее время находится на третьей стадии процесса принятия новых возможностей JavaScript комитетом TC39. Этот синтаксис уже поддерживается в Chrome и в Safari, а также бандлерами Webpack, Rollup и Parcel.
Если говорить о React, то тут существуют абстракции, созданные для упрощения процесса разбиения кода на уровне компонентов с использованием технологии динамического импорта. Один из примеров реализации этого механизма — React.lazy:

import React, { lazy } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));

Одной из основных проблем, связанных с асинхронной загрузкой различных частей приложения, является работа с задержками в работе приложения, с которыми может столкнуться пользователь. Для этого существует компонент Suspense, который может быть использован для того, чтобы «приостановить» вывод на экран дерева некоего компонента. При использовании его вместе с React.lazy, можно, если код нужного компонента всё ещё загружается, показать пользователю индикатор загрузки:

import React, { lazy, Suspense } from 'react';
import LoadingComponent from './LoadingComponent';

const AvatarComponent = lazy(() => import('./AvatarComponent'));

const PageComponent = () => (
  <Suspense fallback={LoadingComponent}>
    <SomeComponent />
  </Suspense>
)

Компонент Suspense пока ещё не работает при применении серверного рендеринга. Если вы хотите воспользоваться методиками разделения кода в React-приложениях, которые рендерятся на сервере, используйте, следуя совету, данному в документации React, библиотеку наподобие loadable-components.

import React from 'react';
import loadable from '@loadable/component'
import LoadingComponent from './LoadingComponent';

const AvatarComponent = 
  loadable(() => import('./AvatarComponent'), {
    LoadingComponent: () => LoadingComponent
  });

LoadingComponent может быть использован с loadable-component во время загрузки основного компонента как индикатор.

Обратите внимание на то, что для того, чтобы использовать библиотеку loadable-components в серверном рендеринге, нужно кое-что настроить.

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

▍Стоит ли заниматься разделением кода, которое опирается на прокрутку страницы?


Вот ещё одна интересная библиотека для разделения кода, react-loadable-visibility, которая построена на базе библиотеки loadable-components и веб-API Intersection Observer. Её можно использовать для загрузки компонентов по мере того, как они становятся видимыми при прокрутке страницы.

import React from 'react';
import loadableVisibility from 'react-loadable-visibility/loadable-components'

const AvatarComponent = loadableVisibility(() => import('./AvatarComponent'), {
  LoadingComponent: Loading,
})

▍О кэшировании того, что стоит кэшировать


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


Ускорение загрузки страниц с использованием кэширования


Workbox
— это набор библиотек, который упрощает включение в проект сервис-воркеров, избавляя программиста от необходимости писать весь необходимый код с нуля. С использованием CRA 2.0 для того, чтобы воспользоваться этим механизмом, достаточно удалить всего пару символов из файла src/index.js. Это даст вам возможность задействовать стандартный сервис-воркер с базовыми возможностями по кэшированию.

  import React from 'react';
  
  //...

  // Если вы хотите, чтобы ваше приложение работало бы без подключения к интернету 
  // и быстрее загружалось бы, вы можете изменить ниже
  // unregister() на register(). Обратите внимание на то, что при работе с сервис-воркерами
  // нужно учитывать некоторые особенности. 
  // Подробности о них смотрите здесь: http://bit.ly/CRA-PWA
  serviceWorker.unregister();

Подробности о сервис-воркерах и о библиотеке Workbox вы можете найти в этом материале.

▍Серверный рендеринг и потоковая передача данных


В основе технологий серверного рендеринга веб-сайтов лежит идея снижения нагрузки на устройства пользователей за счёт отправки в браузеры готового HTML-кода, сгенерированного на сервере, точно такого же, который получился бы в браузере в результате работы механизмов программного формирования страницы. Это позволяет сделать так, чтобы пользователи видели бы содержимое страниц гораздо быстрее, чем в том случае, когда им, чтобы сформировать страницу, сначала нужно загрузить некий JS-код, а потом дождаться окончания его выполнения.

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

Работая с React 16, можно воспользоваться возможностями потоковой передачи данных при серверном рендеринге компонентов. Вместо того чтобы пользоваться методом renderToString() для формирования HTML-строки, можно использовать метод renderToNodeStream() для возврата Readable-потока Node.

Потоковая передача данных с сервера позволяет клиенту получать и по очереди выводить разные части HTML-документа вместо того, чтобы выводить сразу весь документ. Хотя серверный рендеринг всегда улучшает показатель, связанный с первым выводом страницы приложения на экран, этот подход способен ещё сильнее улучшить данный показатель.

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

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


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

Предварительный рендеринг, или статический рендеринг, представляет собой нечто среднее между серверным рендерингом страниц в момент обращения браузера к серверу и рендерингом, который полностью выполняется на клиенте. Такой подход обычно подразумевает создание HTML-страниц для каждого маршрута во время сборки проекта и предоставление этих страниц пользователю после того, как все материалы приложения будут готовы к работе.

Если вы хотите использовать эту технологию в своём приложении, вам могут в этом помочь библиотеки наподобие react-snap, которые пользуются возможностями Puppeteer.

Вот хороший материал, посвящённый различным подходам к серверному рендерингу.

▍Об извлечении важных стилей, описанных в виде CSS-in-JS


Многие React-разработчики, по разным причинам, используют CSS-in-JS-библиотеки вроде emotion и styled-components. Среди таких причин можно отметить возможность использования стилей, область действия которых ограничена компонентом, автоматическое создание селекторов, основанных на свойствах, передаваемых компонентам, и другие. Если при использовании технологии CSS-in-JS не проявлять должную осмотрительность, можно столкнуться с проблемой, которая заключается в том, что все стили будут вычисляться во время выполнения кода. Это означает, что стили будут применяться только после того, как JavaScript-бандл страницы будет выполнен, что приводит к тому, что пользователь может некоторое время наблюдать нестилизованное содержимое страницы. Как и в случае с любыми другими проблемами, касающимися производительности, эта проблема усиливается в тех случаях, когда для работы с сайтом используются слабые мобильные устройства или медленные сетевые соединения.


Ситуация, в которой пользователь некоторое время видит нестилизованное содержимое страницы (взято отсюда)

Если в вашем приложении уже используется какая-нибудь разновидность серверного рендеринга, исправить проблему со стилями можно путём извлечения самых важных из них. Библиотеки emotion и styled-components поддерживают эту возможность. Стили можно извлечь в Readable-поток Node. У Glamor есть отдельная библиотека, которая позволяет делать то же самое.

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


Тут показано Preact-приложение, но этот пример позволяет оценить возможности по извлечению стилей (источник)

Если всё, что вам нужно от технологии CSS-in-JS — это использование стилей, область действия которых ограничена компонентами, и вы при этом не хотите перегружать приложение библиотеками, реализующими более широкий функционал, возможно, вам подойдёт библиотека astroturf. Используя её, вы получите не все возможности, доступные в типичной библиотеке такого рода, но в вашем распоряжении будет возможность работать с управляемыми стилями компонентов, использование которых не создаст дополнительной нагрузки на систему во время выполнения кода приложения.

▍Доступность сайтов


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

Для того чтобы выявить проблемы с доступностью некоей страницы, отлично подойдёт Lighthouse. Для нахождения проблем, характерных для React-элементов, можно воспользоваться проектом React A11y.

Кроме того, вы можете рассмотреть возможность использования react-axe для проведения аудита готового, отрендеренного приложения, а не только его JSX-кода.

▍Улучшение возможностей по добавлению сайтов на домашний экран


Хорошо ли ваш сайт работает на мобильных устройствах? Есть ли у вас сервис-воркеры, которые кэшируют некоторые материалы, что позволяет приложению работать и без подключения к интернету? Является ли ваше приложение настолько полезным, что пользователям может захотеться разместить значок для быстрого доступа к нему на домашнем экране их устройств?

Если вы отвечаете на все эти вопросы положительно, тогда вам нужно написать для своего сайта манифест веб-приложения. Это повысит удобство пользователей по работе с приложением при его «установке» на их мобильные устройства. Манифест позволяет много всего настраивать, в том числе — значок приложения, фоновый цвет и цвет темы.

При использовании create-react-app создаваемый этим инструментом шаблон приложения оснащается стандартным файлом-манифестом:

{
  "short_name": "React App",
  "name": "Create React App Sample",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

Разное


Вот некоторые идеи, которые направлены на оптимизацию труда программистов. Их применение, кроме того, ведёт к снижению объёма кода приложений. Чем меньше объём кода приложения — тем лучше. Почему это так? Дело в том, что чем компактнее будет код приложения, тем меньше данных придётся передавать с сервера в браузер и тем быстрее будут загружаться страницы приложения.

▍Atomic CSS


Методика использования атомарных CSS-стилей (Atomic CSS) предусматривает создание классов, решающих чрезвычайно узкие задачи и отличающихся маленькими размерами. Например, при таком подходе отдельный класс используется для того, чтобы назначить кнопке синий фон:

<button class="bg-blue">Click Me</button>

Отдельный класс используется для настройки свойства кнопки padding:

<button class="bg-blue pa2">Click Me</button>

И так далее. Хотя это приводит к необходимости добавлять очень большое количество значений в атрибут class большинства узлов DOM, это даёт преимущество, заключающееся в том, что разработчику приходится писать гораздо меньше CSS-кода, чем обычно. Достигается это за счёт использования библиотек, ответственных за автоматическую настройку селекторов. Одной из таких библиотек является Tachyons.

Ограничение области действия стилей границами компонента с использованием атомарных классов стало привычной практикой для многих разработчиков. А библиотеки вроде tachyon-components даже позволяют применять эти стили с использованием API, похожего на API styled-components:

import styled from 'tachyons-components'

const Button = styled('button')`
  bg-blue pa2
`

<Button>Click Me</Button>

Хотя это может и не быть основной причиной, по которой разработчики используют библиотеки для работы с атомарными стилями, устранение необходимости в создании новых CSS-селекторов благодаря использованию фиксированного набора «одноразовых» классов означает, что объём CSS-кода, отправляемого в браузер, всегда будет оставаться неизменным.

▍Хуки


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

import { useState } from 'react';

function AvatarComponent() {
  const [name, setName] = useState('Houssein');

  return (
    <React.Fragment>
      <div>
        <p>This is a picture of {name}</p>
        <img align="center" src="avatar.png" />
      </div>

      <button onClick={() => setName('a banana')}>
        Fix name
      </button>
    </React.Fragment>
  );
}

Хуки можно использовать для решения следующих задач:

  • Включение в компонент состояния без использования компонентов, основанных на классах (useState).
  • Включение в компонент побочных эффектов без использования компонентов, основанных на классах (useEffect).
  • Повторное использование одной и той же логики в разных компонентах благодаря созданию собственных хуков.

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

Вот запись с мероприятия React Conf, в начале которой можно найти подробное разъяснение концепции хуков.

Хотя замечательные возможности хуков и сами по себе приведут к их широкому распространению, можно отметить, что их использование способно помочь и в деле уменьшения размеров пакетов JavaScript-кода приложений. Как пишет Дэн Абрамов, если говорить об увеличении библиотеки React после добавления в неё поддержки хуков, то оно составило примерно 1.5 Кб (минифицированная и сжатая версия). Весьма вероятно то, что применение хуков поможет уменьшить размер бандлов из-за того, что код, в котором используются хуки, лучше, чем код, в котором используются компоненты, основанные на классах, поддаётся минификации.

Итоги


Здесь мы рассмотрели множество вопросов, касающихся оптимизации производительности React-приложений. Вот краткая сводка по тем шагам, которые может предпринять тот, кто стремится сделать свои приложения быстрее:

  1. Измерьте производительность компонентов приложения, воспользовавшись одним из следующих инструментов:

    • Панель Performance инструментов разработчика Chrome.
    • Профилировщик инструментов разработчика React.
  2. Минимизируйте число ненужных операций повторного рендеринга компонентов

    • Там, где это применимо, воспользуйтесь методом жизненного цикла компонента shouldComponentUpdate().
    • Для компонентов, основанных на классах, используйте PureComponent.
    • Для функциональных компонентов используйте React.memo.
    • Воспользуйтесь технологией мемоизации селекторов Redux (например, с помощью reselect).
    • Примените технологии виртуализации при выводе очень длинных списков (например, с помощью react-window).
  3. Измерьте общую производительность приложения с помощью Lighthouse.
  4. Улучшите общую производительность приложения, применив следующие подходы к оптимизации:

    • Если вы не пользуетесь серверным рендерингом — подумайте о разбиении кода компонентов с помощью React.lazy.
    • Если вы пользуетесь серверным рендерингом — разделяйте код компонентов с использованием библиотеки наподобие loadable-components.
    • Используйте сервис-воркеры для кэширования материалов, которые стоит кэшировать. Вам в этом может серьёзно помочь Workbox.
    • Если вы пользуетесь серверным рендерингом — используйте потоки вместо строк (с помощью renderToNodeStream и renderToStaticNodeStream).
    • Серверный рендеринг вам не подходит? Тогда задействуйте предварительный рендеринг с использованием библиотек наподобие react-snap.
    • Извлекайте самые важные стили при использовании библиотек, нацеленных на реализацию технологии CSS-in-JS.
    • Постарайтесь сделать так, чтобы ваши приложения были бы доступны как можно более широкому кругу пользователей. Рассмотрите возможность использования библиотек React A11y и react-axe.
    • Оснастите свой проект манифестом веб-приложения в том случае, если полагаете, что мобильным пользователям будет удобно работать с ним, вызывая его с домашнего экрана их устройств.

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

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

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

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

  • +30
  • 7,1k
  • 4
RUVDS.com
1117,00
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией

Комментарии 4

    0
    А именно, я предлагаю всем, кто это читает, сегодня же прекратить пользоваться React.

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

    А мне кажется, что он просто так троллит модных React-разработчиков. По крайней мере первая фраза как раз больше похожа на замаскированный крик души =)

    Статья весьма годная, спасибо вам.
      +2
      Сохранил текущую страницу хабра на диск- 1.5 Мб скриптов и css.
      Яндекс-Почта — 10 Мб скриптов и css.
        0

        Вы не учитываете сжатие при передаче. Текст довольно хорошо жмётся. Поэтому для сравнения таким образом файлы надо хотя бы зазиповать.

        –1
        Суть в том, что если у тебя тормозит один сайт, то тормозят и почти все остальные. Потому что технологии ± одинаковые. И решить проблему глобально можно только купив новый телефон и более быстрый тарифный план. Персонально к конкретным сайтам наврядли будут какие-либо претензии, так как люди видят что тормозит всё и чисто психологически готовятся к апгрейду. А раз так, то зачем рвать задницу? И где те меценаты, готовые до бесконечности оплачивать разработчиков-оптимизаторов?

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое