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

Путешествие началось с Ruby on Rails. Нам понравился Rails много лет назад, он был быстрым в развертывании, простым в загрузке и полностью настроен для обработки аутентификации пользователей. Индустрия была богата инженерами Rails, и мы увидели огромную выгоду в том, чтобы делать ставки на нее как на нашу предпочтительную технологию. По мере роста компании росли и инженерные силы, и через несколько лет мы получили сервер Rails, на котором были обнаружены некоторые исходные HTML и довольно большое одностраничное приложение Backbone.

В худшем случае у нас был поток бронирования с колоссальными 2,4 МБ gzip и минимизированным кодом на стороне клиента. 6 скриптов блокировка рендеринга JS и CSS. 200 КБ шрифтов. И действительно медленный сервер Rails дает нам плохое время до первого байта.

Но послушайте, по крайней мере, у нас было несколько изображений с отложенной загрузкой.

Наши тесты веб-страницы показали ужасное время загрузки страницы - около 23 секунд при хорошем 3G и невероятно болезненное время для отслеживания критического пути рендеринга.

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

Перенесемся на несколько лет вперед….

Наш процесс бронирования - это наиболее повторяющаяся часть пользовательского интерфейса Holiday Extras. Он обрабатывает 15–20 запросов на вытягивание в день, полных итераций сплит-тестов и интеграции новых продуктов. У нас есть магистральный маршрутизатор, обрабатывающий всю нашу навигацию, и один массивный дамп клиентского Javascript, который заботится о каждом дюйме нашего пользовательского интерфейса потока бронирования.

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

  1. У нас были очень сочные фрукты с низкими характеристиками.
  2. Мы знали, что у нас есть заинтересованность в работе над производительностью и структурой команды для ее достижения.
  3. Мы знали, что хотим иметь возможность предоставить платформу в будущем, которая позволила бы инженерам свободно писать несвязанный пользовательский интерфейс и иметь простые инструменты для поддержания производительности своего кода.
  4. Мы хотели, чтобы поток бронирования был самым эффективным в индустрии туризма.

С чего начать?

Реагируй конечно. Как и в случае с Rails, мы, естественно, начали приобретать все больше и больше инженеров, увлеченных и глубоко понимающих React. Мы видели всевозможные комбинации React Router и Webpack для разделения кода в масштабе, который вписывался непосредственно в структуру нашего одностраничного приложения - это был ключевой фактор, помогавший нам задуматься о том, к чему мы стремимся. Маршруты и фрагменты стали обретать смысл, мы увидели, как отдельные команды могут начать владеть своим собственным кодом и нести ответственность за свою производительность. Экосистемы Webpack и React процветали благодаря документации и поддержке для создания высокопроизводительных одностраничных приложений (SPA). Наше мобильное приложение начало формироваться, и ставки на React казались разумным способом поделиться пользовательским интерфейсом в будущем.

Шаг 1, размер пакета

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

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

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

Новые «блоки» были созданы с использованием существующей сборки и добавлены в нашу настройку S3 и Cloudfront, как и наша основная точка входа. Легкий. Мы увидели очень быстрые выгоды, сокращение на 70% количества Javascript, который мы отправляли нашим клиентам при загрузке первой страницы. Время загрузки, время до первого взаимодействия и вес страницы уменьшились.

Прогресс. Теперь мы имели дело с 12-секундным приложением.

Шаг 2. Оптимизация критического пути рендеринга

Наш пакет становился все меньше, но нам не нужен был Javascript, чтобы начать рендеринг пользовательского интерфейса для наших пользователей. Мы начали с переноса разметки в шаблон EJS, который мы могли использовать вместе с HTML Webpack Plugin для создания исходного HTML-кода нашего приложения.

Мы запускаем наш процесс бронирования React на большом количестве доменов с разным брендом и конфигурациями. Большая часть работы над HTML Webpack заключалась в переносе устаревших проверок, которые мы похоронили в представлениях Rails, на простые функции времени сборки. Это означало, что мы могли раскрыть предыдущую «серверную» логику нашему клиентскому приложению во время сборки.

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

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

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

Обслуживание нашего HTML из CDN

