Przejdź do treści
Warsztat

requestIdleCallback nie znaczy „później”. Kosztowało mnie 200 ms TBT

Przeniosłem kilka zapytań w tle do requestIdleCallback z limitem 3000 ms, żeby odciążyć start strony. Efekt: TBT wzrosło czterokrotnie, bo wszystkie callbacki zebrały się w jedną serię dokładnie tam, gdzie mierzy Lighthouse.

6 min czytania
requestIdleCallback nie znaczy „później”. Kosztowało mnie 200 ms TBT

👁 116 przeczytań

// w skrócie
  • Użycie requestIdleCallback z parametrem timeout: 3000 ms spowodowało wzrost TBT ze sklepu WordPress z 64 ms do 265 ms, czyli czterokrotny regres.
  • Timeout w requestIdleCallback nie działa jako bufor bezpieczeństwa, lecz jako synchronizator - kilka callbacków z tym samym limitem odpala się jedną serią, sklejając osobne zadania w jeden blok.
  • Zapytania bez odroczenia startowały rozłożone w przedziale 183-1329 ms, co dało TBT 64 ms - lepszy wynik niż jakakolwiek wersja z odroczeniem przez requestIdleCallback.

Przeniosłem kilka niekrytycznych zapytań w tle do requestIdleCallback z parametrem timeout: 3000. TBT sklepu wzrosło z 64 do 265 ms, a start zapytań przesunął się z przedziału 183-1329 ms na 383-2411 ms. Zmianę wycofałem tego samego dnia.

Wniosek w jednym akapicie

Parametr timeout w requestIdleCallback nie jest zabezpieczeniem „na wypadek gdyby przeglądarka zapomniała” - jest terminem, po którym przeglądarka wykonuje callback niezależnie od tego, czy wątek główny jest wolny. Kiedy kilka callbacków dostaje ten sam limit i jest rejestrowanych w podobnym momencie, ich deadline’y wypadają blisko siebie i cała odłożona praca ląduje w jednej serii. W moim przypadku ta seria trafiła dokładnie w okno, w którym liczony jest Total Blocking Time - stąd wzrost z 64 do 265 ms. Odroczenie zamieniło rozrzucone, tanie zadania w jeden korek. Moja opinia: timeout powinien być domyślnie pomijany, a używany wyłącznie wtedy, gdy niewykonanie zadania łamie funkcjonalność.

Co dokładnie odroczyłem

Sklep na WordPressie, strona kategorii. Na starcie odpalało się kilka zapytań pobocznych - takich, których wynik nie jest potrzebny do pierwszego renderu ani do pierwszej interakcji. Klasyczny kandydat do przeniesienia na później.

Zamiast zostawić je tam, gdzie były, owinąłem każde w requestIdleCallback z limitem 3000 ms. Logika wydawała się szczelna: przeglądarka wykona je, gdy będzie miała chwilę, a jeśli przez trzy sekundy nie znajdzie okna bezczynności, i tak je odpali - więc nic się nie zgubi.

Problem polega na tym, że drugi warunek okazał się tym decydującym. Przeglądarka nie znalazła sensownego okna bezczynności w trakcie ładowania strony sklepu (co przy takiej liczbie skryptów firm trzecich nie jest zaskoczeniem), więc każdy callback czekał na swój deadline. A deadline’y były podobne, bo rejestracja nastąpiła w krótkim odstępie.

Liczby przed i po - optymalizacja JavaScript, która pogorszyła wynik

MetrykaPrzed (bez odroczenia)Po (requestIdleCallback, timeout 3000 ms)
TBT64 ms265 ms
Start pierwszego zapytania183 ms383 ms
Start ostatniego zapytania1329 ms2411 ms
Rozrzut startów183-1329 ms383-2411 ms
Status zmiany-wycofana

Najciekawszy wiersz to rozrzut. Przed zmianą zapytania startowały naturalnie, rozłożone na przestrzeni ponad sekundy - każde w swoim momencie, w luce między innymi zadaniami. Po zmianie przesunęły się dalej w czasie, ale przestały być rozproszone: odpaliły jedną serią pod koniec okna, w którym Lighthouse sumuje blokowanie wątku głównego.

Czterokrotny wzrost TBT nie wziął się z tego, że kod zaczął robić więcej pracy. Pracy było tyle samo. Zmieniło się tylko jej rozłożenie - z rozrzuconej na skupioną. TBT karze właśnie skupienie, bo liczy nadwyżkę ponad 50 ms w obrębie jednego długiego zadania. Pięć zadań po 40 ms to zero wkładu do TBT. To samo pięć zadań zlepione w jedno 200-milisekundowe to 150 ms kary.

