Измерение показателя Interaction to Next Paint (INP)

1. Введение

В этой практической работе показано, как измерять показатель Interaction to Next Paint (INP) с помощью библиотеки web-vitals.

Требования

Что вы узнаете

  • Как добавить библиотеку web-vitals на страницу и использовать ее данные атрибуции.
  • Используйте данные атрибуции, чтобы понять, где и как улучшить показатель INP.

Что вам понадобится

  • Компьютер, на котором можно клонировать код из GitHub и выполнять команды npm.
  • Текстовый редактор.
  • Последняя версия Chrome, чтобы все показатели взаимодействия работали.

2. Подготовка

Как получить и запустить код

Код находится в репозитории web-vitals-codelabs.

  1. Клонируйте репозиторий в терминале: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git.
  2. Перейдите в клонированный каталог: cd web-vitals-codelabs/measuring-inp.
  3. Установите зависимости: npm ci.
  4. Запустите веб-сервер: npm run start.
  5. Откройте в браузере страницу http://localhost:8080/.

Как попробовать страницу

В этой практической работе на примере сайта Gastropodicon (популярного справочника по анатомии улиток) рассматриваются потенциальные проблемы с INP.

Скриншот демостраницы Gastropodicon

Попробуйте взаимодействовать со страницей, чтобы понять, какие действия выполняются медленно.

3. Как ориентироваться в инструментах разработчика Chrome

Откройте инструменты разработчика, выбрав Другие инструменты > Инструменты разработчика, нажав правой кнопкой мыши на странице и выбрав Проверить или используя сочетание клавиш.

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

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

Скриншот панели "Производительность" инструментов разработчика рядом с приложением. Выбрано замедление работы процессора в четыре раза.

4. Установка библиотеки web-vitals

web-vitals – это библиотека JavaScript, которая позволяет отслеживать показатели Web Vitals, с которыми сталкиваются ваши пользователи. С помощью этой библиотеки можно собирать такие значения и отправлять их в конечную точку Аналитики для дальнейшего анализа, чтобы понять, когда и где происходят медленные взаимодействия.

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

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

Вы можете использовать две версии web-vitals:

  • Стандартная сборка используется, если вы хотите отслеживать значения показателей Core Web Vitals при загрузке страницы.
  • Сборка "Атрибуция" добавляет к каждому показателю дополнительную отладочную информацию, чтобы можно было понять, почему показатель имеет то или иное значение.

Для измерения INP в этой практической работе нам нужна сборка атрибуции.

Добавьте карточку "web-vitals" в devDependencies проекта, выполнив команду npm install -D web-vitals.

Добавьте на страницу web-vitals:

Добавьте версию скрипта для атрибуции в конец index.html и зарегистрируйте результаты в консоли:

<script type="module">
  import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';

  onINP(console.log);
</script>

Попробовать

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

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

В web-vitals есть вариант reportAllChanges для более подробных отчетов. Если этот параметр включен, регистрируется не каждое взаимодействие, а только те, которые медленнее предыдущих.

Попробуйте добавить в скрипт параметр и снова взаимодействовать со страницей:

<script type="module">
  import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';

  onINP(console.log, {reportAllChanges: true});
</script>

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

Скриншот консоли инструментов разработчика с успешно выведенными сообщениями INP

5. Что входит в атрибуцию?

Начнем с первого взаимодействия, которое большинство пользователей будет иметь со страницей, – диалогового окна согласия на использование файлов cookie.

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

Нажмите Да, чтобы принять файлы cookie (демоверсия), и посмотрите данные INP, которые теперь регистрируются в консоли DevTools.

Объект данных INP, зарегистрированный в консоли инструментов разработчика

Информация верхнего уровня доступна как в стандартной, так и в атрибутивной сборке web-vitals:

{
  name: 'INP',
  value: 344,
  rating: 'needs-improvement',
  entries: [...],
  id: 'v4-1715732159298-8028729544485',
  navigationType: 'reload',
  attribution: {...},
}

Время от клика пользователя до следующей отрисовки составило 344 миллисекунды, что соответствует неудовлетворительному значению INP. Массив entries содержит все значения PerformanceEntry, связанные с этим взаимодействием. В данном случае это только одно событие клика.