Сейчас это кажется простым, но в то время уход от представлений Rails и настройка процесса сборки Webpack требовали больших усилий, и теперь мы были в состоянии начать перемещать запросы в наш новый статический HTML. Мы просто расширили нашу конфигурацию NGINX для поддержки нового шаблона URL, который проксировал запросы в Cloudfront (наш текущий выбор CDN), а затем просто начал отправлять трафик по новому пути. Такой подход обеспечил нам надежность поэтапного развертывания, а также дал нам возможность отслеживать как исходный критический путь рендеринга, так и производительность нашего нового статического HTML.

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

Критический CSS

В дополнение к нашим проблемам с JS, наш CSS также был тяжелым для клиента. У нас была одна огромная таблица стилей блокировки рендеринга, которая содержала все обычные дублирующиеся и неиспользуемые селекторы. Мы решили разделить наши стили на три основных листа. Критическое, кузовное и нагруженное. Критический состоял из менее чем 20 КБ встроенных стилей в <head> нашего HTML, это было трудно исправить в процессе сборки, поэтому это все еще очень ручной процесс объединения стилей и уточнения того, что вы хотите показать пользователю. во время критического пути рендеринга. Стили body содержат все необходимое для того, чтобы пользователь мог прокручивать сайт, сохраняя при этом визуально законченный опыт. Последний лист загрузки содержал все, что содержалось во взаимодействиях пользовательского интерфейса 2-го уровня, таких как средства выбора даты.

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

Шаг 3. Вовлечение команды в производительность

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

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

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

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

Еще одним ключевым фактором развития культуры производительности было обеспечение бюджета производительности в сборке CI.

Мы добавили очень простые проверки, которые начинались бы с взвешивания общего размера Javascript точки входа в наши приложения и сравнения его с последним значением в промежуточной стадии. Мы начали с того, что установили жесткое ограничение на разархивирование в 700 КБ, и сборка автоматически завершалась ошибкой с полезным сообщением об ошибке инженеру, объясняющим, почему это произошло. Вот какое сообщение мы отображаем в сборке, когда наша точка входа превышает бюджет производительности.

We implement performance budgets to stop bundle sizes gradually creeping up as new features are added and impacting the customer experience. To prevent the budget being exceeded there are a few different courses of action:
* Asynchronously load a library in a separate chunk if it’s not needed at render time
* Fix a different feature or investigate code splitting other features to get under budget
* Push back on adding the change if it doesn’t justify the extra bundle size

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

Шаг 4, мониторинг RUM

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

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

Немного покопавшись в сети, мы увидели, что Speed ​​Curve теперь выполняет RUM, и панели управления выглядят просто потрясающе. У него был простой в использовании фрагмент JS для начала сбора данных, а также обширная документация о том, как писать собственные маркеры для использования в вашем собственном одностраничном приложении. Он сразу же предоставил нам информационные панели и бюджеты производительности по умолчанию. Это потребовало минимальной настройки и дало нам ценность в первый день, показав, что у нас все еще есть JS-скрипт, блокирующий рендеринг.

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

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

Наши общие улучшения производительности:

Резюме

  • Такой инструмент, как Speed ​​Curve, ценится на вес золота, поскольку он выделяет узкие места в производительности и показывает вам, где вы находитесь рядом со своими конкурентами. Очень ценен для обеспечения заинтересованности заинтересованных сторон.
  • Распределите низко висящие плоды среди более тяжелых задач, которые вам нужно спроектировать, это сохранит мотивацию вашей команды на протяжении всего процесса, и это хорошая вещь, которую можно продемонстрировать на демонстрации для заинтересованных сторон.
  • Как можно раньше продемонстрируйте повышение производительности в отчетах, которыми легко поделиться.
  • Обеспечьте долговечность своей миссии по повышению производительности, встраивая бюджеты производительности в CI, а также в инструменты RUM (если у вас есть к ним доступ).

Мы прошли долгий путь, но есть еще дела:

  • Уменьшите количество повторных отрисовок компонентов в React
  • Удалите нашу зависимость от NGINX и вместо этого перейдите прямо к нашему CDN.
  • Абстрактная логика клиентского сплит-теста и начало поставки отдельных фрагментов Javascript для каждого нового теста, который мы производим.

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

Спасибо https://perfmattersconf.com и https://ldnwebperf.org/ за советы и рекомендации, а также всем инженерам Holiday Extras, которые внесли свой вклад в то, где мы находимся сегодня.