Практический пример оптимизации производительности React SPA.

Производительность веб-приложения — это не только время загрузки. Крайне важно предоставить пользователям быстродействующее приложение, особенно для продуктивного веб-приложения, которое люди используют каждый день.

Проблема

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

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

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

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

При загрузке таблицы смен около 80% времени загрузки тратилось на выполнение скриптов.

После профилирования производительности в Chrome DevTools с включенным троттлингом процессора и сети стало понятно, что нужна оптимизация производительности.

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

1. Виртуализируйте большие таблицы

Проблема

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

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

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

Решение

Чтобы снизить стоимость этой операции, команда разработчиков виртуализировала сменный стол. Теперь приложение монтирует только компоненты в области просмотра и демонтирует компоненты за пределами экрана.

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

Результат

Только виртуализация таблицы сократила время выполнения сценариев на 6 секунд (при 4-кратном замедлении процессора + в среде Macbook Pro с троттлингом Fast 3G). Это было самым значительным улучшением производительности.

2. Аудит с помощью User Timing API

Решение

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

React 16 обеспечивает трассировку производительности через User Timing API, который вы можете визуализировать в разделе Timings Chrome DevTools. Команда разработчиков использовала раздел Тайминги, чтобы найти ненужную логику, работающую в событиях жизненного цикла React.

Результат

Команда разработчиков обнаружила, что прямо перед каждой навигацией по маршруту происходит ненужное согласование React Tree. Это означает, что React излишне обновлял таблицу смен перед навигацией. Эта проблема была вызвана ненужным обновлением состояния Redux. Его исправление сэкономило около 750 мс времени написания сценария. Команда разработчиков сделала и другие микрооптимизации, что в конечном итоге привело к общему сокращению времени написания скрипта на 1 секунду.

3. Ленивая загрузка компонентов и перемещение тяжелой логики в веб-воркеры

Проблема

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

Решение

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

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

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

В App.js используйте React.lazy и Suspense для отображения резервного содержимого во время загрузки.

// App.js
import React, { lazy, Suspense } from 'react'
 
// Lazily loading the Cost component with React.lazy
const Hello = lazy(() => import('./Cost'))
 
const Loading = () => (
  <div>Some fallback content to show while loading</div>
)
 
// Showing the fallback content while loading the Cost component by Suspense
export default function App({ userInfo }) {
   return (
    <div>
      <Suspense fallback={<Loading />}>
        <Cost />
      </Suspense>
    </div>
  )
}

В компоненте Стоимость используйте Comlink для выполнения логики расчета.

// Cost.js
import React from 'react'
import { proxy } from 'comlink'
 
// Import the workerlized calc function with comlink
const WorkerlizedCostCalc = proxy(new Worker('./WorkerlizedCostCalc.js'))
export default function Cost({ userInfo }) {
  // Execute the calculation in the worker
  const instance = await new WorkerlizedCostCalc()
  const cost = await instance.calc(userInfo)
  return <p>{cost}</p>
}

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

// WorkerlizedCostCalc.js
import { expose } from 'comlink'
import { someExpensiveCalculation } from './CostCalc.js'
 
// Expose the new workerlized calc function with comlink
expose({
  calc(userInfo) {
    // run existing (expensive) function in the worker
    return someExpensiveCalculation(userInfo);
  }
}, self)

Результат

Несмотря на ограниченный объем логики, которую они обрабатывали в качестве пробной версии, команда разработчиков переместила около 100 мс своего JavaScript из основного потока в рабочий поток (симулируется с 4-кратным дросселированием ЦП).

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

4. Установка бюджета производительности

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

  • Теперь измеряется время завершения скрипта для каждого события Redux.
  • Данные о производительности собираются в Elasticsearch.
  • 10-й, 25-й, 50-й и 75-й процентили производительности каждого события визуализируются с помощью Kibana.

Команда разработчиков сейчас отслеживает событие загрузки таблицы смен, чтобы убедиться, что оно завершается за 3 секунды для пользователей 75-го процентиля. На данный момент это неиспользованный бюджет, но они рассматривают возможность автоматических уведомлений через Elasticsearch, когда они превышают свой бюджет.

Результат

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

Резюме

Это был определенно не самый легкий путь для команды разработчиков по работе над этими оптимизациями, но он определенно окупился. Теперь приложение загружает таблицу смен за 1,5 секунды, что в 6 раз больше, чем до проекта.

После улучшения:

До улучшения:

Спасибо за прочтение ❤

Скажи привет! Твиттер | Гитхаб | ЛинкедИн | Фейсбук | Инстаграм