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ć.
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
- Wszystkie metryki CWV razem: Core Web Vitals dla sklepu B2B
- Lista produktów na większą skalę: Optymalizacja PLP
- Najwrażliwszy etap zakupu: Wydajność checkoutu B2B
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.
Czytaj dalej w temacie wydajności
Wszystkie wpisyMasz 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.