Przejdź do treści
Wydajność 7 min czytania

INP w sklepie B2B - jak optymalizować Interaction to Next Paint

INP zastąpił FID jako oficjalna metryka Core Web Vitals dwa lata temu. Wciąż widzę sklepy, które tego nie zauważyły - i dziwią się, że "wszystko było zielone, a teraz nie". FID był pobłażliwy. INP ocenia każdy klik i każde wpisanie do końca renderu. Magento z filtrami fasetowymi w PLP, konfiguratorem na karcie produktu i checkoutem z walidacją NIP-u wpada w 400-800 ms bez wysiłku. To strefa "Poor" w GSC. Da się zejść do okolic 200 ms bez wymiany stacku, jeśli wiesz gdzie szukać.

Jakub Owsianka Autor
Zaktualizowano:
Okladka artykulu: INP w sklepie B2B - jak optymalizować Interaction to Next Paint
Okladka artykulu: INP w sklepie B2B - jak optymalizować Interaction to Next Paint
Spis treści (6)

INP vs FID - co się zmieniło

FID mierzył jedną rzecz: ile trwa od pierwszego kliknięcia do tego, aż przeglądarka zacznie obsługiwać event. Tylko pierwsze kliknięcie, tylko do startu reakcji JS, bez renderu. Praktycznie u każdego sklepu wychodziło "Good" i ludzie spali spokojnie.

INP poszerzył pomiar w dwa wymiary. Po pierwsze - patrzy na każdą interakcję w sesji, nie tylko pierwszą. Po drugie - liczy do momentu, w którym przeglądarka faktycznie wyrenderuje zmianę. Czyli to, co czuje użytkownik. Google bierze 98. percentyl ze wszystkich interakcji na stronie i to jest wynik.

W sklepie B2B działa to tak: klient klika filtr "Producent: Bosch" w PLP. INP mierzy czas od kliknięcia do momentu, w którym lista produktów faktycznie się przemaluje. Otwiera modal konfiguratora? INP mierzy do pojawienia się treści w modalu, nie do display: block. Dodaje pozycję z koszyka? Liczy do wizualnej aktualizacji licznika i kwoty.

Progi Google: poniżej 200 ms to "Good", 200-500 ms "Needs improvement", powyżej 500 ms "Poor". To pojedynczy próg dla całej strony - nawet jedna interakcja na granicy psuje wynik, bo jest w 98. percentylu.

Niemiła konsekwencja: sklepy z "wszystko zielone na FID" mają teraz czerwone INP. Bo problem nigdy nie był w pierwszym kliknięciu, tylko w trzecim, dziesiątym, w kliknięciu w bogato udekorowany filtr po długiej sesji.

Core Web Vitals dla sklepu B2B - jak INP wpisuje się w cały zestaw metryk.

Diagnostyka INP

Pomiar dzielę na dwie warstwy. Lab to to, co widzisz przed deployem, na własnej maszynie. Field to to, co widzą realni klienci - i to się liczy w GSC.

W labie najszybsza droga to Chrome DevTools, zakładka Performance, włączony "Web Vitals" overlay (Settings → Experiments). Nagrywasz sesję klikając wszystko, co klient klika - filtry, konfigurator, koszyk - i widzisz wartość INP per interakcja. To pokaże najgorszy wynik z nagrania. Lighthouse od wersji 11 też raportuje INP, ale tylko symulacyjnie i dla pierwszych kilku interakcji - dla diagnostyki sklepu B2B to za mało.

Field bez ankietowania robi się przez bibliotekę web-vitals. Wpinasz w layout i wysyłasz do swojego endpointa lub GA4:

<script type="module">
  import {onINP} from 'https://cdn.skypack.dev/web-vitals@4';
  onINP(({value, attribution}) => {
    sendToAnalytics('inp', value, attribution.eventTarget);
  });
</script>

Klucz to attribution. Bez niego dostaniesz suchą liczbę i nie wiesz, czy 600 ms wzięło się z kliknięcia "Dodaj do koszyka", czy z otwarcia menu kategorii. Z attribution wiesz, który selektor DOM dostał event - i to jest pierwszy adres do śledztwa.

Trzecie źródło to Google Search Console. Realne dane z Chrome User Experience Report, ale z opóźnieniem 28 dni. Dobre do raportowania trendu, słabe do reagowania na regresję. Po wdrożeniu nigdy nie czekaj na GSC - jeśli masz własne RUM, regresję zobaczysz w ten sam dzień.

Long tasks i ich identyfikacja

W 90% przypadków INP rośnie z jednego powodu: blok JS dłuższy niż 50 ms blokuje main thread w momencie, gdy klient klika. Reszta to długi render - ale to wynika z tego samego: drogi handler.

