Pomiar interakcji do kolejnego wyrenderowania (INP)

1. Wprowadzenie

To interaktywne ćwiczenie z programowaniem, które pomoże Ci się nauczyć mierzyć czas od interakcji do kolejnego wyrenderowania (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.
  • Korzystaj z danych atrybucji, aby określić, gdzie i jak zacząć poprawiać INP.

Czego potrzebujesz

  • komputer, na którym można klonować kod z GitHuba i wykonywać polecenia npm;
  • edytor tekstu,
  • najnowszą wersję Chrome, aby wszystkie pomiary interakcji działały prawidłowo;

2. Konfiguracja

Pobieranie i uruchamianie kodu

Kod znajdziesz w repozytorium web-vitals-codelabs.

  1. Sklonuj repozytorium w terminalu: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git.
  2. Przejdź do katalogu sklonowanego repozytorium: 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 adres http://localhost:8080/.

Wypróbuj stronę

W tym laboratorium programistycznym używamy strony Gastropodicon (popularnej strony z informacjami na temat anatomii ślimaka), aby poznać potencjalne problemy z interfejsem INP.

Zrzut ekranu strony demonstracyjnej Gastropodicon

Spróbuj wejść w interakcję ze stroną, aby sprawdzić, które interakcje są powolne.

3. Orientacja w Narzędziach deweloperskich w Chrome

Otwórz Narzędzia deweloperskie z menu Więcej narzędzi > Narzędzia dla deweloperów, klikając prawym przyciskiem myszy na stronie i wybierając Zbadaj lub korzystając z skrótu klawiszowego.

W tym ćwiczeniu będziemy korzystać z panelu Skuteczność i z Konsoli. W każdej chwili możesz przełączać się między nimi na kartach u góry DevTools.

  • Problemy z internetem w sieci INP występują najczęściej na urządzeniach mobilnych, dlatego przełącz się na emulację wyświetlacza mobilnego.
  • Jeśli testujesz na komputerze stacjonarnym lub laptopie, wydajność będzie prawdopodobnie znacznie lepsza niż na prawdziwym urządzeniu mobilnym. Aby uzyskać bardziej realistyczny obraz wydajności, kliknij ikonę koła zębatego w prawym górnym rogu panelu Skuteczność, a następnie wybierz 4-krotne spowolnienie procesora.

Zrzut ekranu przedstawiający panel Wydajność w Narzędziach dla programistów obok aplikacji z wybranym spowolnieniem procesora 4x

4. Instalowanie web-vitals

web-vitals to biblioteka JavaScript służąca do pomiaru danych Web Vitals, z których korzystają użytkownicy. Możesz użyć biblioteki do przechwycenia tych wartości, a potem przekazać je do punktu końcowego Analytics w celu późniejszej analizy. W naszym przypadku chodzi o ustalenie, kiedy i gdzie występują powolne interakcje.

Bibliotekę można dodać do strony na kilka sposobów. Sposób zainstalowania biblioteki w Twojej witrynie zależy od tego, jak zarządzasz zależnościami, procesu kompilacji i innych czynników. Zapoznaj się z dokumentami biblioteki, aby poznać wszystkie dostępne opcje.

Aby uniknąć konieczności uruchamiania konkretnego procesu kompilacji, w tym ćwiczeniu z programowania zainstalujesz npm i bezpośrednio wczytasz skrypt.

Dostępne są 2 wersje web-vitals:

  • Jeśli chcesz śledzić wartości wskaźników podstawowych wskaźników internetowych podczas wczytywania strony, użyj wersji „standardowej”.
  • Wersja „przypisania” dodaje do każdego rodzaju danych dodatkowe informacje debugujące, aby można było zdiagnozować, dlaczego ma on daną wartość.

W tym ćwiczeniu do pomiaru INP potrzebujemy wersji z atrybucją.

Dodaj web-vitals do devDependencies projektu, wykonując polecenie npm install -D web-vitals

Dodaj web-vitals do strony:

Dodaj wersję skryptu dotyczącą atrybucji na dole pliku index.html i zapisz wyniki w konsoli:

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

  onINP(console.log);
</script>

Wypróbuj

Spróbuj ponownie wejść na stronę, gdy konsola jest otwarta. Gdy klikasz na stronie, nic nie jest rejestrowane.

INP jest mierzony przez cały cykl życia strony, więc domyślnie web-vitals nie raportuje INP, dopóki użytkownik nie opuści lub nie zamknie strony. To idealne zachowanie dla sygnalizacji za pomocą beaconów w przypadku usług takich jak analityka, ale nie jest ono zbyt korzystne do debugowania w interaktywny sposób.

web-vitals udostępnia opcję reportAllChanges, która umożliwia bardziej szczegółowe raportowanie. Gdy ta opcja jest włączona, nie każda interakcja jest raportowana, ale za każdym razem, gdy jest ona wolniejsza od poprzedniej, jest raportowana.

Spróbuj dodać opcję do skryptu i ponownie wejść na stronę:

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

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

Odśwież stronę. Interakcje powinny być teraz raportowane do konsoli i aktualizowane, gdy pojawi się nowa najwolniejsza interakcja. Możesz na przykład wpisać coś w polu wyszukiwania, a potem usunąć wpis.

Zrzut ekranu konsoli Narzędzi dla programistów z wyświetlonymi komunikatami INP

5. Co zawiera atrybucja?

Zacznijmy od pierwszej interakcji większości użytkowników ze stroną – okna z prośbą o zgodę na używanie plików cookie.

Wiele stron zawiera skrypty, które wymagają synchronicznego użycia plików cookie po ich zaakceptowaniu przez użytkownika, co powoduje, że kliknięcie staje się wolną interakcją. Właśnie tak to działa.

Kliknij Tak, aby zaakceptować (demonstracyjne) pliki cookie i zobaczyć dane INP zapisane w konsoli DevTools.

Obiekt danych INP zarejestrowany w konsoli Narzędzi deweloperskich

Te informacje najwyższego poziomu są dostępne w standardowych i przypisanych wersjach web-vitals:

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

Czas od kliknięcia do następnego wyrenderowania wynosił 344 ms – wymaga poprawy. Tablica entries zawiera wszystkie wartości PerformanceEntry powiązane z tą interakcją – w tym przypadku tylko jedno zdarzenie kliknięcia.

Aby dowiedzieć się, co się dzieje w tym czasie, najbardziej interesuje nas usługa attribution. Aby utworzyć dane atrybucji, web-vitals sprawdza, które ramki długich animacji (LoAF) pokrywają się ze zdarzeniem kliknięcia. LoAF może następnie dostarczyć szczegółowych danych o tym, jak czas został wykorzystany w ramach danego interwału, od skryptów, które zostały uruchomione, po czas spędzony w wywołaniu funkcji requestAnimationFrame, stylu i układzie.

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

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

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

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

Najpierw znajdziesz informacje o tym, z czym użytkownik wejdzie w interakcję:

  • interactionTargetElement: bieżące odniesienie do elementu, z którym nastąpiła interakcja (jeśli element nie został usunięty z DOM).
  • interactionTarget: selektor do znajdowania elementu na stronie.

Następnie czas jest dzielony na etapy:

  • inputDelay: czas między rozpoczęciem interakcji przez użytkownika (np. kliknięcie myszką) a zaczynkiem działania detektora zdarzenia odpowiadającego tej interakcji. W tym przypadku opóźnienie wejścia wynosiło tylko około 27 ms, nawet przy włączonym ograniczaniu procesora.
  • processingDuration: czas potrzebny detektorom zdarzeń na wykonanie wszystkich operacji. Często strony mają wielu słuchaczy dla jednego zdarzenia (np. pointerdown, pointerupclick). Jeśli wszystkie są wykonywane w tym samym klatce animacji, zostaną połączone w ten czas. W tym przypadku przetwarzanie trwa 295,6 ms, co stanowi większość czasu INP.
  • presentationDelay: czas od zakończenia działania detektorów zdarzeń do zakończenia przez przeglądarkę renderowania następnego kadru. W tym przypadku 21, 4 ms.

Te fazy INP mogą być istotnym sygnałem wskazującym, co należy zoptymalizować. Więcej informacji na ten temat znajdziesz w przewodniku pt. Optymalizacja INP.

Jeśli przyjrzymy się bliżej, zobaczymy, że processedEventEntries zawiera 5 zdarzeń, a nie pojedynczego zdarzenia w 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 przetworzone w ramach tego samego przedziału czasowego. Zwróć uwagę, że zawiera ono inne zdarzenia, np. mouseovermousedown, a nie tylko zdarzenie kliknięcia. Informacje o tych innych zdarzeniach mogą być kluczowe, jeśli były one również powolne, ponieważ wszystkie przyczyniły się do spowolnienia działania.

Na koniec tablica longAnimationFrameEntries. Może to być pojedynczy wpis, ale w niektórych przypadkach interakcja może obejmować kilka klatek. Tutaj mamy najprostszy przypadek z jedną długą klatką animacji.

longAnimationFrameEntries

Rozwinięcie wpisu LoAF:

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

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

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

Znajdziesz tu wiele przydatnych wartości, np. czas poświęcony na stylizację. Więcej informacji o tych właściwościach znajdziesz w artykule o interfejsie API Long Animation Frames. Obecnie interesuje nas przede wszystkim usługa scripts, która zawiera wpisy zawierające szczegóły skryptów odpowiedzialnych za długotrwały element:

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żemy stwierdzić, że czas został spędzony głównie w ramach jednego event-listener wywołanego na BUTTON#confirm.onclick. Możemy nawet zobaczyć URL źródłowy skryptu i pozycję znaku, w której zdefiniowano funkcję.

Na wynos

Co można ustalić na podstawie tych danych dotyczących atrybucji?

  • Interakcja została wywołana przez kliknięcie elementu button#confirm (z usługi attribution.interactionTarget i usługi invoker w rekordzie atrybucji skryptu).
  • Czas został wykorzystany głównie na wykonywanie odbiorników zdarzeń (z attribution.processingDuration w porównaniu do danych value).
  • Kod detektora zdarzeń o powolnym działaniu zaczyna się od detektora kliknięcia zdefiniowanego w pliku third-party/cmp.js (od scripts.sourceURL).

Mamy wystarczającą ilość danych, aby wiedzieć, gdzie należy wprowadzić optymalizację.

6. Wiele detektorów zdarzeń

Odśwież stronę, aby konsola Narzędzi deweloperskich była pusta, a interakcja dotycząca zgody na pliki cookie nie była już najdłuższą interakcją.

Zacznij pisać w polu wyszukiwania. Co zawierają dane atrybucji? Co się dzieje?

Dane atrybucji

Najpierw ogólny przegląd jednego przykładu 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 ograniczeniem procesora) po interakcji z klawiaturą elementu input#search-terms. Większość czasu (1061 ms z łącznego czasu INP wynoszącego 1072 ms) zajęło przetwarzanie.

