Wydajność strony często rozbija się o kilka linijek HTML i jedną decyzję: kiedy przeglądarka ma pobrać oraz uruchomić skrypt. Temat, który często trafia do rozmów o wydajności frontendu i bywa skrótowo opisywany jako js defer, dotyczy właśnie tego porządku: HTML może się parsować bez przerwy, a JavaScript startuje dopiero wtedy, gdy DOM jest już gotowy. Ja traktuję to jako jedno z prostszych i najbardziej opłacalnych usprawnień, bo zmniejsza ryzyko błędów i poprawia odczuwalną szybkość ładowania.
Najważniejsze zasady użycia atrybutu defer
- Defer pobiera skrypt równolegle z parsowaniem HTML, ale wykonuje go dopiero po zakończeniu parsowania.
- Kolejność plików jest zachowana, więc można bezpiecznie ładować zależne od siebie skrypty.
- Działa tylko dla zewnętrznych skryptów z `src`; w skryptach inline nie ma efektu.
- Nie jest tym samym co async i nie rozwiązuje każdego problemu z wydajnością.
- Moduły ES mają to zachowanie domyślnie, więc `defer` nie daje im dodatkowej korzyści.
- Najlepiej sprawdza się tam, gdzie kod dotyka DOM albo zależy od kolejności ładowania.
Jak działa defer i co dokładnie zmienia
Atrybut `defer` mówi przeglądarce, żeby pobrała skrypt bez blokowania parsowania HTML, ale nie uruchamiała go od razu. Kod wykonuje się dopiero wtedy, gdy dokument został już przeanalizowany, a DOM istnieje w pełnej postaci. To ważne, bo wiele błędów frontowych bierze się właśnie z tego, że skrypt próbuje odwołać się do elementu, którego jeszcze nie ma na stronie.
W praktyce wygląda to tak: plik JS może zacząć się pobierać już w `
`, ale przeglądarka nie zatrzyma przez to budowania strony. Gdy HTML zostanie sparsowany, skrypt uruchomi się przed zdarzeniem `DOMContentLoaded`, a jeśli masz kilka plików z `defer`, zostaną wykonane w tej samej kolejności, w jakiej pojawiają się w dokumencie.
To jest szczególnie wygodne, gdy najpierw ładujesz bibliotekę albo warstwę pomocniczą, a dopiero potem kod aplikacji. Warto też pamiętać o dwóch ograniczeniach: `defer` ma sens dla skryptów z `src`, a dla modułów ES jest w praktyce zbędny, bo moduły i tak działają podobnie domyślnie. Z tego powodu często lepiej myśleć o nim jako o narzędziu do klasycznych plików JS niż o uniwersalnym przełączniku. Skoro już wiesz, co robi od środka, pora zestawić go z innymi sposobami ładowania skryptów.

