Измерение взаимодействия со следующей покраской (INP)

1. Введение

Это интерактивный практический урок, посвященный изучению измерения взаимодействия с объектом NextPaint (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

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

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

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

Скриншот панели «Производительность» в инструментах разработчика рядом с приложением, с выбранным параметром «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>

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

Скриншот консоли DevTools с успешно выведенными в неё сообщениями INP.

5. Что подразумевается под атрибуцией?

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

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

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

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

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

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

Продолжительность времени от момента щелчка пользователя до следующего кадрирования составила 344 миллисекунды — это показатель, требующий улучшения . Массив 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 ). Если все они выполняются в одном кадре анимации, они объединяются в это время. В этом случае processingDuration составляет 295,6 миллисекунд — основную часть времени INP.
  • presentationDelay : время от завершения обработки событий обработчиками до завершения отрисовки следующего кадра браузером. В данном случае — 21,4 миллисекунды.

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

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

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: [{...}]
}],

Здесь есть ряд полезных значений, например, указание времени, затраченного на стилизацию. В статье об 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 миллисекунд общего времени INP — была затрачена на обработку.

Однако записи 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. Во многих случаях он может указать вам точное место в скрипте, с которого следует начать оптимизацию.

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

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

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);

Узнать больше