W DevTools robię to tak: Performance → Record → klikam interakcję, którą podejrzewam → Stop. Główny pas "Main" pokazuje żółto-czerwone bloki podpisane "Task" lub "Long Task". Otwieram - mam dokładny stack. Zwykle widać, że 70% czasu zjadł jeden konkretny callback.

Pięć rzeczy, które najczęściej widzę w sklepach B2B:

Pierwsze - re-render całej listy produktów po zmianie jednego filtra. 200 kart × ~1 ms na kartę = 200 ms blokady. Komponentowy framework (React, Vue) bez memo / key reraz wszystko od zera.

Drugie - GTM. Klient klika "Dodaj do koszyka", odpala się Pixel, Hotjar, Google Ads conversion, Klaviyo. Każdy z tych skryptów dorzuca 20-40 ms synchroniczne. Cztery skrypty = 120 ms zanim koszyk się odmaluje.

Trzecie - walidacja NIP-u na blur z synchronicznym wywołaniem API GUS. To akurat klasyk z polskich sklepów. Klient wpisuje NIP, klika dalej, czeka 600 ms na response z API. INP: 600 ms.

Czwarte - kalkulacja cennika kontraktowego po stronie frontu. Każde dodanie produktu do koszyka uruchamia funkcję, która iteruje po wszystkich pozycjach i nalicza rabaty per próg ilościowy. Dla koszyka z 50 SKU - kilkaset ms.

Piąte - lazy-loadowane biblioteki ładowane synchronicznie na pierwsze użycie. Klient klika "Konfigurator", JS ściąga 150 kB minified, parsuje, wykonuje. Pierwsze otwarcie zawsze gorsze niż kolejne.

Web workers i offloading

Worker to jedyne narzędzie, które naprawdę zdejmuje pracę z main thread. Wszystko inne (debounce, requestIdleCallback) tylko przekłada problem w czasie - worker go przenosi gdzie indziej.

Reguła: jeśli operacja jest czystą funkcją (input → output, bez DOM, bez window, bez document), nadaje się na worker. W sklepie B2B kandydatów jest sporo: walidacja CSV w quick orderze, parsowanie pliku Excel z zamówieniem, rekalkulacja cen kontraktowych dla wielopozycyjnego koszyka, filtrowanie i sortowanie po stronie klienta na PLP z dużym zbiorem.

Klasyczny przykład - walidacja CSV w quick orderze. Klient wkleja 2000 wierszy SKU + ilość. Bez workera:

function validateQuickOrderCSV(csvText) {
  const lines = csvText.split('\n');
  return lines.map(line => {
    const [sku, qty] = line.split(',');
    return validateSku(sku);
  });
}

Main thread leży na 3-5 sekund, INP w tym czasie idzie w kosmos. Z workerem:

// validation.worker.js
self.onmessage = (e) => {
  const result = validateBatch(e.data);
  self.postMessage(result);
};

// main thread
const worker = new Worker('/js/validation.worker.js');
worker.postMessage(csvText);
worker.onmessage = (e) => displayResults(e.data);

Główny wątek odpowiada na inne kliknięcia, klient widzi pasek postępu, INP nie drgnie nawet dla 10 000 linii.

Limit workera: brak dostępu do DOM. Wymiana danych przez postMessage, struktury serializowalne. Dla dużych payloadów warto użyć Transferable Objects - zerowy koszt kopiowania.

Optymalizacja PLP z filtrami

PLP z 50+ filtrami fasetowymi to scenariusz, w którym INP najszybciej się sypie. Klient klika checkbox "Bosch", chce zobaczyć efekt teraz, a sklep blokuje się na 400 ms.

Pierwsza warstwa: debounce z anulowaniem. Klient klika trzy filtry pod rząd, nie ma sensu uderzać do API trzy razy:

let abortController;
const onFilterChange = debounce(async (filters) => {
  abortController?.abort();
  abortController = new AbortController();
  const products = await fetch('/api/products?' + filters, {
    signal: abortController.signal
  }).then(r => r.json());
  renderProducts(products);
}, 200);

Druga - optymistyczna aktualizacja UI. Klikasz filtr, checkbox zaznacza się natychmiast, lista produktów dostaje skeleton. Klient widzi reakcję w 16 ms, nie w 400 ms. To psychologia, ale Google ją mierzy.

Trzecia - virtual scrolling dla PLP z dużym wynikiem. Jeśli filtr "akcesoria" zwraca 800 produktów, nie ma powodu renderować 800 kart w DOM. react-window, vue-virtual-scroller, na waniliowym JS lekka biblioteka jak clusterize.js. Renderujesz tylko widoczne plus bufor 10 wierszy.

Czwarta - sprzątanie listenerów scroll i resize. Sticky bar w PLP, infinite scroll, popover na hover na karcie - każdy z tych komponentów ma listener, który strzela kilkadziesiąt razy na sekundę. Bez passive: true na scrollu i bez throttle - main thread haruje przy każdym ruchu kółka myszy.