Чтобы узнать, что происходит в этот период, нас больше всего интересует свойство attribution. Чтобы создать данные атрибуции, web-vitals определяет, какой длинный кадр анимации (LoAF) перекрывает событие клика. После этого LoAF может предоставить подробные данные о том, как было потрачено время в течение этого кадра, начиная со скриптов, которые выполнялись, и заканчивая временем, затраченным на обратный вызов requestAnimationFrame, стиль и макет.

Разверните свойство attribution, чтобы посмотреть подробную информацию. Данные гораздо полнее.

attribution: {
  interactionTargetElement: Element,
  interactionTarget: '#confirm',
  interactionType: 'pointer',

  inputDelay: 27,
  processingDuration: 295.6,
  presentationDelay: 21.4,

  processedEventEntries: [...],
  longAnimationFrameEntries: [...],
}

Сначала приводится информация о том, с чем взаимодействовал пользователь:

  • interactionTargetElement – активная ссылка на элемент, с которым взаимодействовал пользователь (если элемент не был удален из DOM).
  • interactionTarget – селектор для поиска элемента на странице.

Далее приводится общая информация о времени:

  • inputDelay – время между началом взаимодействия пользователя (например, кликом мышью) и запуском прослушивателя событий для этого взаимодействия. В этом случае задержка ввода составила всего 27 миллисекунд, даже при включенном ограничении частоты процессора.
  • processingDuration – время, необходимое для выполнения прослушивателей событий. Часто на страницах бывает несколько прослушивателей для одного события (например, pointerdown, pointerup и click). Если все они выполняются в одном кадре анимации, то объединяются в это время. В этом случае время обработки составляет 295, 6 миллисекунды – большую часть времени взаимодействия.
  • presentationDelay – время с момента завершения работы прослушивателей событий до момента, когда браузер закончит отрисовку следующего кадра. В этом случае – 21, 4 мс.

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

Если углубиться в анализ, то можно заметить, что массив processedEventEntries содержит пять событий, в отличие от одного события в массиве INP entries верхнего уровня. Разве это не одно и то же?

processedEventEntries: [
  {
    name: 'mouseover',
    entryType: 'event',
    startTime: 1801.6,
    duration: 344,
    processingStart: 1825.3,
    processingEnd: 1825.3,
    cancelable: true
  },
  {
    name: 'mousedown',
    entryType: 'event',
    startTime: 1801.6,
    duration: 344,
    processingStart: 1825.3,
    processingEnd: 1825.3,
    cancelable: true
  },
  {name: 'mousedown', ...},
  {name: 'mouseup', ...},
  {name: 'click', ...},
],

Запись верхнего уровня – это событие INP, в данном случае клик. Атрибуция processedEventEntries – это все события, которые были обработаны в течение одного кадра. Обратите внимание, что в него входят и другие события, например mouseover и mousedown, а не только клик. Информация об этих событиях может быть очень важна, если они также были медленными, поскольку все они влияют на скорость реагирования.

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

longAnimationFrameEntries

Разверните запись LoAF:

longAnimationFrameEntries: [{
  name: 'long-animation-frame',
  startTime: 1823,
  duration: 319,

  renderStart: 2139.5,
  styleAndLayoutStart: 2139.7,
  firstUIEventTimestamp: 1801.6,
  blockingDuration: 268,

  scripts: [{...}]
}],

Здесь можно найти много полезных значений, например время, затраченное на оформление. Подробнее о свойствах Long Animation Frames API… Сейчас нас в основном интересует свойство scripts, которое содержит записи со сведениями о скриптах, ответственных за длительную обработку кадра:

scripts: [{
  name: 'script',
  invoker: 'BUTTON#confirm.onclick',
  invokerType: 'event-listener',

  startTime: 1828.6,
  executionStart: 1828.6,
  duration: 294,

  sourceURL: 'http://localhost:8080/third-party/cmp.js',
  sourceFunctionName: '',
  sourceCharPosition: 1144
}]

