Pomiar interakcji do kolejnego wyrenderowania (INP)

1. Wprowadzenie

To interaktywne ćwiczenia w programie, dzięki którym dowiesz się, jak mierzyć interakcje z następnym wyrenderowaniem (INP) za pomocą biblioteki web-vitals.

Wymagania wstępne

Czego się nauczysz

  • Jak dodać do strony bibliotekę web-vitals i korzystać z jej danych atrybucji.
  • Wykorzystaj dane o atrybucji, aby zdiagnozować, gdzie i jak zacząć ulepszać INP.

Co będzie potrzebne

  • Komputer z możliwością kopiowania kodu z GitHuba i uruchamiania poleceń npm.
  • Edytor tekstu.
  • Najnowsza wersja Chrome umożliwiająca pomiar wszystkich interakcji.

2. Konfiguracja

Pobierz i uruchom kod

Kod znajduje się w repozytorium web-vitals-codelabs.

  1. Skopiuj repozytorium do terminala: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git.
  2. Przejdź do sklonowanego katalogu: cd web-vitals-codelabs/measuring-inp.
  3. Zainstaluj zależności: npm ci.
  4. Uruchom serwer WWW: npm run start.
  5. W przeglądarce otwórz stronę http://localhost:8080/.

Wypróbuj stronę

W ramach tych ćwiczeń w programie wykorzystano Gastropodicon (popularną witrynę z informacjami o anatomii ślimaków), aby zbadać potencjalne problemy z INP.

Zrzut ekranu strony demonstracyjnej Gastropodicon

Spróbuj wejść w interakcję ze stroną, by przekonać się, które interakcje są powolne.

3. Otwieranie Narzędzi deweloperskich w Chrome

Otwórz Narzędzia deweloperskie w sekcji Więcej narzędzi > menu Narzędzia dla deweloperów, klikając stronę prawym przyciskiem myszy i wybierając Zbadaj lub użyj skrótu klawiszowego.

W tym ćwiczeniu w programowaniu użyjemy zarówno panelu Wydajność, jak i Konsoli. W każdej chwili możesz się między nimi przełączać na kartach u góry Narzędzi deweloperskich.

  • Problemy z INP występują najczęściej na urządzeniach mobilnych, więc przełącz się na emulację wyświetlacza mobilnego.
  • Jeśli testujesz aplikację na komputerze lub laptopie, wydajność będzie prawdopodobnie znacznie lepsza niż na prawdziwym urządzeniu mobilnym. Aby zobaczyć bardziej realistyczne dane, naciśnij ikonę koła zębatego w prawym górnym rogu panelu Wydajność i wybierz 4-krotne spowolnienie procesora.

Zrzut ekranu przedstawiający panel wydajności narzędzi deweloperskich obok aplikacji z wybranym 4-krotnym spowolnieniem procesora

4. Instaluję web-vitals

web-vitals to biblioteka JavaScriptu do pomiaru danych o wskaźnikach internetowych, z których korzystają użytkownicy. Możesz użyć biblioteki, aby przechwytywać te wartości, a następnie przesyłać je do punktu końcowego Analytics do późniejszej analizy, aby ustalić, kiedy i gdzie występują powolne interakcje.

Bibliotekę do strony można dodać na kilka różnych sposobów. Sposób instalacji biblioteki w witrynie zależy od sposobu zarządzania zależnościami, procesu kompilacji i innych czynników. Wszystkie opcje znajdziesz w dokumentacji biblioteki.

To ćwiczenie w Codelabs zostanie zainstalowane z npm i bezpośrednio wczyta skrypt, co pozwoli uniknąć zagłębiania się w konkretny proces kompilacji.

Istnieją 2 wersje obiektu web-vitals, których możesz użyć:

  • „Standardowy” musisz używać kompilacji, jeśli chcesz śledzić wartości podstawowych wskaźników internetowych podczas wczytywania strony.
  • „Atrybucja” dodaje do każdego wskaźnika dodatkowe informacje na potrzeby debugowania, aby można było zdiagnozować wartość danych.