Defer, async i klasyczne skrypty nie rozwiązują tego samego problemu
Najwięcej nieporozumień bierze się stąd, że te trzy opcje brzmią podobnie, ale w praktyce ustawiają zupełnie inny moment wykonania. Ja zwykle porównuję je nie pod kątem tego, czy „przyspieszają stronę”, tylko co dokładnie blokują, kiedy startują i czy można liczyć na kolejność.| Opcja | Kiedy przeglądarka pobiera plik | Kiedy wykonuje kod | Czy kolejność jest przewidywalna | Najlepsze zastosowanie |
|---|---|---|---|---|
| Klasyczny ` | W momencie napotkania tagu | Od razu, z przerwaniem parsowania | Tak, ale kosztem blokowania HTML | Rzadko, głównie dla bardzo małych krytycznych fragmentów |
| ` | Równolegle z parsowaniem HTML | Po sparsowaniu dokumentu | Tak | Skrypty zależne od DOM i od siebie nawzajem |
| ` | Równolegle z parsowaniem HTML | Gdy tylko plik się pobierze | Nie | Niezależne widgety, statystyki, zewnętrzne integracje |
| ` | Równolegle z parsowaniem HTML | Po sparsowaniu, podobnie jak `defer` | Zwykle tak, w ramach modelu modułów | Nowoczesne aplikacje oparte na ES modules |
Najpraktyczniejsza różnica jest taka: `defer` pozwala pobrać skrypt wcześnie, ale uruchamia go późno. To często daje lepszy efekt niż wrzucenie tagu na sam dół `
`, bo wtedy pobieranie pliku startuje dopiero po przejściu przez cały HTML. W małych projektach to może nie robić wielkiej różnicy, ale w większych aplikacjach i przy wolniejszym łączu potrafi być odczuwalne. Jeśli chcesz, by kod nie blokował renderowania, a jednocześnie był gotowy, gdy DOM już istnieje, `defer` zwykle wygrywa z klasycznym podejściem.Kiedy używam defer, a kiedy zostawiam inny sposób ładowania
Najczęściej sięgam po `defer`, gdy skrypt ma coś zrobić z gotową strukturą strony: podpiąć event listenery, zainicjować komponenty, wypełnić menu, uruchomić walidację formularza albo podłączyć kilka zależnych od siebie plików. To dobry wybór także wtedy, gdy kod jest rozbity na warstwy: biblioteka, narzędzia pomocnicze i właściwa logika aplikacji.
W praktyce traktuję `defer` jako domyślne rozwiązanie dla zewnętrznych skryptów aplikacyjnych, które nie muszą wejść do akcji zanim dokument się zbuduje. Jeśli elementy interfejsu istnieją dopiero po parsowaniu HTML, wcześniejsze uruchomienie kodu nie daje żadnej korzyści, a tylko zwiększa ryzyko błędu.
Gdzie daje najlepszy efekt
- Skrypty inicjalizujące interfejs po stronie klienta.
- Pliki zależne od kolejności, na przykład biblioteka i kod aplikacji.
- Duże bundle, które nie muszą blokować pierwszego renderu.
- Rozwiązania, w których kod odwołuje się do elementów DOM, stylów lub atrybutów strony.
Przeczytaj również: JavaScript - Jak czytać i pisać kod bez chaosu?
Kiedy lepiej wybrać coś innego
- Gdy skrypt jest całkowicie niezależny i może wejść do akcji natychmiast po pobraniu.
- Gdy używasz modułów ES i nie potrzebujesz dodatkowego sterowania kolejnością.
- Gdy fragment JS jest naprawdę mały i krytyczny dla pierwszego ekranu.
- Gdy mówimy o inline script bez `src`.
Tu jest ważny niuans: `defer` nie sprawia, że kod staje się „lekki”. On tylko przesuwa moment wykonania. Jeśli sam skrypt wykonuje ciężką pracę, nadal może spowolnić interakcję tuż po parsowaniu HTML. Dlatego ja patrzę na ten atrybut jako na sposób na uporządkowanie startu, a nie jako cudowną metodę na ciężki JavaScript. To prowadzi do typowych błędów, które widzę najczęściej podczas wdrażania tej techniki.
Najczęstsze błędy, które psują efekt
Najbardziej kosztowny błąd to założenie, że `defer` działa wszędzie tak samo. Nie działa. Jeśli skrypt jest osadzony inline, atrybut nie daje praktycznie nic. Jeśli kod jest modułem, efekt jest już zapewniony przez sam model modułów. Jeśli do tego dorzucisz `async` obok `defer`, przeglądarka i tak potraktuje tag tak, jakby miał tylko `async`.
- Mylenie `defer` z opóźnieniem do końca ładowania strony - skrypt uruchamia się po parsowaniu HTML, ale przed `DOMContentLoaded`, nie „po wszystkim”.
- Zakładanie, że działa dla inline script - bez `src` nie ma realnego efektu.
- Ignorowanie kolejności plików - `defer` ją zachowuje, więc zmiana kolejności w HTML nadal ma znaczenie.
- Łączenie z `async` bez zamiaru - jeśli oba są obecne, `async` wygrywa.
- Używanie go jako zamiennika optymalizacji kodu - ciężki JS nadal trzeba uprościć, podzielić albo odłożyć.
Ja szczególnie pilnuję jednego scenariusza: gdy ktoś przenosi skrypt z końca `
` do `` i dodaje `defer`, a potem zapomina przetestować inicjalizację. Wtedy kod, który wcześniej działał przypadkiem dzięki położeniu w HTML, nagle wymaga poprawnego dostępu do DOM, selektorów i zależności. To nie jest wada `defer`, tylko moment, w którym wychodzi na jaw, że aplikacja była oparta na szczęściu, a nie na porządnym cyklu ładowania. Da się temu łatwo zapobiec, jeśli wdrożyć zmianę metodycznie.Jak wdrożyć to poprawnie w projekcie
Jeśli miałbym zrobić to w prosty, bezpieczny sposób, zacząłbym od jednego pytania: czy ten skrypt naprawdę musi działać przed zbudowaniem DOM? W większości stron odpowiedź brzmi „nie”. Wtedy przenoszę plik do `
`, dodaję `defer`, sprawdzam zależności i testuję kolejność uruchamiania.- Wydziel wszystkie zewnętrzne pliki JS, które zależą od struktury strony.
- Dodaj `defer` do tagów `
- Ustaw kolejność plików zgodnie z zależnościami, nie tylko zgodnie z estetyką HTML.
- Przenieś inicjalizację interfejsu do kodu, który działa po parsowaniu DOM.
- Sprawdź konsolę i testuj na wolniejszym łączu, żeby zobaczyć realny moment wykonania.
Przykład praktyczny: jeśli mam bibliotekę pomocniczą i własny kod aplikacji, ustawiam je tak, aby biblioteka była pierwsza, a dopiero potem warstwa inicjalizująca UI. Dzięki temu nie muszę doklejać ręcznych opóźnień, czekać na dodatkowe eventy ani pisać zbędnych wrapperów. W wielu projektach to wystarcza, żeby usunąć przypadkowe błędy związane z kolejnością ładowania. Po wdrożeniu warto jeszcze wykonać jeden krok, którego sporo osób pomija: sprawdzić, co dzieje się z innymi zasobami strony.
Co jeszcze sprawdzam po przełączeniu skryptów na opóźnione ładowanie
Po zmianie sposobu ładowania nie kończę pracy na samym tagu `
Jeśli projekt jest większy, patrzę też na podział kodu: czy wszystkie moduły muszą być ładowane na starcie, czy część z nich da się uruchomić dopiero po interakcji użytkownika. Sam `defer` poprawia start, ale nie zastępuje rozsądnego cięcia bundle'a ani nie rozwiązuje problemu nadmiarowego JavaScriptu. Najlepszy efekt daje połączenie prostego ładowania, dobrej kolejności plików i odciążenia rzeczy, które nie są potrzebne od pierwszej sekundy.
Właśnie dlatego traktuję `defer` jako domyślny wybór dla skryptów, które mają czekać na gotowy DOM, ale nie chcą blokować użytkownika. To małe ustawienie, a często robi większą różnicę niż kolejne kosmetyczne poprawki w HTML.