Dlaczego timeout zbiera pracę w kupę

Mechanika jest prosta, gdy się ją raz zobaczy. requestIdleCallback ma dwa tryby wykonania:

  • Tryb bezczynności - przeglądarka znalazła lukę w wątku głównym i woła callback z sensownym timeRemaining(). To ten tryb, o którym wszyscy myślą.
  • Tryb wymuszony - minął timeout, callback jest wołany niezależnie od stanu wątku, a didTimeout jest ustawione na true. timeRemaining() zwraca wtedy zero.

Jeśli strona jest zajęta przez cały okres ładowania - a strona sklepu z wtyczkami i skryptami zewnętrznych dostawców zwykle jest - to tryb pierwszy nie zachodzi ani razu. Wszystko wykonuje się w trybie wymuszonym. I tu pojawia się efekt kumulacji: skoro każdy callback ma ten sam limit, a rejestrowane są w krótkim odstępie, to ich terminy wypadają niemal równocześnie. Przeglądarka odpala je jeden po drugim w tym samym tasku albo w serii tasków bez oddechu między nimi.

Innymi słowy: im bardziej zajęta strona, tym bardziej timeout działa jako synchronizator, a nie jako bufor bezpieczeństwa.

Kiedy requestIdleCallback ma sens, a kiedy nie

Poniżej moja ocena na podstawie tego jednego pomiaru - traktujcie to jako opinię, nie jako regułę potwierdzoną na wielu wdrożeniach.

SytuacjaCzy wartoUwaga
Zadanie, które może się nigdy nie wykonać bez straty (prefetch, telemetria)Tak, bez timeoutBrak limitu = brak kumulacji
Kilka zadań rejestrowanych razem, każde z tym samym timeoutNieTo mój przypadek - 64 ms zamieniło się w 265 ms
Zadanie krytyczne, które musi się wykonaćNie przez idle callbackJeśli musi się wykonać, to nie jest zadanie bezczynnościowe
Ciężkie obliczenie, które trzeba pokroićTak, ale z kontrolą budżetuSprawdzaj timeRemaining() i didTimeout w każdej iteracji

Dla mnie najważniejsza lekcja brzmi tak: jeśli piszesz timeout, to znaczy, że zadanie musi się wykonać. A jeśli musi się wykonać, to requestIdleCallback jest złym narzędziem - lepszym jest zwykłe rozłożenie pracy w czasie z jawnym odstępem, tak żeby zadania nie sklejały się w jeden blok.

Co zrobiłem zamiast tego

Nie przepłacaj za te subskrypcje

Prowadzę sklep z rocznymi dostępami do narzędzi AI - te same konta, o których piszę wyżej, tylko taniej niż w cenniku producenta.

Zobacz, co jest dostępne

Wycofałem zmianę i wróciłem do stanu wyjściowego z TBT 64 ms. Zapytania odpalają się w przedziale 183-1329 ms, rozłożone naturalnie, i to jest wynik lepszy niż cokolwiek, co osiągnąłem przez odraczanie ich hurtem.

To kontrintuicyjny wniosek dla kogoś, kto szuka poprawy TBT na WordPressie: nie każde przeniesienie kodu „na później” pomaga. Liczy się nie to, kiedy kod się wykonuje, ale czy jest rozproszony. Rozproszona praca na wczesnym etapie bije skupioną pracę na późniejszym.

Jak to sprawdzić u siebie

  1. Otwórz DevTools, zakładka Performance, i nagraj ładowanie strony z opcją twardego odświeżenia oraz z dławieniem CPU - bez dławienia nie zobaczysz efektu kumulacji, bo szybka maszyna znajdzie okna bezczynności.
  2. W nagraniu znajdź wszystkie wywołania swoich odroczonych callbacków i zanotuj czas startu każdego z nich. Interesuje cię rozrzut, nie średnia. U mnie przed zmianą było to 183-1329 ms, po zmianie 383-2411 ms.
  3. W samym callbacku zaloguj wartość deadline.didTimeout. Jeśli w każdym wywołaniu widzisz true, to twoje zadania w ogóle nie korzystają z bezczynności - wykonują się wyłącznie przez limit czasu i sklejają się w serię.
  4. Zmierz TBT przed i po zmianie na tej samej wersji strony, w tych samych warunkach sieciowych, minimum kilka przebiegów. Różnica między 64 a 265 ms jest tak duża, że wyjdzie od razu, ale mniejsze regresje giną w szumie.
  5. Jeśli TBT wzrosło, usuń parametr timeout i zmierz ponownie. To najtańszy test hipotezy o kumulacji - jeśli bez limitu TBT wraca do normy, masz potwierdzenie.
  6. Jeśli mimo to zadania muszą się wykonać w określonym czasie, porzuć requestIdleCallback i rozłóż je jawnie, dodając odstęp między kolejnymi - chodzi o to, żeby żadne pojedyncze zadanie nie przekroczyło 50 ms i żeby między nimi wątek główny dostał oddech.
