- PVSM.RU - https://www.pvsm.ru -
Обычно под производительностью понимают количество операций за определенный интервал времени и чем их больше, тем лучше. Но такое определение, да и подход в целом, мало применим к фронтенду, потому что у каждого пользователя будет свой «фронтенд». Именно об этом я и хочу поговорить, что же происходит «там», у пользователя, на другой стороне, в реальности, а не на вашем топовом MacBook.
Кроме это, я постараюсь вскользь рассмотреть общие правила оптимизации кода и некоторые ошибки на которые стоит обратить внимание. Ещё расскажу про инструмент [1], который помогает не только в профилировании, но и «из коробки» собирает кучу базовых метрик о производительности вашего приложения (и надеюсь, вы дочитаете этот пост до конца).
Первым делом определим, что же такое производительность фронтенда, а затем уже перейдем к тому, как её измерять. Итак, как я уже сказал, мы не будет мерять некие ops/sec, нам нужны реальные данные, которые бы могли ответить на вопрос, что именно происходит с нашим проектом на каждой стадии его работы. Для этого нам понадобиться следующий набор метрик:
Всё это базовые метрики, без которых невозможно понять, что именно происходит на фронтенде. И не просто на фронтенде, а в реальности, у конечного пользователя. Но чтобы начать собирать эти метрики, для начала нужно научиться их измерять, поэтому давайте вспомни, какие способы есть для аналитики производительности.
Первое, с чего стоит начать, — это конечно Performance API. А именно performance.timing [2], через который вы можете узнать, сколько время заняло у пользователя открытие вашего проекта. Но Performance API покрывает только часть метрики, остальные нужно будет измерять самому, и для этого у нас есть следующие инструменты:
Плюсы | Минусы | |
---|---|---|
console.time('label') | Работает из коробки.
Выводится в консоль. Отображается в DevTools -> Performance -> User Timing. |
Вывод только в DevTools, на сервер никак не отправить (т.е. нет возможности получить значение для последующей аналитики).
Требует Нет цветового кодирования. Нет группировки (что-то типа |
performance.now() | Полный контроль над началом и концом.
Можно отправить на сервер. |
Нет отображения в консоли.
Нет отображения в DevTools -> Performance -> User Timing. Нужно таскать переменную «начала». Нет группировки. |
performance.mark / measure | Полный контроль над началом и концом.
Можно отправить на сервер. Отображается в DevTools -> Performance -> User Timing. |
Нет отображения в консоли.
Чтобы что-то померить, надо задать три уникальные метки и вызвать два метода, а по-хорошему нужно ещё Нет группировки. |
Вот в этот момент я понял, что нужно пилить инструмент, который будет совмещать плюсы вышеперечисленного и по возможности не иметь минусов. Так и появился PerfKeeper [1].
Сейчас я не буду расписывать тут API, не для этого писал документацию [1], да и статья не об этом, а продолжу про то, как собирать метрики.
Как я уже говорил, скорость загрузки вы можете узнать из performance.timing [2], который позволит узнать полный цикл от начала загрузки страницы (время на резолв DNS, установку HTTP Handshake, обработку запроса) и до полной загрузки страницы (DomReady и OnLoad):
В итоге должен получиться следующий набор метрик:
![]() |
![]() |
![]() |
Пример работы расширения navigation [3] для @perf-tools/keeper [1].
Но этого недостаточно, мы получили только базовые значения и до сих пор не знаем, что же именно заняло столько времени. А чтобы это узнать, надо нашпиговать и HTML метриками.
Как я уже говорил, примеры я буду показывать с использование PerfKeeper [1], поэтому первым делом инлайним в <heаd/>
сам PerfKeeper (2,5 Кб) и дальше:
В результате в консоли вы увидите вот такую красоту:
Это классический дедовский способ замера, 100 % работает. Но мир не стоит на месте, и для более точных измерений у нас теперь есть Resource Timing API [4] (а если ресурсы находятся на отдельном домене Timing-Allow-Origin [5] вам в помощь).
И тут стоит поговорить о классических ошибках при первоначальной загрузке страницы, а именно:
Способы оптимизации загрузки страницы:
/> и <sсript src="..."/>
, в идеале грузите JS уже после основного контента;Следующий этап после загрузки — это момент, когда пользователь увидел результат, и интерфейс перешел в интерактивный режим. Для этого нам понадобится Performance Paint Timing [10] и PerformanceObserver [11].
С первым всё просто, вызываем performance.getEntriesByType('paint')
и получаем две метрики:
Пример работы расширения paint [12] для @perf-tools/keeper [1].
А вот со следующей метрикой, Time To Interactive, всё немного интереснее. Нет точного способа определить, когда ваше приложение перешло сало интерактивным, т.е. доступным для пользователя, но это можно косвенно понять по отсутствию longtasks [13]:
// TTI
let ttiLastEntry: PerformanceEntry | undefined;
let ttiPerfObserver: PerformanceObserver;
try {
ttiPerfObserver = new PerformanceObserver((list) => {
ttiLastEntry = list.getEntries().pop();
});
ttiPerfObserver.observe({
entryTypes: ['longtask'],
});
} catch (_) {}
domReady(() => {
// TTI Check
if (ttiPerfObserver) {
let tti: number;
const check = () => {
if (ttiLastEntry) {
tti = ttiLastEntry.startTime + ttiLastEntry.duration;
if (now() - tti >= options.ttiDelay) {
// Последний logntask был давно, будем считать,
// что эра интерактивности настала ;]
send('tti', 'value', 0, tti);
ttiPerfObserver.disconnect();
} else {
setTimeout(check, options.ttiDelay);
}
} else if (tti) {
send('tti', 'value', 0, tti);
ttiPerfObserver.disconnect();
} else {
// Не было logntask, поэтому делаем паузу и если их опять не будет,
// то считает, что приложение уже готово на момент DOMReady!
tti = now();
setTimeout(check, 500);
}
}
// Запускаем проверку
check();
}
});
Пример работы расширения performance [14] для @perf-tools/keeper [1].
Кроме этих базовым метрик ещё нужна именно ваша метрика готовности приложения, т.е. где-то в вашем коде должно быть подобное:
Import { system } from '@perf-tools/keeper';
export function applicationBoot(el, data) {
const app = new Application(el, data);
// Подписываемся на готовность приложения
app.ready(() => {
system.add('application-ready', 0, system.perf.now());
//
application-ready: 3074.000ms
});
return app;
}
Тут огромное поле для метрик и они очень индивидуальны, поэтому расскажу о двух базовых, которые подходят любому проекту, а именно:
first-event — время первого события, например первый click (с делением куда пользователь ткнул), такая метрика особенно актуальна для разного рода поисковых выдачей, списка товаров, новостных лент и т.п. С помощью неё вы сможете контролировать, как меняется время реакции и флоу пользователя от ваших действий (изменений в: дизайн/новые фичи/оптимизации и т.п.)
Пример работы расширения performance [14] для @perf-tools/keeper [1].
latency — задержка при обработке некоторых событий, например: click
, input
, submit
, scroll
и т.д.
Чтобы измерить задержку, достаточно повесить обработчик события на window
с capture = true
и через requestAnimationFrame
посчитать разницу, это и будет задержка:
window.addEventListener(eventType, ({target}) => {
const start = now();
requestAnimationFrame(() => {
const latency = now() - start;
if (latency >= minLatency) {
// ….
}
});
}, true);
Пример работы расширения performance [14] для @perf-tools/keeper [1] когда на клик вычисляется Число Фибонначи.
Это самая интересная метрика, обычно её измеряют через requestAnimationFrame
, и если вам нужно делать постоянный замер FPS, то классический FPSMeter [15] подойдет (хоть он излишне оптимистичен). Но он совсем не годится, если нужно измерить плавность прокрутки страницы, т.к. ему нужен «прогрев». И тут я наткнулся на очень интересный способ [16].
Гениально, на самом деле, просто создаём прозрачный div (1x1px), добавляем ему transition: left 300ms linear
и запускаем его из одного угла в другой, а пока он анимируется, через requestAnimationFrame
проверяем его реальный left, и если новая длина отличается от предыдущей, то увеличиваем количество отрисованных кадров (иначе имеем просадку FPS).
И это ещё не всё, если вы пользуетесь FF, то там просто есть mozPaintCount [17], который отвечает за количество отрисованных кадров, т.е. запоминаем «ДО», а на transitionend
вычисляем разницу.
Итого, без какого-либо прогрева мы точно знаем, перерисовал ли браузер кадр или нет.
Ещё в скором времени обещают нормальное API: http://wicg.github.io/frame-timing/ [18]
![]() |
![]() |
Пример работы расширения fps [19] для @perf-tools/keeper [1].
Оптимизация скрола:
requestAnimationFrame
, либо даже requestIdleCallback
;pointer-events: none
, включение и отключение его может дать обратный эффект, поэтому лучше провести A/B-эксперимент с использованием pointer-events
и без;Тут есть только одно правило: детализируйте так, чтобы вы точно могли ответить, что именно съело время от инициализации приложения до финального запуска. В итоге должно получиться как минимум следующие метрики:
Т.е. на выходе у вас должны получиться такие метрики, по которым вы точно сможете отследить, на какой именно фазе у вас идет просадка.
User Timing
Во-первых, должна быть общая метрика для оценки производительности (время перехода по маршруту) в целом, но также обязательно нужно иметь метрику по каждому маршруту (например у нас это «Список тредов», «Чтение треда», «Поиск» и т.д.), сама метрика должна быть разбита на подметрики:
Без всего этого невозможно понять, в каком месте начинаются проблемы, поэтому у нас многие модули из коробки имеют тайминги (например тот же модуль для XHR имеет startTime
и endTime
, которые автоматически журналируются).
Но и этих метрик недостаточно, чтобы адекватно оценить, что же происходит. Они слишком общие, т.к. мы говорим про SPA, то у вас точно имеется какой-либо Runtime Cache (чтобы не ходить на сервер лишний раз, если вы уже там были), поэтому наши метрики дополнительно разделены на маршрутизацию с cache и без. Ещё, конкретно в нашем случае, мы делим метрику по количеству сущностей в ней. Иначе говоря, нельзя складывать в одну метрику просмотр «Треда» с 1, 5, 10 или 100+ письмами, поэтому если у вас есть вывод какого-либо списка, надо выбрать контрольные точки и дополнительно разделить метрику.
Начнем с памяти. И тут нас ждет большое разочарование. На данный момент есть только нестандартизированный (Chrome only) performance.memory, который выдает до смешного низкие числа. Но всё же их нужно измерять и смотреть, как «течет» приложение со временем:
![]() |
![]() |
![]() |
Пример работы расширения memory [20] для @perf-tools/keeper [1]
Трафик. Чтобы считать трафик, вам понадобится Timing-Allow-Origin [5] (если ресурсы находятся отдельном домене) и Resource Timing API [21], это поможет не просто посчитать трафик, но и детализировать его:
![]() |
![]() |
![]() |
Пример работы расширения resource [22] для @perf-tools/keeper [1].
Что даёт подсчет трафика?
Ну в догонку доклад от моего коллеги Игоря Дружинина [23] на эту тему: Оценка качества работы приложения – мониторинг потребления трафика [24]
Метрики мы расставили, а что дальше? А дальше их нужно куда-то отправить. И тут либо вы поднимаете у себя какой-нибудь Graphite [25], либо, для начала, можно использовать в корыстных целях Google Analytics [26] или подобные для агрегации данных.
И не забывайте, недостаточно просто получить график, по всем важным метрикам должны быть процентили, которые позволят понять, например, у какого процента аудитории проект загружается за <1s, <2s, <3s, <5s, 5s+ и т.п.
Сначала я хотел, написать тут что-то осмысленное, мол используйте WebWorker, не забывайте requestIdleCallback
или что-то из экзотики, например сквозной Runtime Cache сквозь вкладки браузера при помощи SharedWorker или ServiceWorker (который не только про кеширование, если что). Но это всё очень абстрактно, да и многие темы избиты до невозможности, поэтому просто напишу следующее:
object
или Set
(например вместо successSteps.includes(currentStep)
нужно successSteps.hasOwnProperty(currentStep)
), O(1) наше всё.Но если говорить про DOM, то например вместо удаления фрагмента из DOM, лучше его скрыть или deattach-нуть. Если всё же нужно удалить, то вынесите эту операцию в requestIdleCallback
(если возможно), или разделить процесс уничтожения на две фазы: синхронную и асинхронную.
Сразу оговорюсь, используйте с умом такой подход, а то можно и колено прострелить.
Ещё одну интересную технику мы используем на списках, например «Списке Тредов». Суть техники в том, что вместо одного глобального «Списка» и обновления его данных, мы генерируем «Список Тредов» под каждую «Папку». В итоге когда пользователь переходит между «Папками», один список вынимается из DOM (не удаляется), а другой обновляется либо частично, либо не обновляется вовсе. А не весь, как в случае с «Единым Списком».
Всё это даёт мгновенный отклик на действия пользователя.
Математика. Всю математику с легкостью убираем либо в Worker, либо в WebAssembly, это давно уже работает.
Транспиллеры. Ох, многие даже не задумываются о том, что код, который они пишут, проходит через транспиллер. Да, они знают про него, но на этом всё. А вот что же он превратится их уже не волнует. Ведь в DevTools они видят результат source map.
Поэтому изучайте инструменты, которые вы используете, например у того же babel в playground [27] есть возможность посмотреть, во что он генерирует код в зависимости от выбранных пресетов, просто гляньте на тот же yeild
, await
или for of
.
Тонкости языка. Ещё меньше людей знает про мономорфность кода, или банально почему bind медленный и… используйте вы наконец handleEvent
!
Данные и прекеширование. Меньше запросов, больше кеширования. Кроме этого, очень часто мы используем технику «предвидения», это когда в фоне мы подгружаем данные. Например, мы после рендера «Списка тредов» начинаем подгрузку N-непрочитанных тредов в текущей «Папке», чтобы при клике на них пользователь сразу перешел на «Чтение», а не очередной «лоудер». Подобную технику мы используем не только для Данных, но и JS. Например, «Написание Письма» — это огромный бандл (из-за редактора), а пишут письма не все и не сразу, поэтому грузим его в фоне, после инициализации приложения.
Лоудеры. Не знаю почему, но я не видел статей, в которых бы учили, как не делать лоудер, а наоборот, взять хоть презентацию «будущего» React, в которой этой проблеме в рамках Suspense уделено очень много времени. Но ведь идеальное приложение именно без лоудеров, мы в Почте уже очень давно стараемся показывать его только в экстренных ситуациях.
В целом политика у нас такая, нет данных, нет view, нечего рисовать полу-интерфейс, сначала загружаем данные и только потом «рисуем». Именно поэтому мы используем «предвидение» того, куда пользователь собирается пойти и подгружаем эти данные, чтоб юзер не увидел лоудер. Кроме этого, очень сильно в этой задаче помогает наш дата-слой, который обладает персистентностью, т.е. если вы где-то в одном месте запросили «Тред», то при следующем запросе из другого или того же места, запроса не будет, мы возьмем данные из Runtime Cache (точнее ссылку на данные). И так во всем, коллекции тредов тоже всего лишь ссылки на данные.
Но если вы всё же решили делать лоудер, то не забывайте основные правила, которые сделают ваш лоудер менее раздражающим:
Эти нехитрые правила нужны, чтобы лоудер появлялся только на тяжелых запросах и не «мигал» по завершению. Ну а главное, лучший лоудер — это лоудер, который не появился.
Спасибо за внимание, на это всё, измеряйте, анализируйте и используйте PerfKeeper [1] (Live example [28]), а так же вот мой github [29] и twitter [30], на случай вопросов!
Автор: Константин Лебедев
Источник [31]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/320058
Ссылки в тексте:
[1] инструмент: https://github.com/artifact-project/perf-tools/tree/master/keeper#readme
[2] performance.timing: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming#Properties
[3] navigation: https://github.com/artifact-project/perf-tools/tree/master/keeper/ext/navigation#readme
[4] Resource Timing API: https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API
[5] Timing-Allow-Origin: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin
[6] CSSO: https://github.com/css/csso
[7] font-awesome: https://www.npmjs.com/package/font-awesome
[8] применяйте lazy loading: https://css-tricks.com/a-deep-dive-into-native-lazy-loading-for-images-and-frames/
[9] нативная поддержка: https://chromestatus.com/feature/5645767347798016
[10] Performance Paint Timing: https://developer.mozilla.org/en-US/docs/Web/API/PerformancePaintTiming
[11] PerformanceObserver: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
[12] paint: https://github.com/artifact-project/perf-tools/tree/master/keeper/ext/paint#readme
[13] longtasks: https://www.w3.org/TR/longtasks/
[14] performance: https://github.com/artifact-project/perf-tools/tree/master/keeper/ext/performance#readme
[15] FPSMeter: http://darsa.in/fpsmeter/
[16] интересный способ: http://www.kaizou.org/2011/06/effectively-measuring-browser-framerate-using-css/
[17] mozPaintCount: https://developer.mozilla.org/en-US/docs/Web/API/Window/mozPaintCount
[18] http://wicg.github.io/frame-timing/: http://wicg.github.io/frame-timing/
[19] fps: https://github.com/artifact-project/perf-tools/tree/master/keeper/ext/fps#readme
[20] memory: https://github.com/artifact-project/perf-tools/tree/master/keeper/ext/memory#readme
[21] Resource Timing API: https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API
[22] resource: https://github.com/artifact-project/perf-tools/tree/master/keeper/ext/resource#readme
[23] Игоря Дружинина: https://t.me/Edwardgrolsh
[24] Оценка качества работы приложения – мониторинг потребления трафика: https://www.youtube.com/watch?v=aQ5NbkoY9N8
[25] Graphite: https://graphiteapp.org/
[26] Google Analytics: https://github.com/artifact-project/perf-tools/tree/master/keeper/analytics/google
[27] playground: https://babeljs.io/repl
[28] Live example: https://artifact-project.github.io/perf-tools/keeper/
[29] мой github: https://github.com/RubaXa
[30] twitter: https://twitter.com/ibnrubaxa
[31] Источник: https://habr.com/ru/post/454920/?utm_source=habrahabr&utm_medium=rss&utm_campaign=454920
Нажмите здесь для печати.