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

Наши закусочные веб-приложения, Grubhub и Seamless, использовали AngularJS с 2014 года и Angular с 2017 года после длительного обновления / преобразования между ними. Angular пережил период удивительного роста и продуктивности. Но наша мобильная производительность по-прежнему замедлялась - среднее время загрузки мобильной страницы составляло от 9 до 11 секунд, а полная интерактивность занимала до 17 секунд после того, как посетитель открыл сайт.

Хотя это время загрузки было плохим для посетителей и потенциально могло повлиять на наш SEO-рейтинг, это было не единственной мотивацией. Как и многие другие начинания по разработке программного обеспечения, все началось с личных неудобств. Приложение запускалось медленно: на компиляцию в режиме разработки / модульного тестирования ушло больше минуты, а на предварительную компиляцию Angular (AOT) - около трех минут.

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

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

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

Утопление в лишних модулях

Для приложений JavaScript в Интернете, в отличие от серверного программного обеспечения или загружаемых приложений, стоимость дополнительного кода влияет на пользователя дважды: 1) время загрузки и 2) время выполнения. В то же время предложения с открытым исходным кодом - внешние модули, которые добавляют уровни абстракции - резко выросли по популярности и количеству, и создание современного веб-приложения включает выбор десятков этих внешних модулей, включая важное решение по системе рендеринга - «фреймворк». . »

Наше приложение было хорошим кандидатом для удаления этих модулей:

  • По сравнению, например, с корпоративной подпиской SaaS, первые впечатления (одно из которых - скорость сайта) имеют большее влияние на коэффициенты конверсии электронной коммерции.
  • API документов, который поставляется со всеми браузерами, уже представляет собой потрясающий уровень абстракции. Не нужно добавлять к нему так много всего. Это мнение, похоже, разделяет Preact.
  • Если бы некая внешняя библиотека предоставляла действительно незаменимый API, мы могли бы реализовать API самостоятельно, если бы внешняя реализация была слишком большой.

Размер нашего приложения составлял около 200 тыс. LOC, 400–450 шаблонных компонентов и, возможно, сотня других логических классов.

Для начала у нас было:

  • 6 МБ JavaScript (измерено до gzip)
  • приложение TypeScript Angular
  • среднее время загрузки мобильной страницы 9–11 с, полная интерактивность - 17 с.

Я достал терку для сыра и стал искать модули, которые можно было бы измельчить.

moment, библиотека хронографов

Снять момент было легко. К тому времени, как я присоединился, команда уже написала альтернативную DateTime реализацию. Здесь ничего не поделаешь.

Программное обеспечение для AB-тестирования

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

core-js, полифилл

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

Шибболет определения функции:

eval(‘for(const v of new Set([Map,Symbol,MutationObserver,IntersectionObserver,Intl,Promise,CustomEvent])){};[].includes();[].fill();[].find(()=>{});’);

lodash, коллекция коллекций / алгоритмов

Следующим самым простым делом было удаление lodash. Мы уже перешли на использование lodash-es для встряхивания деревьев, поэтому полезная нагрузка оказалась не такой серьезной, как раньше. Тем не менее, удаление было простым. Я проверил все способы использования и преобразовал их для использования собственных методов, где это применимо, и написал альтернативные реализации функций, которые не имели прямого эквивалента, например uniqBy. Размер окончательной замены: менее 1кб.

Angular, фреймворк

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

Итак, зачем кому-то удалять фреймворк, который рекламирует на своей домашней странице:

«Достигните максимальной скорости, возможной на веб-платформе уже сегодня…»

Давайте поговорим о технических деталях. При всех своих преимуществах у Angular есть проблемы:

  • Сама по себе среда выполнения большая.
  • Для этого требуются два сотрудника, zone.js и rxjs, в результате чего общий объем составляет примерно 150–160 КБ (gzip).
  • Код, который он генерирует на специальном языке, похожем на HTML, примерно в 2 раза больше, чем эквивалентный JSX.

У него также есть множество инструментов и проблем с инфраструктурой:

  • Его компилятор AOT работает медленно и требует этапа компиляции, несовместимого с другими инструментами.
  • Его компилятор AOT считает допустимым только подмножество JavaScript по сравнению с его компилятором JIT.
  • Сложно использовать существующие инструменты статического анализа или написать новые для шаблонов Angular, потому что они используют свой собственный язык.
  • Он использует систему настраиваемых модулей, которая не соответствует существующему языковому стандарту (ESM), что затрудняет анализ неиспользуемого кода.
  • Его системы публикации зависимостей в то время практически не существовало.

Мы могли подождать, пока нас не спасут Bazel и / или средство визуализации Ivy, но мы решили удалить фреймворк. Учитывая эту историю, мы не были уверены, что Базел / Айви прибудет на место происшествия достаточно быстро и что они окончательно решат эти проблемы.

Переход на Preact