В этом случае можно сказать, что время в основном было потрачено на одну event-listener, вызванную на BUTTON#confirm.onclick. Мы даже можем посмотреть URL источника скрипта и позицию символа, где была определена функция.

Заказ навынос

Что можно узнать об этом деле из данных атрибуции?

  • Взаимодействие было вызвано кликом по элементу button#confirm (из attribution.interactionTarget и свойства invoker в записи атрибуции скрипта).
  • Основное время было потрачено на выполнение прослушивателей событий (attribution.processingDuration по сравнению с общим показателем value).
  • Код медленного прослушивателя событий начинается с прослушивателя кликов, определенного в third-party/cmp.js (из scripts.sourceURL).

Этого достаточно, чтобы понять, что нужно оптимизировать.

6. Несколько прослушивателей событий

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

Начните вводить запрос в строке поиска. Что показывают данные атрибуции? Как вы думаете, что происходит?"

Данные атрибуции

Сначала рассмотрим пример тестирования демоверсии.

{
  name: 'INP',
  value: 1072,
  rating: 'poor',
  attribution: {
    interactionTargetElement: Element,
    interactionTarget: '#search-terms',
    interactionType: 'keyboard',

    inputDelay: 3.3,
    processingDuration: 1060.6,
    presentationDelay: 8.1,

    processedEventEntries: [...],
    longAnimationFrameEntries: [...],
  }
}

Это низкое значение INP (с включенным ограничением ЦП) при взаимодействии с элементом input#search-terms с помощью клавиатуры. Большая часть времени (1061 мс из 1072 мс) ушла на обработку.

Однако записи scripts более интересны.

Пробуксовка макета

Первая запись массива scripts содержит важную информацию:

scripts: [{
  name: 'script',
  invoker: 'BUTTON#confirm.onclick',
  invokerType: 'event-listener',

  startTime: 4875.6,
  executionStart: 4875.6,
  duration: 497,
  forcedStyleAndLayoutDuration: 388,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: 'handleSearch',
  sourceCharPosition: 940
},
...]

