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

1. Введение

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

Попробуйте страницу

В этой кодовой лаборатории используется Gastroodicon (популярный справочный сайт по анатомии улиток) для изучения потенциальных проблем с INP.

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

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

3. Как попасть в Chrome DevTools

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

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

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

Снимок экрана панели DevTools Performance рядом с приложением с выбранным замедлением процессора в 4 раза.

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

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

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

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

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

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

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

Эта информация верхнего уровня доступна как в стандартной сборке, так и в сборках атрибутивных веб-показателей:

{
  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 миллисекунды — большую часть времени INP.
  • presentationDelay : время с момента завершения прослушивания событий до момента, когда браузер завершит отрисовку следующего кадра. В данном случае 21,4 миллисекунды.

Эти фазы INP могут быть важным сигналом для диагностики того, что необходимо оптимизировать. Дополнительную информацию по этому вопросу можно найти в руководстве Optimize 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: [{...}]
}],

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

Обновите страницу, чтобы консоль DevTools была понятна, а взаимодействие с согласием на использование файлов 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);

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