Предисловие

Постановка проблемы:
Многие пользователи upGrad в Индии не имеют доступа к мощным устройствам или высокоскоростному Интернету, что влияет на их работу в Интернете. Учащиеся upGrad с плохой или нестабильной пропускной способностью или с устройствами низкого уровня сообщали о проблемах со временем загрузки экрана и общей отзывчивостью пользовательского интерфейса (UI).

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

Пользовательский интерфейс обучающей платформы upGrad был написан на Backbone.js несколько лет назад, был большим по размеру, не обладал высокой производительностью повторного рендеринга, и его было трудно поддерживать и улучшать. Чтобы решить эту проблему, мы решили обновить нашу архитектуру с помощью современной переписи и создать более легкую версию платформы - upGrad Learn Lite, которая гораздо более снисходительна к низкой пропускной способности и плохим устройствам.

Результат

  1. Оценки Lighthouse:

В результате этого упражнения мы повысили показатель Lighthouse с минимального 1 до ошеломляющего 92 на главной странице приложения. Помимо сокращения времени загрузки, мы также преобразовали приложение в легко расширяемое прогрессивное веб-приложение (PWA).

2. Улучшение времени загрузки

Как видно из приведенного выше изображения, устаревшая обучающая платформа была тяжелой: размер запросов составлял 13,4 МБ, а загрузка занимала 6,6 секунды. Платформа Learn Lite имеет размер 3,4 МБ, полная загрузка занимает 3 секунды. Самое приятное, что после загрузки нового приложения все пакеты и ресурсы кэшируются, размер запроса составляет всего 1,7 МБ!

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

3. Интерактивная производительность.

Как видите, значительно сократилось время сценариев и загрузки (в 5 раз). Пользовательский опыт нового приложения заметно улучшился; каждое взаимодействие гладкое и плавное, а повторный рендеринг невысок из-за использования Reactjs и благодаря согласованию React's Virtual DOM.

Остальная часть этого блога описывает технические детали модернизации (кодовое имя проекта Ares) устаревшей платформы.

Как мы это сделали

Прежде всего, технология, лежащая в основе всей Revamp (кодовое название проекта - Ares).

Интерфейс:

Инструменты

Инфраструктура

Наша архитектура очень проста и выполняет свою работу. Приложение компилируется и загружается в AWS S3 после удаления старой сборки через наш конвейер CI / CD (Jenkins) и доставляется в разные регионы с помощью CDN AWS Cloudfront. При каждом выпуске очищается старый кеш.

Файл index.html никогда не кэшируется. Это сделано намеренно, так как любые новые обновления направляются через новый index.html

Мы используем Imagekit (Image CDN с автоматической оптимизацией) для доставки изображений.

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

Код

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

Ленивая загрузка

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

import React, { Suspense } from 'react';
Import FirstComponent from './FirstComponent';
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <Switch>
         <Route to="/"> <FirstComponent /> </Route>
         <Route to="/lazy-route"> <OtherComponent /> </Route>
        </Switch>
      </Suspense>
    </div>
  );
}

С помощью React.lazy вы можете лениво загружать определенные компоненты. Нам пришлось лениво загружать основные маршруты модулей, чтобы уложиться в наши бюджеты производительности (менее 500 КБ). Suspense - это компонент React, который будет отображать некоторый заполнитель до тех пор, пока не будет выбран компонент с отложенной загрузкой. У вас может быть компонент-заполнитель (загрузчик, изображение и т. Д.) В резервной опоре до тех пор, пока не будет загружен новый пакет.

import React from 'react';
import { somefunction } from 'someLib';
function MyComponent() {
  
  const lazyFun = () => {
    import('lazy-lib').then((lazyLibraryFunction) => {
     lazyLibraryFunction();
   });
  };
  
  
  return (
    <div>
     <button onClick={() => lazyFun()}> Click to lazy execute   
     </button>
    </div>
  );
}

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

export const changeLanguage = (locale) => (dispatch) => {
 import(`../locale/${locale}.json`).then(({ default: language }) =>{ 
    dispatch({
             type: 'SET_LANGUAGE',
             language,
             locale,
             });
  }).catch(() => {});
};

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

Использование React Hooks:

React Хуки - это новое дополнение в React 16.8, и они, безусловно, к лучшему. Вы можете написать чистые функциональные компоненты и по-прежнему использовать State и Lifecycle с помощью хуков, любезно предоставленных useState и useEffect. Возможно, вы задумываетесь о том, как использование перехватчиков в функциональных компонентах увеличивает производительность.

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

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

Реагировать на заметку:

Решая обновить DOM, React сначала визуализирует ваш компонент, а затем сравнивает результат с предыдущим результатом визуализации. Если результаты рендеринга отличаются, React обновляет DOM.

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

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