Do pomiaru INP w tym ćwiczeniach z programowania potrzebna jest kompilacja atrybucji.

Dodaj web-vitals do zasobu devDependencies projektu, uruchamiając npm install -D web-vitals

Dodaj web-vitals do strony:

Dodaj wersję atrybucji skryptu na dole sekcji index.html i zarejestruj wyniki w konsoli:

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

  onINP(console.log);
</script>

Wypróbuj

Przy otwartej konsoli spróbuj jeszcze raz wejść w interakcję ze stroną. Gdy klikasz dookoła strony, nic nie jest rejestrowane.

Wartość INP jest mierzona przez cały cykl życia strony, więc domyślnie web-vitals nie zgłasza INP, dopóki użytkownik nie opuści lub nie zamknie strony. Jest to idealne rozwiązanie w przypadku zastosowań typu beaconing na przykład w przypadku statystyk, ale nie do interaktywnego debugowania.

web-vitals udostępnia opcję reportAllChanges, która zapewnia bardziej szczegółowe raporty. Po włączeniu tej opcji raportowana jest nie każda interakcja, ale każda interakcja jest wolniejsza niż poprzednie.

Spróbuj dodać tę opcję do skryptu i ponownie wejść w interakcję ze stroną:

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

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

Odświeżenie strony i interakcje powinny być teraz raportowane w konsoli. Aktualizujemy je za każdym razem, gdy pojawi się nowa najwolniejsza strona. Możesz na przykład wpisać tekst w polu wyszukiwania, a potem usunąć dane wejściowe.

Zrzut ekranu konsoli Narzędzi deweloperskich z wydrukowanymi komunikatami INP

5. Co oznacza atrybucja?

Zaczniemy od pierwszej interakcji ze stroną – od okna z prośbą o zgodę na stosowanie plików cookie.

Wiele stron zawiera skrypty wymagające synchronicznego wywoływania plików cookie po zaakceptowaniu ich przez użytkownika, przez co kliknięcie przebiega wolniej. Tak się dzieje w tym przypadku.

Kliknij Tak, aby zaakceptować (demograficzne) pliki cookie, i przejrzyj dane INP zarejestrowane teraz w konsoli Narzędzi deweloperskich.

Obiekt danych INP zarejestrowany w konsoli Narzędzi deweloperskich

Te najważniejsze informacje są dostępne zarówno w ramach standardowych funkcji Web Vitals, jak i w przypadku atrybucji w ramach:

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

Czas od kliknięcia przez użytkownika do kolejnego wyrenderowania wyniósł 344 milisekundy – „wymagana poprawa” INP. Tablica entries zawiera wszystkie wartości PerformanceEntry powiązane z tą interakcją – w tym przypadku tylko jedno zdarzenie kliknięcia.

Aby jednak dowiedzieć się, co dzieje się w tym czasie, najbardziej interesuje nas usługa attribution. Aby utworzyć dane atrybucji, web-vitals ustala, która długie klatki animacji (LoAF) pokrywa się ze zdarzeniem kliknięcia. LoAF może następnie udostępnić szczegółowe dane o czasie spędzonym w tej klatce – od uruchomionych skryptów po czas spędzony w wywołaniu zwrotnym, stylu i układzie requestAnimationFrame.

Aby zobaczyć więcej informacji, rozwiń właściwość attribution. Dane są znacznie bogatsze.

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

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

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

Najpierw są informacje o tym, z czym nawiązano interakcję:

  • interactionTargetElement: odwołanie do aktywnego elementu, z którym nastąpiła interakcja (jeśli nie został on usunięty z DOM).
  • interactionTarget: selektor do znalezienia elementu na stronie.