Сначала я сказал, что могу преобразовать все приложение для использования собственного document.createElement API, потому что TypeScript позволяет вам выбрать что угодно в качестве фабричной функции JSX. Я рад, что руководитель моей команды остановил меня. Мы решили использовать Preact и преобразовывать наши компоненты по одному.

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

Используя компонент «angular-preact-bridge», мы смогли начать с конечных узлов нашего дерева компонентов приложения и двигаться вверх, достигнув кульминации в компоненте site-container и удалении Angular, не требуя общей остановки других разработок. .

Сокращенный компонент моста:

Наш подход к преобразованию концепций, специфичных для Angular, в Preact:

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

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

  • Запустите код шаблона Angular через преобразователь регулярных выражений, создав аннотированный код TSX, который можно завершить вручную. (Я знаю, знаю, разбираю HTML с регулярным выражением).
  • @Input() и @Output() были преобразованы в реквизиты. EventEmitters стал реквизитом обратного вызова.
  • Переменные шаблона преобразованы в переменные состояния.
  • Методы жизненного цикла в большинстве случаев можно сопоставить один к одному.
  • Остальные методы и все, что содержало асинхронные обновления, такие как сетевые запросы или подписчики, были изменены, чтобы гарантировать, что они вызывают setState в конце вместо zone.js обнаружения скрытых изменений.

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

Компоненты, у которых были события жизненного цикла, такие как асинхронные запросы данных или переменные состояния, были преобразованы в компоненты класса.

Угловые директивы - это уникальная концепция Angular, для преобразования которой требовалось определенное творчество. Некоторые директивы преобразованы в компоненты оболочки. Этот узор больше соответствует композиционному стилю, который мы сейчас переняли. Другие директивы, которые не имели смысла в качестве компонентов, были либо преобразованы в нативные HTMLElement взаимодействия, либо встроены в наше «промежуточное ПО Preact». Документация Preact поощряет использование такого рода адаптеров промежуточного программного обеспечения для его основных функций. Поэтому мы написали функцию, которая служила нашим основным jsxFactory, делегируя ее createElement в Preact после преобразования таких атрибутов, как ngClass, ngStyle, i18n, href и т. Д. Это позволило нам уменьшить изменения в исходном коде шаблона во время преобразования, сохранив при этом ту же функциональность, что и эти Angular предоставленные директивы.

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

Мы взяли простой Preact router и немного доработали его под свои нужды. Поскольку маршрутизация Angular имеет концепции наблюдаемых RouterEvents и Resolvers, мы реализовали эквивалентные конструкции, чтобы уменьшить объем кода, который нам пришлось изменить. В середине преобразования у нас были активны оба маршрутизатора, которые передавали страницы друг другу.

Что касается модулей Angular, я достаточно уверен, что эта система существует только потому, что в противном случае невозможно определить зависимости компонентов, потому что селектор шаблонов Angular не отслеживается ESM до класса компонента. Тем не менее, об этом говорят, как будто это особенность. Благодаря поддержке TypeScript + Webpack для отложенной загрузки и разделения пакетов с помощью динамического синтаксиса import() нам больше не нужно настраивать модули Angular для отложенной загрузки. Внедрение зависимостей и имитация внедрения зависимостей были переписаны на простом JavaScript. Хотя у нас было ~ 100 классов в соответствии с шаблоном DI Angular, преобразование их с помощью регулярного выражения потребовало только одного запроса на перенос.

В итоге мы получили:

  • 3,3 МБ JavaScript до сжатия gzip, по сравнению с 6,0 МБ
  • приложение TypeScript, использующее Preact для рендеринга
  • среднее время загрузки мобильной страницы от 3 до 4 с.

TypeScript провел нас через все это и остается неотъемлемой частью нашего приложения. Меня постоянно впечатляет, насколько это помогает в понимании и рефакторинге кода. Кроме того, TSX (JSX), являющийся частью TypeScript, который допускает HTML-подобный шаблонный код, представляет собой такой короткий шаг компиляции по сравнению с обычным JavaScript, что гораздо проще поддерживать мысленную модель от источника к среде выполнения. Это не только проще для понимания людьми, но и инструменты статического анализа, такие как проверка типов TS, линтер и IDE, позволяют лучше понять нашу кодовую базу.

С той передышкой, которую это принесло нам, я планирую продолжать играть роль ублюдка. Когда разработчик спрашивает, можем ли мы принять новый модуль, я спрашиваю: «Сколько это килобайт?» и "есть ли меньшая реализация API?"

tl;dr

С октября 2018 года по апрель 2019 года был выпущен ряд улучшений производительности JavaScript для Grubhub и Seamless, наших веб-приложений для закусочных.

По одному важному показателю - среднему времени загрузки страницы для мобильных устройств - мы заметили улучшение с 9–11 до 3–4 с.

Особую помощь в этом проекте оказали две основные библиотеки с открытым исходным кодом.

  • TypeScript (devtool), который мы продолжаем использовать во всем приложении, позволил повысить уверенность в рефакторинге.

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