Много раз мы обсуждаем производительность веб-сайта и улучшения в области того, что ваш код должен и не должен делать, а также множество лучших практик. Реальность повышения производительности веб-сайта заключается в том, что иногда это достигается методом проб и ошибок.
Так что эта история об одном из таких случаев повышения производительности путем проб. Но сначала давайте немного объясним контекст нашей системы.
Приложения
Бизнес-модель Wix построена вокруг того, что мы называем «компаниями».
Каждая компания отвечает за определенную область в деловом (или техническом) мире. У каждой компании есть свой менеджмент, разработчики, продукт, UX, ну вы понимаете — это как компания внутри компании. Вот некоторые примеры компаний: Ecommerce, Chat, Bookings, Identity и многие другие.
Каждая компания имеет представительство одного или нескольких приложений.
В нашем мире техническое представление приложения представляет собой набор:
- Микросервисы (серверы)
- Статические артефакты
- Конфигурации

Каждый микросервис и статический артефакт развертывается отдельно в зависимости от скорости работы команды и предоставляемых функций. Так, например, если группа электронной коммерции добавляет новую функцию в виджет «Продукт», они могут развернуть в рабочей среде только этот клиентский артефакт.
Итак, какое отношение все это имеет к названию?!??!?!
Подождите, мы почти у цели..
Хост-приложения
Итак, у нас есть разные приложения, которые создаются и развертываются независимо, но где они на самом деле работают?
Большинство приложений (не все, у нас есть так называемые «автономные» приложения) запускаются тем, что мы называем хост-приложениями.
Хост-приложения – это приложения, которые загружают собственный код. и запускаются, затем они загружают и запускают интерфейсный код других приложений.
Давайте рассмотрим один пример: наше приложение панели мониторинга (называемое «бизнес-менеджер»).

Это поток, который охватывает то, что происходит, пока пользователь не сможет полностью взаимодействовать со страницей. Он начинается с того, что пользователь загружает сайт в браузер.
1. Браузер вызывает сервер Business Manager (наш главный сервер приложений)
2. Этот сервер вызывает другие серверы для получения конфигураций через RPC (для простота Я назвал их «Интеграция»)
3. Все они получают свои данные из своих хранилищ данных
4. HTML-код бизнес-менеджера возвращается с сервера, он загружает код JS-клиента
5. Клиентский код загружает весь клиентский код установленного приложения и запускает их.
Это, очевидно, упрощенный поток, поэтому мы можем обсудить вариант использования :).
Где начинается наша проблема
Итак, на шаге 5 предыдущей диаграммы мы загрузили код всех приложений, которые планируем запустить. Это может быть 30 различных пакетов кода.
Итак, мы делаем 30 запросов от клиента, чтобы получить пакеты Javascript и запустить их.
Мы столкнулись с вопросом, что на самом деле лучше для производительности?

- Много запросов на небольшие пакеты клиентского кода
- Несколько запросов на пакеты среднего размера?
- Один запрос на все пакеты клиентского кода?
Наша модель по умолчанию была первой:
Загружайте много довольно маленьких пакетов (обычно мы стараемся ограничить их размером около 20 КБ в сжатом виде). Для нас это было тривиально, поскольку наши артефакты создавались и развертывались отдельно многими командами, работающими независимо друг от друга.
Но у нас было неприятное чувство внутри, которое подсказывало нам, что этот шаблон может быть не очень хорош для конечного пользователя.
Мы создали хороший сервер, чтобы попробовать и посмотреть, что работает лучше: Динамический компоновщик JavaScript.
Динамическое связывание скриптов
Когда мы говорим о пакетировании, в большинстве случаев мы исходим из предположения, что это происходит во время времени сборки. В мире существует довольно много замечательных инструментов для эффективного объединения, встряхивания неиспользуемого кода и оптимизации пакетов, и мы их тоже используем. В основном мы полагаемся на webpack для объединения нашего кода.
Проблема в том, что эти пакеты являются статическими, так как мы создали их в разных репозиториях, и они приводят к разным артефактам. Таким образом, у нас нет единого пакета веб-пакетов, который фактически содержал бы весь наш код.
Вторая проблема заключается в том, что разным сайтам нужны разные пакеты в зависимости от того, какие приложения установили наши пользователи. Таким образом, даже если бы у нас был монорепозиторий всех приложений, создание всех перестановок для тестирования кажется очень сложной задачей.
Введите динамическое связывание скриптов.
Мы создали довольно простой сервер, который получает запрос на определенные пакеты, объединяет их и отправляет обратно новый пакет Javascript.
Давайте посмотрим на пример API:

Параметр bundlesquery представляет собой список клиентских артефактов, разделенных запятыми, которые нам нужно загрузить. Сервер извлекает файлы из нашего CDN и объединяет их вместе.
Любой файл, который был загружен на сервер, кэшируется в Redis и в памяти с ограниченным TTL, чтобы новые запросы на тот же пакет были более производительными.

Все успешные ответы, то есть те, которым удалось загрузить все запрошенные файлы, кэшируются на нашем уровне CDN. Таким образом, любой последующий запрос к этим файлам на самом деле не достигает сервера, а получает ответ от нашего уровня CDN.
Упаковщик поддерживает три разных метода получения данных:
- Как объект JSON URL-адрес артефакта для строки Javascript
- В виде составного файла Javascript всех запрошенных пакетов (с суффиксом .js)
- Как вызов JSONP (по сути, файл Javascript), который вызывает глобально доступную функцию и передает ей ключ URL-адреса артефакта в обернутый пакет анонимных функций (с суффиксом .jsonp). )
Из моей предыдущей работы в Liveperson я предполагал, что предпочтительными вариантами использования будут JSON или JSONP, но я ошибался ¯\_(ヅ)_/ ¯.
Первым вариантом использования был бизнес-менеджер, он начал загружать объединенные пакеты приложений.
Итак, что мы узнали?
Уменьшение количества запросов пакетов до меньшего количества пакетов большего размера фактически повышает производительность загрузки на 5–9 %.
Неплохой выигрыш от достаточно простой системы!
Эм, я не могу ОТЛАДИТЬ свой код в продакшене…
Так что да, мы увеличили производительность, но затем начали поступать жалобы разработчиков на то, что код больше не подходит для отладки в рабочей среде.
Так оказалось, что одной из вещей, которую мы потеряли по пути, были SourceMaps.
SourceMaps — это стандартная практика отладки клиентского кода в рабочей среде. Каждый пакет содержал ссылку на файл sourceMap, размещенный в CDN. Если кто-то захочет что-то отладить, браузер загрузит sourceMap и позволит отлаживать исходный код, который они написали, а не какой-то уменьшенный, запутанный динамический пакет.
Поэтому мы отправились решать и это. Похоже, что спецификация SourceMap версии 3 имела в виду этот вариант использования. Я был так счастлив!
Все, что вам нужно было, это добавить URL (см. строку 5 ниже) в каждый раздел, чтобы обеспечить загрузку конкретной исходной карты для этого раздела! Легкий!

Итак, мы очистили предыдущие аннотации исходной карты и добавили новые в сгенерированные пакеты.
Затем мы опробовали его, и консоль разработчика Chrome сообщила об этом следующее:
SourceMap ««/api/v1/cache.js.map?bundles» содержит неподдерживаемое поле URL в одном из своих разделов».
Итак, немного поискав, мы нашли эту ссылку, которая объясняет, что эта функция фактически не реализована в Chrome.
Мы вернулись к чертежной доске и решили, что можем повторно использовать те же концепции, что и при связывании Javascript: мы сделали это динамическим запросом на пакет sourceMap.
Мы добавили новый вызов API, который получает запрос sourceMap и создает объединенную исходную карту на основе созданного пакета.
В конце файла динамического пакета мы добавили единственный URL-адрес sourceMap, который является фактическим вызовом API.
Вот так://# sourceMappingURL=http://example.com/path/to/your/sourcemap.map
Для этого мы добавили информацию о том, где начинается каждый пакет и какие исходные карты он использует для запроса, затем мы объединили эти SourceMaps и отправили их обратно в соответствии со спецификацией (так же, как вы видите в строке 6 в приведенном выше примере).
Суммирование
Итак, после небольшого экскурса вы теперь знаете, как мы пытались выжать больше производительности из нашей системы, не меняя при этом принцип работы наших DEV (пока). Для меня, как попытка (почти) бесплатного повышения производительности, это был классный опыт обучения.
Если вы зашли так далеко, я надеюсь, что вы тоже почерпнули что-то из нашего опыта.
Ваше здоровье,
IC