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.

👁 117 przeczytań
- 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
| Metryka | Przed (bez odroczenia) | Po (requestIdleCallback, timeout 3000 ms) |
|---|---|---|
| TBT | 64 ms | 265 ms |
| Start pierwszego zapytania | 183 ms | 383 ms |
| Start ostatniego zapytania | 1329 ms | 2411 ms |
| Rozrzut startów | 183-1329 ms | 383-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, adidTimeoutjest ustawione natrue.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.
| Sytuacja | Czy warto | Uwaga |
|---|---|---|
| Zadanie, które może się nigdy nie wykonać bez straty (prefetch, telemetria) | Tak, bez timeout | Brak limitu = brak kumulacji |
| Kilka zadań rejestrowanych razem, każde z tym samym timeout | Nie | To mój przypadek - 64 ms zamieniło się w 265 ms |
| Zadanie krytyczne, które musi się wykonać | Nie przez idle callback | Jeśli musi się wykonać, to nie jest zadanie bezczynnościowe |
| Ciężkie obliczenie, które trzeba pokroić | Tak, ale z kontrolą budżetu | Sprawdzaj 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.
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
- 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.
- 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.
- W samym callbacku zaloguj wartość
deadline.didTimeout. Jeśli w każdym wywołaniu widzisztrue, to twoje zadania w ogóle nie korzystają z bezczynności - wykonują się wyłącznie przez limit czasu i sklejają się w serię. - 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.
- Jeśli TBT wzrosło, usuń parametr
timeouti zmierz ponownie. To najtańszy test hipotezy o kumulacji - jeśli bez limitu TBT wraca do normy, masz potwierdzenie. - Jeśli mimo to zadania muszą się wykonać w określonym czasie, porzuć
requestIdleCallbacki 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.
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.
Cały tydzień w AI, w jednym mailu
Wybrane premiery, narzędzia i analizy. Raz w tygodniu, prosto do skrzynki.
Zapisz się za darmo →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.
