Klikasz w menu hamburgerowe, a ono rozwija się dopiero po sekundzie? Wpisujesz tekst w pole wyszukiwarki, a litery pojawiają się na ekranie z irytującym opóźnieniem? To nie wina wolnego internetu – to zablokowany wątek główny przeglądarki. Od kiedy wskaźnik Interaction to Next Paint (INP) oficjalnie zastąpił FID jako kluczowy element oceny stron, Google bezlitośnie obniża pozycję witryn, które wolno reagują na akcje użytkownika.
W tym artykule nie będę zanudzał Cię teorią. Pokażę Ci, jak na przykładzie jednego z moich projektów przeprowadziłem pełną refaktoryzację kodu JavaScript, rozbiłem długie zadania (Long Tasks) i zjechałem ze wskaźnikiem INP z alarmujących 420 ms do bezpiecznych i zielonych 75 ms.
Czym dokładnie jest INP i dlaczego Twój stary kod go zabija?
Zanim przejdziemy do modyfikacji skryptów, musimy zrozumieć, co tak naprawdę mierzy ten wskaźnik. Jeśli czytałeś mój poradnik o Core Web Vitals, wiesz, że Google kładzie ogromny nacisk na realne doświadczenia użytkownika (RUM). INP mierzy czas, jaki upływa od momentu interakcji (kliknięcia, dotknięcia ekranu czy naciśnięcia klawisza) do momentu, w którym przeglądarka faktycznie wyrenderuje na ekranie kolejną klatkę obrazu potwierdzającą tę akcję.
Najczęstszym zabójcą INP jest monolityczny, nieoptymalny JavaScript. Przeglądarka dysponuje tylko jednym głównym wątkiem (Main Thread), który musi obsłużyć zarówno parsowanie stylów, kalkulację układu, jak i wykonywanie Twoich skryptów. Jeśli wrzucisz tam ciężką pętlę filtrującą produkty lub skomplikowaną operację matematyczną, wątek „zamarznie”. Użytkownik będzie klikał w ekran, a strona nie odpowie, dopóki skrypt nie skończy swojego działania.
Jak namierzyłem winowajców? Profilowanie w Chrome DevTools
Optymalizacja INP „na ślepo” rzadko przynosi rezultaty. Aby precyzyjnie zlokalizować, które funkcje JavaScript blokują interfejs, wykorzystałem sprawdzoną procedurę diagnostyczną:
- Karta Performance w DevTools: To absolutny fundament. Uruchomiłem nagrywanie profilu wydajności, a następnie kilkukrotnie przeklikałem najbardziej wymagające elementy strony (filtry, koszyk, rozwijane sekcje). Czerwone flagi na wykresie od razu wskazały tzw. Long Tasks – zadania, których wykonanie zajmowało procesorowi więcej niż 50 ms.
- Web Vitals Extension: Proste rozszerzenie do przeglądarki, które w konsoli na żywo wypluwało mi informację o każdej interakcji, precyzyjnie rozbijając wynik INP na trzy składowe: Input Delay (opóźnienie wejścia), Processing Time (czas przetwarzania skryptu) oraz Presentation Delay (opóźnienie renderowania).
- Monitoring w terenie: Wykorzystując zewnętrzne narzędzia SEO oraz raport Chrome User Experience Report (CrUX), sprawdziłem, jak strona zachowuje się u użytkowników ze słabszymi urządzeniami mobilnymi. To tam problem z długim czasem przetwarzania JS był najbardziej widoczny.
Strategie refaktoryzacji: Jak uwolniłem wątek główny?
Po zebraniu danych okazało się, że głównym problemem była funkcja dynamicznego filtrowania i sortowania tabeli z produktami na liście. Każde kliknięcie checkboxa odpalało gigantyczną operację na tablicach. Oto trzy kroki, które całkowicie uzdrowiły kod:
1. Rozbijanie długich zadań za pomocą yieldowania (scheduler.yield)
Zamiast pozwalać, aby jedna funkcja wykonywała się bez przerwy przez 150 ms, zastosowałem technikę zwaną „yieldowaniem”. Polega ona na celowym przerywaniu pracy skryptu w kluczowych momentach i oddawaniu kontroli przeglądarce, aby ta mogła w międzyczasie obsłużyć kliknięcie użytkownika i zaktualizować interfejs.
Do niedawna używało się do tego sztuczek z setTimeout(..., 0), jednak nowoczesny JavaScript dostarcza nam dedykowane, natywne narzędzie: scheduler.yield() (lub fallback do requestIdleCallback / setTimeout dla starszych przeglądarek).
Tak wygląda schemat asynchronicznego dzielenia pracy w pętli:
async function processLargeArray(items) {
for (let i = 0; i < items.length; i++) {
// Ciężkie operacje na pojedynczym elemencie
doHeavyCalculation(items[i]);
// Co 50 iteracji oddaj fabrykę w ręce przeglądarki
if (i % 50 === 0 && typeof scheduler !== 'undefined' && scheduler.yield) {
await scheduler.yield();
}
}
}
2. Przeniesienie ciężkich kalkulacji do Web Workerów
Skoro wątek główny powinien zajmować się wyłącznie obsługą interfejsu i renderowaniem (o czym pisałem szerzej analizując działanie strony internetowej w przeglądarce), to dlaczego mielibyśmy zmuszać go do sortowania wielotysięcznych tablic JSON? Data crunching to idealne zadanie dla **Web Workerów**.
Wydzieliłem całą logikę odpowiedzialną za filtrowanie i wyszukiwanie tekstowe do osobnego pliku workera. Teraz wątek główny jedynie wysyła paczkę danych przez worker.postMessage(), a sam pozostaje całkowicie wolny i natychmiast reaguje na kliknięcia. Gdy worker kończy obliczenia w tle, zwraca wynik, a UI aktualizuje się bez ani jednego szarpnięcia.
3. Optymalizacja Event Listenerów (Debouncing i Throttling)
Kolejnym problemem było podpięcie ciężkich funkcji pod zdarzenia typu input lub scroll. Przeglądarka próbowała odpalać skrypt przy każdym, nawet najmniejszym ruchu myszką lub wpisaniu pojedynczej litery. Wdrożyłem mechanizm **Debouncingu** dla wyszukiwarki (funkcja uruchamia się dopiero, gdy użytkownik przestanie pisać na 300 ms) oraz **Throttlingu** dla zdarzeń przewijania strony.
Biznesowe i techniczne efekty zmian
Po wdrożeniu poprawek do repozytorium i uruchomieniu produkcyjnego pipeline'u, ponowne testy laboratoryjne oraz zbieranie danych RUM potwierdziły gigantyczny skok jakościowy:
- Wskaźnik INP: Spadek z 420 ms (status: POOR) do powtarzalnych 75 ms (status: GOOD).
- Total Blocking Time (TBT): Redukcja o ponad 70%, co przełożyło się również na lepszy start witryny.
- Konwersja: Zmniejszenie frustracji użytkowników na urządzeniach mobilnych zaowocowało realnym, kilkuprocentowym wzrostem finalizacji zamówień w koszyku.
Twoja Checklista Optymalizacji JavaScript pod kątem INP
Chcesz okiełznać czas reakcji w swoim projekcie? Przejdź przez poniższe kroki refaktoryzacji skryptów:
Krok 1: Audyt i lokalizacja anomalii
- Otwórz Chrome DevTools, wejdź w zakładkę Performance i zaznacz opcję "Web Vitals". Przetestuj interakcje i znajdź zadania przekraczające 50 ms.
- Sprawdź, czy nie duplikujesz obsługi zdarzeń (np. wielokrotnie podpięty ten sam event listener pod przycisk).
Krok 2: Architektura i wykonanie kodu
- Zastosuj Debounce/Throttle wszędzie tam, gdzie zdarzenia odpalają się seryjnie (input, resize, scroll).
- Zidentyfikuj zadania przetwarzające duże zestawy danych i rozbij je za pomocą
scheduler.yield()lub przenieś do osobnego Web Workera. - Upewnij się, że operacje modyfikujące drzewo DOM (zapis) są odseparowane od operacji czytających geometrię okna (odczyt), aby uniknąć wymuszonego reflow/layout thrashingu.
Krok 3: Weryfikacja efektów
- Uruchom testy wydajnościowe na spowolnionym procesorze (CPU 4x/6x slowdown) w DevTools, aby zasymulować starszy smartfon.
- Monitoruj zakładkę Core Web Vitals w Google Search Console po wdrożeniu nowej wersji kodu na produkcję.
JavaScript daje nam niesamowite możliwości budowania interaktywnych aplikacji, ale niesie też ogromną odpowiedzialność za płynność ich działania. Refaktoryzacja kodu pod kątem INP to nie tylko ukłon w stronę robotów Google i walka o lepsze pozycje. To przede wszystkim szacunek do czasu i nerwów Twojego użytkownika. Responsywny kod to fundament nowoczesnego front-endu.