Bardziej interesujące są jednak wpisy scripts.

Wypełnianie układu

Pierwszy wpis tablicy scripts dostarcza nam cennego kontekstu:

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 przypada na wykonanie tego skryptu, który jest odbiorcą komunikatów input (wywołujący to INPUT#search-terms.oninput). Podana jest nazwa funkcji (handleSearch), a także pozycja znaku w pliku źródłowym index.js.

Jest jednak nowa właściwość: forcedStyleAndLayoutDuration. Czas spędzony w ramach wywołania skryptu, w którym przeglądarka była zmuszona do ponownego ułożenia strony. Inaczej mówiąc, 78% czasu (388 ms z 497 ms) poświęconego na wykonanie tego odbiornika zdarzeń zostało faktycznie wykorzystane na chaotyczne przetwarzanie układu.

To powinno być priorytetem.

Powtarzający się słuchacze

W przypadku tych dwóch wpisów skryptu nie ma nic szczególnie niezwykłego:

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

Oba wpisy to keyup listener, który jest wykonywany jeden po drugim. Listenery to funkcje anonimowe (dlatego nic nie jest zgłaszane w usłudze sourceFunctionName), ale nadal mamy plik źródłowy i pozycję znaku, więc możemy znaleźć kod.

Dziwne jest to, że oba pochodzą z tego samego pliku źródłowego i mają ten sam znak.

Przeglądarka przetwarzała wiele naciśnięć klawiszy w jednym klatce animacji, co spowodowało, że ten odbiornik zdarzeń został uruchomiony 2 razy, zanim cokolwiek zostało narysowane.

Ten efekt może się nasilać, ponieważ im dłużej trwa przetwarzanie przez obiekty odbiorcze zdarzeń, tym więcej dodatkowych zdarzeń wejściowych może się pojawić, co wydłuża czas interakcji.

Ponieważ jest to interakcja wyszukiwania/autouzupełniania, dobrym rozwiązaniem byłoby odfiltrowanie sygnałów wejściowych, aby w każdej klatce przetwarzane było maksymalnie 1 wciśnięcie klawisza.

7. Opóźnienie wejściowe

Typowym powodem opóźnień w działaniu (czas od interakcji użytkownika do momentu, gdy odbiorca zdarzenia może rozpocząć przetwarzanie interakcji) jest zajęty wątek główny. Może to mieć kilka przyczyn:

  • Strona się wczytuje, a wątek główny jest zajęty początkową konfiguracją DOM, układem i stylami strony oraz analizą i uruchamianiem skryptów.
  • strona jest ogólnie zajęta – np. wykonuje obliczenia, animacje oparte na skryptach lub reklamy;
  • Przetwarzanie poprzednich interakcji zajmuje tak dużo czasu, że opóźnia to przyszłe interakcje, co widać w ostatnim przykładzie.

Strona demonstracyjna ma tajną funkcję. Jeśli klikniesz logo ślimaka u góry strony, zacznie się ono animować i wykonywać ciężką pracę w głównym wątku JavaScript.

  • Aby rozpocząć animację, kliknij logo ślimaka.
  • Zadania JavaScript są uruchamiane, gdy ślimak znajduje się na dole strony. Spróbuj wejść w interakcję ze stroną tak blisko dolnego progu odrzucenia, jak to możliwe, i zobacz, jak wysoki INP możesz wywołać.

Na przykład nawet wtedy, gdy nie wywołasz żadnych innych odbiorników zdarzeń (np. przez kliknięcie i skupienie się na polu wyszukiwania w momencie, gdy ślimak się wycofuje), praca głównego wątku spowoduje, że strona nie będzie reagować przez zauważalny czas.

Na wielu stronach intensywna praca w głównym wątku nie będzie tak dobrze zorganizowana, ale to dobry przykład tego, jak można ją zidentyfikować w danych atrybucji INP.

Oto przykład atrybucji, która została zarejestrowana tylko wtedy, gdy użytkownik skupił się na polu wyszukiwania podczas powolnego opuszczania strony:

{
  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 oczekiwaniami, wywołania zdarzeń były wykonywane szybko – czas przetwarzania wynosił 4,9 ms. Większość czasu poświęconego na interakcję o niskiej jakości pochłaniał opóźnienie wprowadzania danych, które zajęło 702,3 ms z łącznego czasu 728 ms.

W takiej sytuacji trudno jest przeprowadzić debugowanie. Wiemy, z czym i jak użytkownik wchodzi w interakcję, ale wiemy też, że ta część interakcji zakończyła się szybko i nie była problemem. Zamiast tego coś innego na stronie opóźniło rozpoczęcie przetwarzania interakcji, ale skąd mieliśmy wiedzieć, od czego zacząć szukać?

Wpisy skryptu LoAF są tu, aby Ci pomóc:

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 ona ramkę animacji, więc jest uwzględniona w danych LoAF połączonych ze zdarzeniem interakcji.

Dzięki temu możemy zobaczyć, jak została wywołana funkcja, która opóźniła przetwarzanie interakcji (przez listenera animationiteration), która dokładnie funkcja była za to odpowiedzialna i gdzie znajdowała się w plikach źródłowych.

8. Opóźnienie wyświetlania: gdy aktualizacja nie zostanie namalowana

Opóźnienie wyświetlania to czas od zakończenia działania odbiorników zdarzeń do momentu, w którym przeglądarka jest w stanie narysować nową klatkę na ekranie, aby wyświetlić użytkownikowi widoczne informacje zwrotne.

Odśwież stronę, aby ponownie zresetować wartość INP, a potem otwórz menu hamburgera. Na pewno jest jakiś problem z otwieraniem.

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 wyświetlania odpowiada za większość opóźnień interakcji. Oznacza to, że wszystko, co blokuje wątek główny, występuje po zakończeniu działania odbiornikó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,
}]