Następnie mamy ogólny podział czasu:

  • inputDelay: czas od momentu rozpoczęcia interakcji przez użytkownika (np. kliknięcia myszki) do uruchomienia detektora tej interakcji. W tym przypadku opóźnienie sygnału wejściowego wyniesieło około 27 milisekund, nawet przy włączonym ograniczaniu wykorzystania procesora.
  • processingDuration: czas do zakończenia działania detektorów zdarzeń. Strony często mają wielu detektorów pojedynczego zdarzenia (np. pointerdown, pointerup i click). Jeśli wszystkie elementy znajdują się w tej samej klatce animacji, zostaną połączone w ten czas. W tym przypadku czas przetwarzania trwa 295,6 milisekunda, czyli większość czasu INP.
  • presentationDelay: czas od zakończenia działań detektorów zdarzeń do momentu, w którym przeglądarka zakończy malowanie następnej klatki. W tym przypadku będzie to 21, 4 milisekundy.

Fazy INP mogą być niezbędnym sygnałem do diagnozowania potrzeb optymalizacji. Więcej informacji na ten temat znajdziesz w przewodniku po optymalizacji INP.

Co więcej, tabela processedEventEntries zawiera pięć zdarzeń, a nie tylko pojedyncze zdarzenia z tablicy INP entries najwyższego poziomu. Na czym polega różnica?

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', ...},
],

Wpis najwyższego poziomu to zdarzenie INP, w tym przypadku kliknięcie. Atrybucja processedEventEntries to wszystkie zdarzenia, które zostały przetworzone w ramach tej samej klatki. Zwróć uwagę, że obejmuje on też inne zdarzenia, np. mouseover i mousedown, a nie tylko zdarzenie kliknięcia. Wiedza o tych innych zdarzeniach może być ważna, jeśli również były powolne, ponieważ wszystkie przyczyniły się do wolnego reagowania.

Ostatnia klasa to tablica longAnimationFrameEntries. To może być pojedynczy wpis, ale w niektórych przypadkach interakcja może obejmować wiele klatek. Oto najprostszy przypadek z pojedynczą długą klatką animacji.

longAnimationFrameEntries

Rozwijanie wpisu LoAF:

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

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

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

Jest tu kilka przydatnych wartości, np. informacji o ilości czasu poświęcanego na określanie stylu. Więcej informacji o tych właściwościach znajdziesz w artykule o interfejsie Long Animation Frames API. Obecnie interesuje nas przede wszystkim właściwość scripts, która zawiera wpisy dostarczające szczegółowe informacje o skryptach odpowiedzialnych za długotrwałą ramkę:

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
}]

W tym przypadku można stwierdzić, że czas został wykorzystany głównie w jednym obiekcie event-listener wywołanym BUTTON#confirm.onclick. Widzimy nawet adres URL źródła skryptu i pozycję znaku w miejscu, w którym funkcja została zdefiniowana.

Na wynos

Co można ustalić w tym przypadku na podstawie tych danych atrybucji?

  • Interakcja została wywołane przez kliknięcie elementu button#confirm (z attribution.interactionTarget i właściwości invoker w wpisie o atrybucji skryptu).
  • Czas poświęcony głównie na wykonywanie detektorów zdarzeń (od attribution.processingDuration w porównaniu z łącznym wskaźnikiem value).
  • Kod detektora powolnych zdarzeń zaczyna się od odbiornika kliknięć zdefiniowanego w zasadzie third-party/cmp.js (z scripts.sourceURL).

To wystarczająco dużo danych, by określić obszary wymagające optymalizacji.

6. Wiele detektorów zdarzeń

Odśwież stronę, aby konsola Narzędzi deweloperskich była przejrzysta, a interakcja z prośbą o zgodę na stosowanie plików cookie nie była już najdłuższą interakcją.

Zacznij pisać w polu wyszukiwania. Co zawierają dane o atrybucji? Jak myślisz, co się dzieje?

Dane atrybucji

Najpierw wyświetl ogólny skan jednego z przykładów testowania wersji demonstracyjnej:

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

To niska wartość INP (z włączonym ograniczaniem wykorzystania procesora) wynikającą z interakcji klawiatury z elementem input#search-terms. Przez większość czasu – 1061 milisekund z łącznej liczby 1072 milisekund – INP – był poświęcany czas przetwarzania.

Wpisy w języku scripts są jednak ciekawsze.

Rzutowanie układu

Pierwszy wpis tablicy scripts zapewnia nam cenny kontekst:

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

