Всем привет! Меня зовут Илья Алонов. Я фронтенд-инженер и уже второй год занимаюсь производительностью фронта в Авито в платформенной команде Perfomance.  Хочу рассказать про профилирование фронтденда в продакшене с использованием JS Self-Profiling API. Этот инструмент я откопал, когда пытался ускорить исполнение клиентского JS в Авито. Если интересуетесь перформансом веб-приложений — эта статья для вас. С этим материалом я выступал на митапе MoscowJS. Запись моего выступления с докладом «Профилирование фронтенда в проде» можно посмотреть на YouTube-канале AvitoTech. Профилирование —  это измерение и анализ производительности и эффективности кода. Оно помогает разработчикам искать узкие места, ненужные запросы и другие проблемы, которые могут замедлить работу программы или ухудшить пользовательский опыт. Профилирование бывает двух видов: инструментальное и сэмплирующее. Инструментальный профайлер программно дополняет таймингами каждый вызов на стеке. Эти тайминги вычисляют время исполнения того или иного кода. Сэмплирующий профайлер раз в N времени берёт снэпшот (сэмпл) стека вызовов и вычисляет из них время исполнения кода. Под профилированием фронтенда в основном подразумевают использование инструментов браузера, таких как профайлеры в Chrome или Firefox.  Есть метрика Total Blocking Time (TBT). Она отражает, как долго исполнение JS блокирует главный поток браузера — то есть время, в течение которого браузер не отвечает на взаимодействия пользователя с сайтом. У нас оно выглядело вот так:  TBT измеряется для всей страницы. Соответственно, любая блокировка учитывается в этой метрике, и из-за этого её сложно оптимизировать. Эта сложность растёт вместе с количеством загружаемого и исполняемого кода, запросов в сеть, загруженных ресурсов. Подробнее о Total Blocking Time → Спустя множество ручных оптимизаций и предшествовавших им рисерчей я решил, что с этим надо что-то делать. Моих рук и головы (да и в целом всей нашей команды) просто не хватит, чтобы постоянно следить за метрикой на всех страницах, реагировать на просадки и пилить оптимизации. Хочется иметь инструмент, который поможет командам самим следить за перформансом своего кода в рантайме. С такими мыслями я начал искать способ декомпозировать TBT и набрёл на JS Self-Profiling API. JS Self-Profiling API — это браузерный API, который позволяет запускать сэмплирующий профайлер. Не стоит путать с профайлерами, доступными в devtools — это разные вещи. Профайлеры сэмплирующего класса собирают сэмплы стека вызовов, из которых строят трейс — то, что профайлер выплёвывает как результат: JS Self-Profiling API позволяет запускать и останавливать профилирование в любой момент жизненного цикла страницы. Давайте рассмотрим, как это работает на практике. Идея такая: инициализируем профайлер, в нужный момент останавливаем его и получаем трейс. Важно: на момент выхода статьи JS Self-Profiling API доступен в Chrome 94 и выше. Проверьте, совместим ли инструмент с вашим браузером. 1. Инициализируем профайлер: Для инициализации нужно 2 параметра:   sampleRate — частота сэмплирования в миллисекундах. Любой number. На деле для *nix-систем минимальное значение — 10 мс, для Windows — 16 мс. Если поставить значения меньше минимальных, браузер под капотом всё равно выставит нужные.  maxBufferSize — предельное количество сэмплов, которое мы хотим снять.  2. После профилирования получаем объект Trace с 4 полями:  resources — список url-ов исполненных JS-файлов; frames — список фреймов с информацией об исполняемом коде: имена функций и их позиции в загруженном файле; stacks — список стеков вызовов. Элементы списка состоят из двух полей: frameId — ссылка на фрейм из списка frames, parentId — ссылка на вызывающий стек. Фактически в этом списке хранятся деревья вызовов; samples — список всех собранных сэмплов: состоит из timestamp и ссылки на стек.  Вот как это выглядит: Трейс загрузки главной странцы Авито весит ~90Kb. Эта информация понадобится нам чуть позже. Главное стоп-слово для разработчиков, желающих включить профилирование (необязательно в браузере) — оверхед. Давайте смотреть, что с ним у нашей технологии. Во-первых, есть исследование от ребят из Facebook* об оверхеде от JS Self-Profiling API. Они пишут, что в их случае оверхед составляет 1% в скорости загрузки. Во-вторых, я сам провёл A/B-тест в Авито и тоже обнаружил низкий оверхед. На мобильной версии сайта он в среднем составил 1–2% от скорости загрузки и Total Blocking Time при частоте сэмплирования 10 мс.  Итого оверхед у JS Self-Profiling API незначительный, следовательно, можно развлекаться дальше. С оверхедом разобрались, можно приступать к дизайну. Что можно сделать с полученным трейсом? Первое, что приходит в голову — слить его на сервер и там как-то обрабатывать. План выглядит надёжно, давайте посчитаем математику: предположим, что у нас 10K RPM, трейс загрузки в проде весит 90 Кб. Несложными вычислениями получаем 1 Тб в день. И это при условии, что все данные нормализованные: 90Kb / 1024^2 * 10000 * 60 * 24 = 1 Tb/day Это, конечно, можно процессить на сервере и даже хранить, но для этого нужна инфраструктура. А зачем делать инфраструктуру, если можно её не делать? Я решил уменьшить масштаб задачи и обрабатывать данные только о лонгтасках, из которых складывается Total Blocking Time. Чтобы это получилось, нужно вытащить лонгтаски с помощью PerformanceObserver и смержить их с трейсом профилирования: Этот алгоритм хорошо описал Nic Jansma в своей статье. В результате получаются обогащённые лонгтаски — со временем начала, конца и списком причин. Вот как это выглядит: Мы получили обогащённые таски со списком причин. Проблема в том, что файлы, на которые ссылаются фреймы, — это обфусцированный исходный код.  Замапить трейс собранного кода в исходнки можно с помощью сорсмап, но грузить их на клиент дорого и не очень безопасно. Но нам и не нужны сорсмапы на весь код. Давайте подумаем о том, как обычно выглядит фронтендерский проект. Он состоит из:  NPM-пакетов: проприетарных или open source; модулей: компонентов, утилит; стороннего кода: рекламы, трекеров, счётчиков и прочего подобного. Зафиксируем эту мысль в виде конфига: Так мы заранее знаем, профилирование каких модулей нам интересно. Чтобы всё это заработало, на этапе сборки мы вытащим из Source Map информацию о том, куда были собраны нужные нам модули. Тогда для каждого чанка получится сгенерировать некий json-файл, который мы назовём profiling map. Каждая такая мапа будет содержать информацию об интересующих нас модулях, попавших в соответствующий чанк. Profiling map — это JSON с двумя полями:  modules — список профилируемых модулей; intervals — отсортированный список интервалов. Каждый интервал состоит из 3 значений: начало, конец, индекс модуля из списка modules. Вот как это выглядит: Весит Profiling Map немного, в том числе за счёт нормализации данных. Для главной мобильной страницы Авито — всего 500 байт.  В прошлых двух разделах мы научились получать обогащённые лонгтаски и переходить от собранного кода к исходникам. Осталось всё это соединить, чтобы технология работала. Делать это будем в два этапа. В начале загрузки страницы: Запустить PerfomanceObserver.  Запустить Profiler. В конце загрузки страницы:  Остановить PerfomanceObserver и получить лонгтаски. Остановить Profiler и получить трейс.  Слить лонгтаски и трейс, получить обогащённые лонгтаски. Параллельно с мержем лонгтасок и трейса запросить profiling map оттуда, где они лежат. Я положил рядом со статикой. Достать из обогащённых лонгтасок то, что указано в profiling map’ах. Так как профайлер сэмплирующий, разовое профилирование может не зацепить исполнение какого-то кода, и некоторые таски мы не увидим. Решить это можно банально: профилировать чаще. Чем чаще профилируем, тем больше данных имеем, тем лучше наши метрики. CORS. Для всех ресурсов с доменов, отличных от текущего, должны быть настроены cross-origin политики. В противном случае JS Self-Profiling втихую удалит информацию о них из профилирования.  Инструмент воспринимает только исполнение JavaScript. Информацию о сборке мусора, парсинге HTML, вычислениях стилей и лейаута он не даст. Сейчас этого нет, но на GitHub есть предложение о добавлении такой информации в JS Self-Profiling API в виде маркеров на стеке.  Вот как это будет выглядеть: Мы создали инструмент, который:  умеет собирать метрики интересующих нас модулей, почти не влияет на производительность, прост в использовании. Вот как выглядит график метрик пакета avito/core: Данные для графика собраны моим инструментом, который я создал с помощью JS Self-Profiling API Сейчас я развиваю инструмент дальше. Уже появилась поддержка микрофронтов и сильно улучшился DX — теперь включать профилирование пакетов можно из внутреннего дашборда, не запариваясь изменениями конфига. Также у нас есть планы: автоматизировать мониторинг метрик, использовать технологию в NFR. Запись моего выступления «Профилирование фронтенда в проде» на YouTube Статья JS Self-Profiling API In Pratice, на которую я опирался Драфт спецификации JS Self Profiling API Репозиторий JS Self Profiling API на GitHub * Компания Meta, которой принадлежит Facebook, признана экстремистской и запрещена в России Предыдущая статья: «Всё происходит само собой, когда тебе не всё равно»: как из игрока в покер стать руководителем тимлидов  Frontend Engineer