Google · Twoje źródłaPromptowy wyżej w Twoim Google - jednym kliknięciemDodaj do preferowanych źródeł →

Skąd te liczby

Pomiar własny na stronie kategorii sklepu opartego na WordPressie, wykonany 25.09.2026. Porównanie dwóch wersji tego samego szablonu: bez odroczenia oraz z kilkoma zapytaniami w tle owiniętymi w requestIdleCallback z parametrem timeout: 3000. Mierzone wartości: TBT (64 ms przed, 265 ms po) oraz czasy startu odroczonych zapytań (przedział 183-1329 ms przed, 383-2411 ms po). Zmiana z limitem czasu została wycofana. Wszystkie wnioski dotyczące tego, kiedy requestIdleCallback warto stosować, są moją opinią wyprowadzoną z tego jednego przypadku, a nie wynikiem serii testów na wielu witrynach.

// Newsletter

Cały tydzień w AI, w jednym mailu

Wybrane premiery, narzędzia i analizy. Raz w tygodniu, prosto do skrzynki.

Zapisz się za darmo →
Za darmo. Wypisujesz się jednym kliknięciem.

Najczęstsze pytania

Dlaczego requestIdleCallback z timeout zwiększył TBT zamiast go zmniejszyć?

Przeglądarka nie znalazła okien bezczynności podczas ładowania strony sklepu i wykonała wszystkie callbacki w trybie wymuszonym, po upływie limitu. Ponieważ każdy callback miał timeout 3000 ms i był rejestrowany w krótkim odstępie, ich deadline'y wypadły niemal równocześnie, sklejając osobne zadania w jedną serię - a TBT karze właśnie skupienie pracy, licząc nadwyżkę ponad 50 ms w obrębie jednego długiego zadania.

Co to jest tryb wymuszony w requestIdleCallback i jak go wykryć?

Tryb wymuszony zachodzi, gdy minie timeout - callback jest wtedy wołany niezależnie od stanu wątku głównego, a didTimeout ma wartość true i timeRemaining() zwraca zero. Żeby to sprawdzić, należy zalogować wartość deadline.didTimeout wewnątrz callbacka - jeśli zawsze wynosi true, zadania w ogóle nie korzystają z bezczynności przeglądarki.

Kiedy requestIdleCallback warto stosować, a kiedy nie?

Według autora warto go stosować bez parametru timeout dla zadań, które mogą się nigdy nie wykonać bez żadnej straty, takich jak prefetch czy telemetria. Nie należy go używać, gdy kilka zadań jest rejestrowanych razem z tym samym timeoutem, ani dla zadań krytycznych, które muszą się wykonać - te lepiej rozłożyć jawnie z odstępami między kolejnymi wywołaniami.

Jak sprawdzić w DevTools czy requestIdleCallback powoduje kumulację zadań?

Należy nagrać ładowanie strony w zakładce Performance z twardym odświeżeniem i dławieniem CPU - bez dławienia szybka maszyna znajdzie okna bezczynności i efekt nie będzie widoczny. Następnie trzeba znaleźć wywołania odroczonych callbacków, zapisać czasy ich startu i sprawdzić rozrzut - u autora przed zmianą wynosił 183-1329 ms, po zmianie 383-2411 ms, co bezpośrednio przełożyło się na wzrost TBT.

// czytaj też

Podobne tematy na Promptowym

Piotr Olszewski

Piotr Olszewski

ADMINISTRATOR

Piotr Olszewski - twórca i autor Promptowego, polskiego serwisu o sztucznej inteligencji. Codziennie śledzi premiery modeli, narzędzia i regulacje AI, i tłumaczy je prostym, konkretnym językiem.

// mapa strony

🛒 Sklep Kinetyka Google AI Gemini Pro 170 zł CapCut Pro 460 zł/rok n8n Cloud Starter 320 zł/rok Zobacz wszystko →
promptowy w liczbach 0tekstów w archiwum0newsów z ostatnich 7 dni0modeli wideo w obserwatorium0zagadek w grach
× ‹ powiększenie ›