Większość czasu przetwarzania zachodzi podczas wykonywania tego skryptu, który jest odbiornikiem input (wywołującym jest INPUT#search-terms.oninput). Podana jest nazwa funkcji (handleSearch) oraz pozycja znaku w pliku źródłowym index.js.

Dostępna jest jednak nowa właściwość: forcedStyleAndLayoutDuration. Był to czas spędzony w trakcie wywoływania tego skryptu, podczas którego przeglądarka została zmuszona do przekazania strony. Innymi słowy, 78% czasu – 388 milisekund z 497 – poświęcane na wykonanie tego detektora zdarzeń było poświęcane na thrashowanie układu.

Ten problem powinien być priorytetowo rozwiązany.

Słuchacze powracający

Same kolejne 2 kolejne pozycje skryptu nie są szczególnie istotne:

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
}]

Obie wpisy to detektory keyup, które są wykonywane jeden po drugim. Detektory to funkcje anonimowe (dlatego właściwość sourceFunctionName nie zawiera żadnych danych), ale nadal mamy plik źródłowy i pozycję znaku, więc możemy ustalić, gdzie znajduje się kod.

Dziwne jest, że oba adresy pochodzą z tego samego pliku źródłowego i tej samej pozycji znaku.

W rezultacie przeglądarka przetworzyła wiele naciśnięć klawiszy w ramach jednej klatki animacji, przez co detektor zdarzeń uruchomił się dwukrotnie, zanim zdołano cokolwiek wyrenderować.

Ten efekt może się również nasilać – im dłużej detektory zdarzeń minęły, tym więcej mogą przybyć dodatkowych zdarzeń wejściowych, co znacznie wydłuża powolną interakcję.

Ponieważ jest to interakcja z wyszukiwaniem lub autouzupełnianiem, dobrym rozwiązaniem będzie osłabienie danych wejściowych, tak by na klatkę można było przetworzyć co najwyżej 1 naciśnięcie klawisza.

7. Opóźnienie wejściowe

Typowym powodem opóźnień danych wejściowych – czas od interakcji użytkownika do momentu, w którym detektor zdarzeń może rozpocząć przetwarzanie interakcji – jest to, że wątek główny jest zajęty. Może to mieć kilka przyczyn:

  • Strona się wczytuje, a wątek główny jest zajęty konfigurowaniem DOM, układem i stylem strony oraz ocenianiem i uruchamianiem skryptów.
  • Strona jest przeważnie zajęta – na przykład trwają obliczenia, animacje oparte na skryptach lub reklamy.
  • Przetwarzanie poprzednich interakcji trwa tak długo, że opóźniają przyszłe interakcje, co zaobserwowaliśmy w ostatnim przykładzie.

Na stronie demonstracyjnej znajduje się tajna funkcja: gdy klikniesz logo ślimaka u góry strony, rozpocznie się animowanie i przeprowadzenie intensywnej pracy w języku JavaScript.

  • Kliknij logo ślimaka, aby rozpocząć animację.
  • Zadania JavaScript są wyzwalane, gdy ślimak znajduje się na dole strony odsyłającej. Postaraj się wejść w interakcję ze stroną jak najbliżej dołu odrzuceń i sprawdź, jak wysoki wskaźnik INP możesz wywołać.

Na przykład nawet jeśli nie aktywujesz żadnych innych detektorów zdarzeń, takich jak kliknięcie i skupienie się na polu wyszukiwania w chwili, gdy ślimak wyskakiwał, działanie wątku głównego spowoduje, że strona przestanie odpowiadać przez zauważalny czas.

Na wielu stronach skomplikowana praca w wątku głównym nie jest tak dobra, ale to dobry przykład tego, jak można ją zidentyfikować w danych atrybucji INP.

Oto przykład atrybucji polegającej na skupieniu się tylko na polu wyszukiwania podczas skakania ślimaka:

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

Zgodnie z przewidywaniami detektory zdarzeń były wykonywane szybko – czas przetwarzania wynosił 4,9 milisekunda, a większość słabych interakcji była poświęcana w opóźnieniu danych wejściowych, co daje 702,3 milisekunda z 728.

