Powiem Ci coś, czego większość artykułów o wydajności stron nie powie. Problem z szybkością Twojej strony to nie obrazki, nie fonty, nie strategia lazy loadingu, lecz fizyka. Konkretnie - fizyka tego, jak TCP decyduje o wysłaniu pierwszej strony do przeglądarki.

A jeśli wybrałeś Reacta, Angulara albo Next.js do serwowania treści, to ten wyścig przegrałeś, zanim napisałeś pierwszą linijkę kodu aplikacji. Nie dlatego, że to złe narzędzia. To świetne narzędzia. Ale czy naprawdę potrzebujesz dostawczaka albo tira, żeby dostarczyć list do skrzynki na listy? (Parafraza słów pewnego mądrego człowieka, który za dużo lat spędził, patrząc, jak ładują się bundliki Angulara)

Mur 14KB

Kiedy przeglądarka łączy się z Twoim serwerem po raz pierwszy, uruchamia się mechanizm zwany TCP slow start. Serwer nie wie, jaką przepustowość wytrzyma połączenie, więc zaczyna ostrożnie. Wysyła dokładnie 10 pakietów TCP bez czekania na potwierdzenie odbioru. Każdy pakiet przenosi maksymalnie 1460 bajtów payloadu (to 1500 bajtów Ethernet MTU minus 40 bajtów nagłówków TCP/IP).

Matematyka: 10 pakietów x 1460 bajtów = 14 600 bajtów. W przybliżeniu 14KB.

To nie jest porada. To RFC 6928, opublikowany w 2013 roku, zaimplementowany w każdym nowoczesnym stosie TCP na planecie. Po tych początkowych 10 pakietach serwer zatrzymuje się i czeka, aż przeglądarka potwierdzi odbiór. Dopiero wtedy podwaja okno i wysyła więcej.

Jeśli cała Twoja strona mieści się w 14KB (po kompresji), przeglądarka dostaje wszystko w tym pierwszym zrywie. Jeden round trip. Jeśli się nie mieści, płacisz dodatkowy round trip za każde podwojenie: 14KB, potem 28KB, potem 56KB. Przy połączeniu z latencją 100ms to dodatkowe 100ms na każdy krok. Przez satelitę albo na chwiejnym połączeniu mobilnym może to być 612ms lub więcej za każdą dodatkową podróż.

Rzecz w tym, że 14KB po kompresji to całkiem sporo. Przy kompresji Brotli z ~70% redukcją to mniej więcej 47KB nieskompresowanego HTML-a, CSS-a i JavaScriptu. Wystarczająco na kompletną, użyteczną stronę. Jeśli zbudujesz ją dobrze.

Ważenie frameworków

Przeliczyłem to. Oto, ile popularne frameworki wysyłają do przeglądarki przy minimalnej aplikacji hello-world, po gzipie:

Vanilla HTML0 KBAstro (default)0 KBSvelte 52 KBSolidJS4 KBVue 318 KBAngular 1948 KBReact 19 + DOM55 KBNext.js (App Router)65 KBRuntime JavaScript frameworków wysyłany do przeglądarki (gzip)bundlephobia.com, framework docs, byteiota.com
Chart data
LabelValue
Vanilla HTML0 KB
Astro (default)0 KB
Svelte 52 KB
SolidJS4 KB
Vue 318 KB
Angular 1948 KB
React 19 + DOM55 KB
Next.js (App Router)65 KB

Wykres mierzy wyłącznie JavaScript runtime - kod frameworka, który przeglądarka musi pobrać i wykonać, zanim będzie w stanie cokolwiek zrobić. Vanilla HTML i Astro wysyłają domyślnie zero JS-a (strona nadal ma HTML i CSS, ale zero narzutu frameworka).

Przeczytaj ten wykres jeszcze raz. React 19 z ReactDOM wysyła 55KB po gzipie tylko na runtime frameworka. To prawie czterokrotność budżetu 14KB, a nie wyrenderowałeś jeszcze ani jednego komponentu. Angular jest gorszy - 48-130KB zależnie od użytych funkcji. Next.js, mimo server components, wciąż zrzuca 50-80KB JavaScriptu na klienta przy minimalnej stronie.

Vanilla HTML, Astro i Svelte to jedyne frameworki, które fizycznie mieszczą się w pierwszym oknie TCP. Wszystko inne wymaga co najmniej jednego dodatkowego round tripu, zanim przeglądarka ma dość JavaScriptu, żeby cokolwiek zrobić.