export function MemoComp({ prop1, prop2 }) {
  return (
    <div>
      <div>Property 1 : {prop1}</div>
      <div>Property 2: {prop2}</div>
    </div>
  );
}

export const MemoizedComp = React.memo(MemoComp);

Предотвращение примирения:

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

  1. Использование React.memo, как показано выше, значительно сокращает количество повторных отрисовок.
  2. Подпишитесь только на используемые разделы глобального магазина. Подписка на более крупный раздел открывает возможности нежелательного повторного рендеринга, вызванного другими изменениями состояния.
  3. Отправка одного действия для одного редуктора, то есть, если вы изменяете состояние Redux в любой заданной точке, поддерживайте соотношение действия к отправке 1–1. В приведенном ниже примере отправка двух действий одному и тому же редуктору запускает один дополнительный нежелательный Rerender. Это для одного простого действия API, но по мере роста приложения многие из этих вещей остаются незамеченными.
const ActionGenerator = () => dispatch => {
 dispatch(ON_LOADER);
 api().then((data) => { 
  dispatch(OFF_LOADER)
  dispatch(SET_DATA, data)
 })
}
// This causes 2 updates to store and 2*componenets times Rerenders 

Что делать, если вам нужно отправить несколько действий или вам нужно обновить два подсостояния. Для ускорения этого вы можете использовать Redux Batch. Пакетный API позволяет объединять любые обновления React в тике цикла событий в один проход рендеринга.

import { batch } from 'react-redux'
const myThunk = () => (dispatch, getState) => {
 // should only result in one combined re-render, not two
 batch(() => {
  dispatch(increment())
  dispatch(increment())
 })
}

Избегайте использования больших библиотек:

Существуют определенные библиотеки, такие как Moment, Loadash и т. Д., Которые представляют собой огромные пакеты без поддержки Tree Shaking (Tree shaking - это термин, обычно используемый в контексте JavaScript для удаления неиспользуемого кода. ). Эти библиотеки увеличивают время начальной загрузки. Взгляните ниже.

Хорошей альтернативой Moment будет date-fns (18kb) или DayJS (2kb) day js имеет меньше возможностей по сравнению с date-fns. Для lodash мы начали писать собственные утилиты, вдохновленные lodash.

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

📝Кстати, Ant Design - одна из самых функциональных библиотек React. Решает большинство ваших проблем с меньшими затратами времени и усилий на разработку.

Оптимизация CSS и активов

Как упоминалось в предыдущем пункте, мы создаем собственные компоненты пользовательского интерфейса, также мы воздерживались от использования популярных библиотек CSS, таких как Bootstrap, Bulma и т.д.

Очистка CSS: это инструмент, удаляющий неиспользуемый CSS. Это особенно полезно, если вы используете какую-либо библиотеку CSS (нам не пришлось), она просто удаляет все ненужные стили.

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

// Component 
import React from 'react';
import SvgWrapper from '.';
const AssetsExample = props => (
 <SvgWrapper {...props}>
  <path></path>
  // other SVG markup  
 </SvgWrapper>
);
// Usage
...
<AssetsExample fill="#555555" width={x} />
...

Преимущество этого заключается в том, что все наши ресурсы обрабатываются как файлы JS. Они компилируются и минифицируются вместе с другими JS, что делает его намного легче. В противном случае вам придется загружать SVG как изображения даже через CDN.

Особые улучшения Lighthouse

Lighthouse - это отраслевой стандарт веб-производительности. Критерии для оценки производительности с lighthouse 6.0 по порядку веса: Самая большая отрисовка контента, Общее время блокировки, Первая отрисовка контента, Индекс скорости, Время до взаимодействия и Совокупный сдвиг макета.

Мы стремились максимизировать четыре основных критерия, которые дают 80% оценки Lighthouse. Потому что именно здесь мы видим наибольшее влияние на наш бизнес.

Самая большая полноформатная краска - LCP [25%] LCP по сути означает время, затрачиваемое на визуализацию самого большого изображения или текстового поля, видимого в области просмотра.

элементы, которые считаются ‹img›, ‹image›, ‹video›, элемент с фоновым изображением, загруженным через url (), и элементы блока с текстом или встроенными элементами .

Размер элемента, о котором сообщается для Крупнейшего Contentful Paint, обычно равен размеру, который виден пользователю в области просмотра. Если элемент выходит за пределы области просмотра, или если какой-либо элемент обрезан или имеет невидимое переполнение, эти части не учитываются в размере элемента.

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

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

Для всех элементов не учитываются поля, отступы или границы, применяемые с помощью CSS.

Что мы сделали, так это представили использование скелетного загрузчика с мерцающей анимацией через `background-image: URL (‹imageUrl›)` еще до вызова API для получения данных. После получения данных мы заменяем скелет-загрузчик фактическим пользовательским интерфейсом.

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