Taka sytuacja może być trudna do debugowania. Mimo że wiemy, z czym i w jaki sposób użytkownik wchodził w interakcję, wiemy też, że ta część interakcji zakończyła się szybko i nie była problemem. Zamiast tego pojawiło się na stronie coś innego, co opóźniło interakcję z rozpoczęciem przetwarzania, ale skąd mamy wiedzieć, od czego zacząć szukać?

Wpisy skryptu LoAF mogą pomóc Ci w tej kwestii:

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
}]

Mimo że ta funkcja nie miała nic wspólnego z interakcją, spowolniła klatkę animacji i dlatego jest uwzględniana w danych LoAF, które są połączone ze zdarzeniem interakcji.

Widać tutaj, jak została uruchomiona funkcja opóźniania przetwarzania interakcji (przez detektor animationiteration), która funkcja była za to odpowiedzialna i gdzie znajduje się w plikach źródłowych.

8. Opóźnienie prezentacji: gdy aktualizacja nie zostanie wyrenderowana

Opóźnienie prezentacji mierzy czas od zakończenia działania detektorów zdarzeń do momentu, gdy przeglądarka jest w stanie wyrenderować nową klatkę na ekranie i wyświetlić użytkownikowi informację zwrotną.

Odśwież stronę, aby ponownie zresetować wartość INP, a następnie otwórz menu z 3 kreskami. Po otwarciu występują jakieś problemy.

Jak to wygląda?

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

Tym razem to opóźnienie prezentacji stanowi większość powolnej interakcji. Oznacza to, że to, co blokuje wątek główny, następuje po zakończeniu detektorów zdarzeń.

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,
}]

Gdy patrzymy na pojedynczy wpis w tablicy scripts, widzimy czas spędzony w tabeli user-callback w tabeli FrameRequestCallback. Tym razem opóźnienie prezentacji jest spowodowane wywołaniem zwrotnym requestAnimationFrame.

9. Podsumowanie

Agregowanie danych pól

Łatwo to zrobić, analizując pojedynczy wpis atrybucji INP podczas wczytywania strony. W jaki sposób można agregować te dane na potrzeby debugowania INP na podstawie danych z pól? Ilość pomocnych szczegółów tak naprawdę utrudnia ten proces.

Bardzo dobrze jest na przykład dowiedzieć się, który element strony jest częstym źródłem powolnych interakcji. Jeśli jednak na stronie są skompilowane nazwy klas CSS, które zmieniają się z kompilacji, selektory web-vitals tego samego elementu mogą się różnić w zależności od kompilacji.

Zamiast tego trzeba wymyślić konkretną aplikację, aby określić, co jest najbardziej przydatne i w jaki sposób można agregować dane. Na przykład przed zwróceniem danych atrybucji typu beacon możesz zastąpić selektor web-vitals własnym identyfikatorem utworzonym na podstawie komponentu, w którym znajduje się cel, lub ról ARIA realizowanych przez ten cel.

Podobnie wpisy scripts mogą mieć w ścieżkach sourceURL oparte na plikach hasze, co utrudnia ich łączenie. Możesz jednak przed wysłaniem danych z powrotem usunąć te hasze zgodnie ze znanym procesem kompilacji.

Niestety nie ma łatwej ścieżki z danymi, ale nawet wykorzystanie ich podzbioru jest cenniejsze niż żadne dane atrybucji w procesie debugowania.

Atrybucja w każdym miejscu

Atrybucja INP oparta na LoAF to zaawansowane narzędzie ułatwiające debugowanie. Udostępnia szczegółowe dane na temat tego, co dokładnie wydarzyło się podczas INP. W wielu przypadkach może wskazać dokładne miejsce w skrypcie, od którego należy rozpocząć działania optymalizacyjne.

Teraz możesz używać danych atrybucji INP w dowolnej witrynie.

Nawet jeśli nie masz uprawnień do edytowania strony, możesz odtworzyć ten proces z tego ćwiczenia, uruchamiając w konsoli Narzędzi deweloperskich ten fragment kodu:

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

Więcej informacji