To nie jest problem do zoptymalizowania. Nie da się tree-shake'ować, code-splitować ani lazy-loadować poniżej 55KB runtime'u. To podłoga, nie sufit.

"Ale HTTP/2 zmienia wszystko!"

Słyszę ten zarzut non stop i jest częściowo słuszny. Barry Pollard z zespołu Google Chrome napisał szczegółową analizę w Wiresharku, która pokazuje, że podczas handshake'u TLS klient wysyła ACK-i, które powiększają okno przeciążenia jeszcze zanim zacznie się transmisja HTML-a. Na Amazon.com zaobserwował co najmniej 20 potwierdzonych pakietów przed pierwszym bajtem HTML-a, co oznaczało, że efektywne okno pierwszego zrzutu wynosiło bliżej 28-57KB.

Więc liczba 14KB to raczej ostrożne oszacowanie niż twarda ściana. Zgoda.

Ale jest coś, co optymiści HTTP/2 pomijają: HTTP/3 (QUIC) ma ten sam limit. RFC 9002 określa początkowe okno przeciążenia na 14 720 bajtów. Protokół się zmienił. Fizyka nie. A dla użytkowników na połączeniach z dużą latencją - satelita, 2G/3G, kraje rozwijające się - kara za przekroczenie pierwszego okna to wciąż setki milisekund.

Sam Pollard podsumowuje: "Nie przejmujcie się za bardzo tą magiczną liczbą 14KB, ale ogólna rada wciąż obowiązuje." Ta ogólna rada to: utrzymuj strony małe i umieszczaj ważne zasoby na początku. Czyli dokładnie to, o czym tu piszę.

Pieniądze leżące na stole

Jeśli myślisz, że to akademickie rozważania, oto liczby, które rozumieją dyrektorzy finansowi:

Amazon odkrył, że każde 100ms dodatkowego czasu ładowania strony kosztuje ich 1% sprzedaży. Przy przychodach z 2025 roku to w przybliżeniu 3,8 miliarda dolarów na 100ms. Pinterest przebudował swoje strony pod wydajność i zanotował 40% spadek czasu oczekiwania, 15% wzrost rejestracji. Vodafone poprawił LCP o 31% i zyskał 15% więcej leadów, 11% więcej wizyt w koszyku, 8% więcej sprzedaży. Rakuten poprawił Core Web Vitals i zanotował 53% wzrost przychodu na odwiedzającego.

Dla SEO dane są równie jednoznaczne. Badanie 143 827 URL-i wykazało, że strony na pozycjach 1-3 miały medianę TTFB na poziomie 180ms w porównaniu z 420ms na pozycjach 7-10. Strony z LCP poniżej 1 sekundy rankują średnio o 7,5 pozycji wyżej niż te z LCP powyżej 4 sekund.

Wiesz, co daje szybkie TTFB i niskie LCP? Wysyłanie mniej niż 14KB w pierwszym round tripie.

developers.pl w liczbach

Oto, co ta strona faktycznie wysyła:

  • HTML (SSR): 7,2 KB - mieści się w 14KB z zapasem na nagłówki HTTP
  • Suma zasobów: 620 KB (fonty, CSS, skrypty do embedów)
  • Transfer (po kompresji): 273 KB

HTML o wadze 7,2 KB dociera w pierwszym zrzucie TCP. Przeglądarka może natychmiast zacząć renderowanie. Fonty i CSS ładują się w kolejnych round tripach, ale treść jest już czytelna. Żadnego spinnera. Żadnego skeleton screena. Żadnego tańca hydratacji.

Nie dlatego, że jestem czarodziejem od wydajności, tylko dlatego, że Astro domyślnie nie wysyła żadnego JavaScriptu. Runtime frameworka to 0 KB. Waga strony to sam HTML i CSS potrzebny dla tej konkretnej strony. Wszystko inne ładuje się na żądanie, o ile w ogóle jest potrzebne.

Porównaj to z hipotetyczną implementacją tej samej strony w Reakcie. Nawet z renderowaniem po stronie serwera przeglądarka musiałaby pobrać, sparsować i wykonać ponad 55KB JavaScriptu frameworka, zanim strona stałaby się interaktywna. Na szybkim łączu możesz tego nie zauważyć. W pociągu przejeżdżającym przez tunel, na telefonie z 3G gdzieś na Podlasiu, albo przez satelitę - zauważysz.