Piąta - rozdziel zmianę stanu od aktualizacji DOM. Klik na filtr zmienia stan, ale faktyczny re-render odpalasz przez requestIdleCallback (z fallbackiem na setTimeout(..., 0)). Klient widzi natychmiastowy efekt na checkboxie, ciężka praca dzieje się w wolnej chwili.

Optymalizacja PLP - całościowe podejście do listingu, nie tylko INP.

Targety INP dla typów stron

Z mojego ostatniego audytu trzech sklepów na Magento 2.4 (każdy 30-80k SKU, każdy z konfiguratorem na PDP) wyszły dość spójne wartości startowe. Trzymam je jako benchmark, kiedy ktoś pyta "czy mój INP jest zły":

Typ strony Target Typowy start na Magento 2.4
Strona główna poniżej 200 ms 250-400 ms
PLP z filtrami poniżej 200 ms 400-800 ms
PDP zwykła poniżej 200 ms 200-350 ms
PDP z konfiguratorem poniżej 250 ms 500-1200 ms
Koszyk poniżej 200 ms 300-600 ms
Checkout poniżej 200 ms 250-500 ms
Quick order z CSV poniżej 250 ms 500-1500 ms

Dwa najgorsze przypadki to zawsze PLP z filtrami i PDP z konfiguratorem. Quick order CSV w main thread przebija sekundę regularnie, ale to jedyny przypadek, w którym worker rozwiązuje problem w godzinę pracy seniora. Reszta wymaga audytu handlerów.

Jeśli chcesz mieć wszystko zielone w GSC, monitoruj INP per typ strony, nie tylko zbiorczo. Średnia 220 ms wygląda OK, ale jeśli pochodzi z 150 ms dla strony głównej i 700 ms dla checkoutu - klient odejdzie na checkoucie, a ranking spadnie tam, gdzie zarabiasz.

Wydajność checkoutu B2B - osobne podejście do najwrażliwszego etapu.

FAQ

Jaki INP jest akceptowalny dla sklepu B2B? Oficjalnie Google daje "Good" poniżej 200 ms. W praktyce sklep B2B z konfiguratorem, dużym koszykiem i cennikiem kontraktowym osiąga 250-300 ms i to jest realny cel po pierwszej rundzie optymalizacji. Powyżej 500 ms to "Poor" w GSC - i to już realnie wpływa na pozycję.

Czy INP naprawdę wpływa na ranking? Tak, od marca 2024. Pojedyncza metryka nie waży dużo, ale strony, które mają wszystkie trzy CWV (LCP, INP, CLS) w czerwonym, tracą widocznie wobec konkurencji, która ma zielone. Im węższa nisza, tym mocniej to widać - bo Google wybiera między kilkoma stronami, nie między tysiącami.

Jak mierzyć INP w realnym ruchu? Najtaniej - biblioteka web-vitals od Google plus własny endpoint zbierający. Drożej i wygodniej - komercyjne RUM (SpeedCurve, Sentry Performance, Cronitor). GSC pokazuje INP, ale z opóźnieniem 28 dni i tylko zbiorczo. Do reagowania na regresję po deployu - własne RUM albo nic.

Czy INP można poprawić bez wymiany frameworka frontowego? Zwykle tak. Większość problemów INP, które widzę, siedzi w handlerach event-ów, blokujących skryptach analitycznych i synchronicznych walidacjach formularzy. Te rzeczy są tak samo do naprawienia w React, Vue i waniliowym JS. Wymiana frameworka pomaga dopiero, gdy reszta jest już wyczyszczona.

Czy AMP rozwiązuje problem INP? AMP wymusza dobre praktyki, więc INP wychodzi naturalnie niski. Ale dla sklepu B2B z koszykiem, logowaniem, konfiguratorem i cennikiem AMP jest za ciasny. Dla strony głównej, blogu i kategorii - można rozważyć. Dla PDP i checkoutu - praktycznie nie da się. Lepiej zoptymalizować klasyczną stronę niż ciąć funkcje.

Co dalej

O autorze

Jakub Owsianka

Architekt rozwiązania w WiseB2B - silniku platform B2B. Zaczynał po stronie biznesu (własne sklepy), potem deweloper, dziś projektuje wdrożenia dla sklepów z katalogami w dziesiątkach tysięcy SKU. W ostatnich latach wdrożył AI-development w zespole i funkcjonalności oparte o AI bezpośrednio w silniku sklepu.

Masz pytanie do tego artykułu?

Dodatkowy kontekst, problem z własnym wdrożeniem, druga opinia - napisz wprost. Odpowiadam osobiście w 1-2 dni robocze.