Общее время блокировки - TBT [25%]: TBT - это минимальное время ожидания, необходимое пользователю для использования приложения в триггерных событиях, таких как щелчки мыши, касания экрана или нажатия клавиатуры. Сумма рассчитывается путем добавления блокирующей части всех длинных задач между Первой отрисовкой содержимого и Время до интерактивности. Любая задача, которая выполняется более 50 мс, - это долгая задача. Время после 50 мс - это блокирующая часть. Например, если Lighthouse обнаруживает задачу длительностью 70 мс, блокирующая часть будет составлять 20 мс.

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

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

FCP: показатель First Contentful Paint (FCP) измеряет время от момента начала загрузки страницы до отображения на экране любой части содержимого страницы. Этим содержимым могут быть изображения, элементы SVG, текстовые элементы и белый холст.

Что мы сделали: в SPA (одностраничном приложении) у вас будет элемент div Root, и все ваше приложение будет монтироваться внутри этого элемента. . как фрагмент ниже.

<body>
 <div id="app"></div>
</body>
...

Без загрузчика в html файле.

...
<body>
<div id="app">
 <div id="pageLoaderRoot" class="loaderRoot">
  <img class="spinnerImage" src="/path/to/loaderImg"
   alt="Loading..." />
  <div class="barSpinner"></div>
 </div>
</div>
</body>
...

С загрузчиком внутри HTML файла

Здесь мы разместили загрузчик в виде изображения внутри Root Div, при этом файл HTML сначала загрузит загрузчик, а затем наши пакеты JS переопределят содержимое внутри div с нашим приложением.

При добавлении такого загрузчика сразу после загрузки файла index.html счетчик загрузчика отображается как можно скорее. Это в принципе могло бы значительно повысить показатели FCP.

Индекс скорости. Индекс скорости измеряет, насколько быстро контент визуально отображается во время загрузки страницы. Lighthouse в основном снимает видео загрузки веб-страницы и вычисляет визуальную разницу между каждым кадром.

Что мы сделали:

  1. Видимость шрифта: самый простой способ избежать отображения невидимого текста при загрузке пользовательских шрифтов - временно отобразить системный шрифт. Включив font-display: swap в свой @font-face стиль, вы можете избежать FOIT в большинстве современных браузеров.
  2. Предварительная загрузка ключевых запросов: мы предварительно загрузили все наши шрифты, CSS и некоторые файлы JS через Webpack и с помощью preload-webpack-plugin. Таким образом, все наши важные ресурсы, необходимые для всего приложения, предварительно загружаются без блокировки.
  3. Предварительное подключение к CDN и другим инструментам. Директива preconnect позволяет браузеру устанавливать ранние подключения до фактической отправки HTTP-запроса на сервер. Это включает поиск DNS, согласование TLS, рукопожатие TCP. Это, в свою очередь, устраняет задержку в обоих направлениях и экономит время на разрешение используемых ресурсов.
  4. Очистить CSS: PurgeCSS - это инструмент для удаления неиспользуемого CSS. Если вы используете какую-либо стороннюю библиотеку или свою библиотеку для стилизации, то они огромны, и вы на самом деле не используете все селекторы. Очистка CSS просканирует все ваши селекторы и удалит весь неиспользуемый CSS, для которого не используется селектор.

Эпилог

В этом блоге рассказывается о том, как мы достигли хорошей веб-производительности для одностраничного приложения на основе React JS с использованием стандартного SCSS. Существуют определенные приложения и процессы, такие как использование CSS в JS или использование приложения Server Side Rendered (SSR), которые в некоторых случаях могут отличаться от указанных выше.

Потребность в создании быстрых приложений возникла как необходимость, но мои знания были извлечены из прошлого опыта на всю жизнь. Точно так же моя работа в upGrad сейчас направлена ​​только на создание приложений, но также и на создание платформы, которая расширяет возможности множества людей и помогает строить карьеру завтрашнего дня. . Посетите upGrad.com, чтобы ознакомиться с нашими программами, которые полностью онлайн! Если вы хотите сотрудничать с нашей энергичной командой, загляните на нашу страницу вакансий. Мы всегда ищем амбициозных и талантливых людей!

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

Ресурсы

Https://reactjs.org/docs/optimizing-performance.html - Оптимизация, специфичная для React.

Https://ui.dev/why-react-hooks/ - Почему хуки реагируют?

Https://web.dev/performance-scoring/ - Lighthouse, оценивающий глубокое погружение.

Https://dmitripavlutin.com/use-react-memo-wisely/ - подробное описание React Memo

Https://developers.google.com/web/tools/workbox/guides/configure-workbox - подробное описание конфигурации рабочей панели

Https://medium.com/@joecrobak/production-deploy-of-a-single-page-app-using-s3-and-cloudfront-d4aa2d170aa3 - развертывание S3 стало проще