Большая часть времени обработки приходится на выполнение этого скрипта, который является прослушивателем input (вызывающий объект – INPUT#search-terms.oninput). Указано название функции (handleSearch), а также позиция символа в исходном файле index.js.

Однако появилось новое свойство: forcedStyleAndLayoutDuration. Время, затраченное на вызов скрипта, в течение которого браузер был вынужден перекомпоновать страницу. Другими словами, 78% времени (388 мс из 497), затраченного на выполнение этого прослушивателя событий, было потрачено на перекомпоновку.

Эту проблему нужно устранить в первую очередь.

Повторные прослушивания

По отдельности следующие две записи скрипта не представляют собой ничего особенного:

scripts: [...,
{
  name: 'script',
  invoker: '#document.onkeyup',
  invokerType: 'event-listener',

  startTime: 5375.3,
  executionStart: 5375.3,
  duration: 124,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: '',
  sourceCharPosition: 1526,
},
{
  name: 'script',
  invoker: '#document.onkeyup',
  invokerType: 'event-listener',

  startTime: 5673.9,
  executionStart: 5673.9,
  duration: 95,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: '',
  sourceCharPosition: 1526
}]

Оба элемента являются слушателями keyup, которые выполняются один за другим. Слушатели – это анонимные функции (поэтому в свойстве sourceFunctionName ничего не сообщается), но у нас есть исходный файл и позиция символа, поэтому мы можем найти, где находится код.

Странно, что обе ошибки относятся к одному и тому же исходному файлу и позиции символа.

Браузер обработал несколько нажатий клавиш в одном кадре анимации, в результате чего этот прослушиватель событий был запущен дважды до того, как что-либо было отрисовано!

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

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

7. Задержка ввода

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

  • Страница загружается, и основной поток занят первоначальной работой по настройке DOM, макета и стиля страницы, а также оценке и выполнению скриптов.
  • На странице выполняется много действий, например вычисления, анимация на основе скриптов или показ рекламы.
  • Предыдущие взаимодействия обрабатываются так долго, что задерживают последующие, как показано в последнем примере.

На демостранице есть секретная функция: если нажать на логотип улитки вверху страницы, начнется анимация и выполнение ресурсоемких задач JavaScript в основном потоке.

  • Чтобы запустить анимацию, нажмите на логотип улитки.
  • Задачи JavaScript запускаются, когда улитка находится в нижней точке прыжка. Попробуйте взаимодействовать со страницей как можно ближе к нижней части отскока и посмотрите, какое значение INP будет получено.

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

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

Вот пример атрибуции, полученной при фокусировке на окне поиска во время отскока улитки:

{
  name: 'INP',
  value: 728,
  rating: 'poor',

  attribution: {
    interactionTargetElement: Element,
    interactionTarget: '#search-terms',
    interactionType: 'pointer',

    inputDelay: 702.3,
    processingDuration: 4.9,
    presentationDelay: 20.8,

    longAnimationFrameEntries: [{
      name: 'long-animation-frame',
      startTime: 2064.8,
      duration: 790,

      renderStart: 2065,
      styleAndLayoutStart: 2854.2,
      firstUIEventTimestamp: 0,
      blockingDuration: 740,

      scripts: [{...}]
    }]
  }
}

Как и ожидалось, обработчики событий выполнялись быстро – время обработки составило 4,9 мс, а большая часть времени плохого взаимодействия пришлась на задержку ввода – 702,3 мс из 728.

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

В этом случае вам помогут записи скриптов LoAF:

scripts: [{
  name: 'script',
  invoker: 'SPAN.onanimationiteration',
  invokerType: 'event-listener',

  startTime: 2065,
  executionStart: 2065,
  duration: 788,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: 'cryptodaphneCoinHandler',
  sourceCharPosition: 1831
}]

Хотя эта функция не имеет отношения к взаимодействию, она замедлила кадр анимации и поэтому включена в данные LoAF, которые связаны с событием взаимодействия.

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

8. Задержка отрисовки: когда обновление не отображается.

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

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

Как это выглядит?

{
  name: 'INP',
  value: 376,
  rating: 'needs-improvement',
  delta: 352,

  attribution: {
    interactionTarget: '#sidenav-button>svg',
    interactionType: 'pointer',

    inputDelay: 12.8,
    processingDuration: 14.7,
    presentationDelay: 348.5,

    longAnimationFrameEntries: [{
      name: 'long-animation-frame',
      startTime: 651,
      duration: 365,

      renderStart: 673.2,
      styleAndLayoutStart: 1004.3,
      firstUIEventTimestamp: 138.6,
      blockingDuration: 315,

      scripts: [{...}]
    }]
  }
}

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

scripts: [{
  entryType: 'script',
  invoker: 'FrameRequestCallback',
  invokerType: 'user-callback',

  startTime: 673.8,
  executionStart: 673.8,
  duration: 330,

  sourceURL: 'http://localhost:8080/js/side-nav.js',
  sourceFunctionName: '',
  sourceCharPosition: 1193,
}]

В единственной записи в массиве scripts указано, что время было потрачено на user-callback из FrameRequestCallback. В этот раз задержка презентации вызвана обратным вызовом requestAnimationFrame.

9. Заключение

Агрегирование данных полей

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

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

Вместо этого вам нужно подумать о своем приложении, чтобы определить, какие данные будут наиболее полезны и как их можно агрегировать. Например, перед тем как передавать данные атрибуции, вы можете заменить селектор web-vitals собственным идентификатором, основанным на компоненте, в котором находится целевой объект, или на ролях ARIA, которые выполняет целевой объект.

Аналогично, записи scripts могут содержать хеши на основе файлов в своих путях sourceURL, что затрудняет их объединение. Однако вы можете удалить хеши на основе известного процесса сборки, прежде чем отправлять данные обратно.

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

Атрибуция везде!

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

Теперь вы можете использовать данные атрибуции INP на любом сайте.

Даже если у вас нет доступа к редактированию страницы, вы можете воссоздать процесс из этой практической работы, запустив следующий фрагмент кода в консоли инструментов разработчика, чтобы посмотреть, что вы сможете найти:

const script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.iife.js';
script.onload = function () {
  webVitals.onINP(console.log, {reportAllChanges: true});
};
document.head.appendChild(script);

Подробнее…