Co z tym zrobić

Jeśli budujesz stronę z treścią, blog, portal z dokumentacją, stronę marketingową, gazetę - nie potrzebujesz 55KB runtime'u JavaScript. Kropka. Użyj Astro, użyj Hugo, użyj czystego HTML-a. Domyślnie zero JS-a, wyspy interaktywności tam, gdzie naprawdę ich potrzebujesz.

Jeśli budujesz aplikację - dashboard, narzędzie do pracy w czasie rzeczywistym, IDE - React i Angular zarabiają na swój rozmiar. Koszt runtime'u amortyzuje się w długich sesjach, gdzie użytkownik nigdy nie przeładowuje strony. Reguła 14KB ma mniejsze znaczenie, kiedy użytkownik siedzi godzinę.

Problem pojawia się, gdy ludzie używają frameworków do aplikacji do serwowania treści. Gdy strona marketingowa działa na Next.js, bo "zespół zna Reacta". Gdy portal z dokumentacją to Angular SPA, bo "integruje się z naszym design systemem". Gdy blog to Create React App, bo ktoś go zaczął w 2019 i nikt nie chciał przepisywać.

Słoń w pokoju, czyli WordPress

A potem jest WordPress. Słoń w pokoju, na którym stoi 43% internetu.

Prowadzę firmę hostingową - chmurka.pl. Widzę, co ludzie deployują każdego dnia. Wzorzec jest zawsze ten sam: strona wizytówka - pięć podstron, formularz kontaktowy, może embed Google Maps - postawiona na WordPressie z Elementorem, WooCommerce (bo "a nuż kiedyś będzie sklep"), trzema wtyczkami SEO, które ze sobą walczą, sliderem, którego nikt nie zamawiał, i "pakietem bezpieczeństwa", który waży więcej niż treść, którą chroni.

Efekt? 4MB strona główna, 8 sekund ładowania, serwowana na PHP 7.4 (to i tak dobrze). A potem klient dzwoni i pyta, dlaczego strona jest wolna, dlaczego Google go nie widzi, dlaczego użytkownicy mobilni uciekają. Odpowiedź jest zawsze ta sama: pięciostronicowa wizytówka działa na infrastrukturze zaprojektowanej pod platformę e-commerce.

Ta wizytówka mogłaby być statycznym plikiem HTML poniżej 14KB, hostowanym za darmo na Cloudflare Pages, ładującym się w 200ms. Albo na dowolnym przyzwoitym hostingu, bo pliki statyczne nie potrzebują niczego - żadnego PHP, żadnego MySQL, żadnego wp-cron zjadającego CPU przy każdej wizycie, żadnych śmieci zalegających w bazie danych.

Zamiast tego to instancja WordPressa, która potrzebuje comiesięcznych aktualizacji, sprawdzania kompatybilności wtyczek i modlitwy, żeby kolejny update Elementora nie rozwalił layoutu. Widziałem klientów, którzy wydają na utrzymanie WordPressa więcej niż na sam hosting. Płacą komuś za aktualizację wtyczek, naprawianie konfliktów i przywracanie backupów, gdy update pójdzie nie tak. Za wizytówkę. Pięć stron. Numer telefonu i embed Google Maps.

Jak mawiał pewien klasyk: "Nie mamy pańskiego płaszcza i co pan nam zrobi?" (cytat z "Miś", reż. S. Bareja, 1981) - tyle że tu to WordPress mówi do właściciela strony za każdym razem, gdy coś się wysypie. WordPress tax na prostej wizytówce to nie dług technologiczny, lecz rana zadana sobie samemu. "Łatwość obsługi" to marketingowa etykieta, która ukrywa rachunek.

Każdy z tych wyborów oznacza, że Twoja treść nigdy nie dotrze w pierwszym zrzucie TCP. Nie dlatego, że Twój kod jest zły. Bo podłoga frameworka jest powyżej sufitu.

14 600 bajtów. Albo się w tym mieścisz, albo czekasz na następną kolejkę. Fizyka nie negocjuje.

Źródła: RFC 6928, endtimes.dev, Tune The Web analysis, RFC 9002 (QUIC), Neil Patel speed study, WPO Stats, Bundlephobia