Z jednego wpisu w tablicy scripts wynika, że czas spędzony w user-callback pochodzi z FrameRequestCallback. Tym razem opóźnienie prezentacji spowodowane jest wywołaniem requestAnimationFrame.

9. Podsumowanie

Aggregating field data

Warto pamiętać, że jest to łatwiejsze, gdy analizujesz pojedynczy wpis atrybucji INP z jednego wczytania strony. Jak można zagregować te dane, aby debugować INP na podstawie danych polowych? Ilość przydatnych informacji utrudnia to zadanie.

Bardzo przydatne jest np. poznanie elementu strony, który jest częstym źródłem powolnego działania. Jeśli jednak na stronie są zaimplementowane nazwy klas CSS, które różnią się w zależności od wersji, web-vitalsselektory tego samego elementu mogą się różnić w zależności od wersji.

Zamiast tego musisz zastanowić się nad swoją konkretną aplikacją, aby określić, co jest najbardziej przydatne i jak można agregować dane. Na przykład przed przekazaniem danych atrybucji beacona możesz zastąpić selektor web-vitals własnym identyfikatorem na podstawie komponentu, w którym znajduje się element docelowy, lub ról ARIA, które spełnia ten element.

Podobnie wpisy scripts mogą zawierać ścieżki sourceURL z haszami plików, co utrudnia ich łączenie. Możesz jednak usunąć hasze na podstawie znanego procesu kompilacji przed wysłaniem danych z powrotem.

Niestety nie ma łatwej ścieżki do rozwiązania tego problemu w przypadku tak złożonych danych, ale nawet użycie ich podzbioru jest bardziej wartościowe niż brak jakichkolwiek danych atrybucji w procesie debugowania.

Informacje o wykonawcach i utworach wszędzie

Atrybucja INP na podstawie LoAF to przydatne narzędzie do debugowania. Zawiera szczegółowe dane o tym, co dokładnie działo się podczas INP. W wielu przypadkach może on wskazać dokładną lokalizację w skrypcie, od której warto zacząć optymalizację.

Możesz już używać danych atrybucji INP w dowolnej witrynie.

Nawet jeśli nie masz uprawnień do edycji strony, możesz odtworzyć proces z tego Codelab, uruchamiając ten fragment kodu w konsoli Narzędzi deweloperskich, aby sprawdzić, co możesz znaleźć:

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