Viewport Googlebota
W ramach eksperymentu zmusiłem Googlebota wiele razy (blisko 100 testów) do wyrenderowania strony o wysokości 10,000,000 pikseli (słownie: 10 milionów pikseli!). Wyniki ujawniają m.in istnienie trzyfazowej architektury i mechanizmu, który powoduje, ze strona jest „płaskim i statycznym obrazem” bez względu na to jak bardzo jest długa.
Zapraszam do artykułu, w którym przeprowadziłem inżynierię wsteczną Web Rendering Service (WRS). Ten artykuł w pewnym stopniu redefiniuje zasady technicznego SEO …a kolejne testy w drodze, łącznie jakieś 40 dodatkowych 🙂

Grupa 1 testów: Viewport Expansion i mechanizm „spłaszczania”
Jak powszechnie wiadomo, Googlebot nie scrolluje w dosłownym tego znaczeniu (wiadomo, prawda?). Natomiast zainteresował mnie temat limitu tego co widzi Google. Stworzyłem stronę testową o wysokości 10 milionów pikseli i rozmieściłem na niej 17 markerów, czyli elementów DOM monitorowanych przez IntersectionObserver1, w skrócie IO. Każdy marker wysyłał żądanie HTTP do serwera w momencie pojawienia się w polu widzenia, raportując swoją głębokość, aktualną wartość window.innerHeight oraz precyzyjny czas od startu renderowania w milisekundach.
Dodatkowo uruchomiłem próbkowanie stanu co kilkaset milisekund przez 15 sekund. Każde próbkowanie rejestrowało aktualny innerHeight oraz liczbę odpalonych markerów.
Faza ciszy
Pierwsze markery odpaliły się prawie od razu po załadowaniu strony:
[100ms] MARKER 0px fired (innerH=732)
[102ms] MARKER 200px fired (innerH=732)
[104ms] MARKER 400px fired (innerH=732)
[106ms] MARKER 600px fired (innerH=732)
[108ms] MARKER 1.1Kpx fired (innerH=732)
[110ms] MARKER 1.5Kpx fired (innerH=732)Sześć markerów, wszystkie w przedziale 100-110 milisekund. Wartość innerHeight przy każdym wynosiła 732px. Markery te mieściły się w tym viewport, więc IntersectionObserver natychmiast zaraportował ich widoczność.
Pozostałe 11 markerów – od 5 tysięcy do 10 milionów pikseli głębokości – milczało. Próbkowanie pokazywało stabilny stan przez następne sekundy:
[112ms] SAMPLE: innerH=732, fired=6/17
[509ms] SAMPLE: innerH=732, fired=6/17
[1009ms] SAMPLE: innerH=732, fired=6/17
[2009ms] SAMPLE: innerH=732, fired=6/17
[3009ms] SAMPLE: innerH=732, fired=6/17
[4009ms] SAMPLE: innerH=732, fired=6/17
[4509ms] SAMPLE: innerH=732, fired=6/17
[5009ms] SAMPLE: innerH=732, fired=6/17
[5109ms] SAMPLE: innerH=732, fired=6/17
[5209ms] SAMPLE: innerH=732, fired=6/17Przez ponad 5 sekund nic się nie zmieniało. Viewport pozostawał na 732 pikselach. IO widział tylko elementy mieszczące się w tym obszarze. Googlebot zachowywał się dokładnie jak normalna przeglądarka mobilna.
Moment skoku
Między próbkowaniem 5209ms, a 5300ms nastąpiła nagła zmiana.
Wartość innerHeight skoczyła z 732px do 9,999,040px – pełnej wysokości dokumentu. Nie było żadnego stopniowego wzrostu, po prostu jeden dyskretny skok:
[5209ms] SAMPLE: innerH=732, fired=6/17
[5300ms] 🚨 EXPANSION: innerHeight 732px → 9999040pxNatychmiast po nim IntersectionObserver zaczął odpalać callbacki dla wszystkich pozostałych markerów:
[5302ms] MARKER 5.0Kpx fired (innerH=9999040)
[5304ms] MARKER 20.0Kpx fired (innerH=9999040)
[5306ms] MARKER 50.0Kpx fired (innerH=9999040)
[5308ms] MARKER 100.0Kpx fired (innerH=9999040)
[5310ms] MARKER 250.0Kpx fired (innerH=9999040)
[5312ms] MARKER 500.0Kpx fired (innerH=9999040)
[5314ms] MARKER 1.0Mpx fired (innerH=9999040)
[5316ms] MARKER 2.5Mpx fired (innerH=9999040)
[5318ms] MARKER 5.0Mpx fired (innerH=9999040)
[5320ms] MARKER 7.5Mpx fired (innerH=9999040)
[5322ms] MARKER 10.0Mpx fired (innerH=9999040)Wszystkie 11 markerów – od 5 tysięcy do 10 milionów pikseli – odpaliło się w przedziale 20 milisekund. Każdy z nich raportował tę samą wartość innerHeight: 9,999,040px. Viewport został rozciągnięty na całą stronę i wszystkie elementy nagle znalazły się „w polu widzenia” Googlebota jednocześnie.
Gdyby Googlebot przewijał stronę, to markery odpalałyby się sekwencyjnie, w miarę jak viewport przesuwałby się w dół, więc w 100% potwierdziłem, że Googlebot nie scrolluje. Marker na 5 milionach pikseli musiałby odpalić się przed markerem na 10 milionach. Tutaj wszystkie wystartowały w tej samej chwili – kolejność ich rejestracji w logach (5k przed 20k przed 50k) wynikała z kolejności przetwarzania przez silnik JS, nie z jakiegokolwiek ruchu viewport.
Desktop kontra mobile
Logi pokazały jeszcze jedną rzecz. Test wykonywał się równolegle dla dwóch user-agentów – desktop i mobile. Moment ekspansji różnił się między nimi:
Desktop: EXPANSION @ elapsed: 5001ms
Mobile: EXPANSION @ elapsed: 5301msDesktop wykonał ekspansję około 300 milisekund wcześniej niż mobile. Różnica może wynikać z szybszego renderowania (desktop ma prostszy layout przy tej samej stronie), różnych konfiguracji związanych z urządzeniem lub innych parametrów WRS. Bazując na danych, które mogłem pozyskać nie jestem w stanie jednoznacznie stwierdzić, a jedynie się domyślać.
Co to oznacza?
Przez pierwsze ~5 sekund Googlebot renderuje stronę jak normalna przeglądarka mobilna. Potem, w jednej klatce rozciąga viewport do pełnej wysokości dokumentu. Wszystkie elementy stają się „widoczne” w jednej chwili. Cała strona zostaje spłaszczona do statycznego obrazu.
Limit wysokości strony nie istnieje w praktycznym sensie. Googlebot poprawnie zarejestrował marker na głębokości 10 milionów pikseli. W takim razie ograniczeniem nie jest geometria, a czas renderowania i przepustowość sieci, o czym więcej w dalszych testach.
Dla IntersectionObserver oznacza to, że wszystkie callbacki zostaną odpalone dopiero po fazie ekspansji. Skrypt ładujący obrazki przez IO będzie działał poprawnie. Musi tylko zdążyć wykonać swoją pracę w krótkim oknie czasowym między ekspansją, a zatrzymaniem silnika JavaScript.
Grupa 2 testów: Co dzieje się w momencie ekspansji viewportu
W Test 2 analizuję chronologię wcześniej opisywanego zjawiska i zakładam istnienie trójfazowego cyklu renderowania (faza emulacji mobilnej → ekspansja i okres przejściowy → zamrożenie i snapshot).
Strona testowa była wyposażona w pingi HTTP wysyłane co 200 milisekund przez 20 sekund. Każdy raportujący aktualną wartość window.innerHeight i stan Media Queries. Jeśli warstwy byłyby izolowane, to JavaScript powinien przez cały czas widzieć 732px niezależnie od tego co się dzieje w kompozytorze2.
Trzy fazy renderowania
Faza 1: emulacja mobilna
Przez pierwsze pięć sekund WRS zachowuje się jak standardowa przeglądarka mobilna.
Elementy zdefiniowane jako height: 100vh mają w tej fazie obliczoną wysokość 732px, a layout wygląda dokładnie tak, jak na prawdziwym urządzeniu mobilnym.
| Właściwość | Wartość | Źródło |
|---|---|---|
| window.innerHeight | 732px | Nexus 5X viewport |
| window.innerWidth | 412px | Nexus 5X viewport |
| devicePixelRatio | 2.625 | Nexus 5X DPR |
| matchMedia('(max-height: 1000px)’) | true | Standard mobile |
| matchMedia('(min-height: 5000px)’) | false | Brak ekspansji |
Wszystkie mechanizmy JavaScript i CSS widzą telefon. IntersectionObserver w tej fazie raportuje widoczność tylko dla elementów mieszczących się w tych 732px. Elementy poniżej mają isIntersecting: false. To jest faza „spokojnego” renderowania – JavaScript się wykonuje, zasoby są pobierane, drzewo DOM jest budowane.
[99ms] Callback fired for: p0, p100, p200, p300, p400, p500, p600, p700
[99ms] Elements beyond 732px: isIntersecting = falseCzas trwania: 0ms do ~5300ms
Faza 2: ekspansja viewport i okres przejściowy
W okolicach 5,3 sekundy silnik WRS wykonuje operację ekspansji. Viewport zostaje rozciągnięty do wartości scrollHeight dokumentu, czyli 10 milionów pikseli. JavaScript ma ~200ms na obsługę tych callbacków przed zatrzymaniem silnika.
Operacja Viewport Expansion:
W jednym cyklu renderowania silnik WRS wykonuje:
Height_Viewport = document.documentElement.scrollHeight
To uruchamia serie zdarzeń. IO odpala callbacki dla wszystkich obserwowanych elementów jednocześnie – nagle wszystkie znajdują się w viewport.
[5299ms] IO callback: p3500, p10500, p51000, p101500 → isIntersecting: true
[5299ms] IO callback: p502000, p1002500, p5003000, p10003500 → isIntersecting: true
[5299ms] Total: 11/11CSS przelicza style względem nowych wymiarów viewport, więc Media Queries reagują na zmianę.
@media (min-height: 5000px) → ACTIVE
@media (min-height: 100000px) → ACTIVE
@media (min-height: 1000000px) → ACTIVE (dla stron >1M px)Test 21 w tej grupie potwierdza w logach
[3ms] background-color: rgb(0, 34, 0) [reguła domyślna]
[5501ms] background-color: rgb(102, 0, 0) [reguła @media min-height: 1M]Rekalkulacja jednostek viewport-relative
.hero { height: 100vh }
Faza 1: computed height = 732px
Faza 2: computed height = 100020pxElementy zdefiniowane jako height: 100vh ulegają przeskalowaniu:
Najważniejsze odkrycie: JavaScript nie zostaje natychmiast zatrzymany po ekspansji. Istnieje okno czasowe – w moich testach wynoszące 100-200 milisekund – w którym silnik JS nadal pracuje. W tym czasie kod może zareagować na ekspansję: obsłużyć callbacki IntersectionObserver, podmienić źródła obrazków, zaktualizować DOM. Nazwijmy to roboczo „okresem karencji”.
Czas trwania: 100-200ms (zarejestrowany zakres: 5399ms -> 5501ms)
Faza 3: zamrożenie i snapshot
Po upływie „okresu karencji” silnik V8 zostaje zatrzymany. Wszystkie zaplanowane zdarzenia, setTimeout, kolejka zadań – wszystko zostaje porzucone bez wykonania. DOM jest zamrażany w aktualnym stanie i przekazywany do systemu indeksowania.
W teście 23 tej grupy ostatnia zarejestrowana aktualizacja DOM miała miejsce w 5501 milisekundzie. Marker przestał się zmieniać po tym czasie.
[5501ms] js-alive-marker: "JS alive @ 5501ms, innerH: 100020"
[5600ms] (brak aktualizacji markera)
[5800ms] (brak aktualizacji markera)Czas trwania: ~5500ms+
Wnioski o izolacji CSS
Wcześniejsze testy (13-16) z tej grupy wykazywały, że CSS nie widzi ekspansji viewport. Obserwacja była technicznie poprawna ale niepełna, bo dotyczyła tylko Fazy 1. W tym czasie rzeczywiście Media Queries widzą mobilny viewport i nie reagują na reguły typu @media (min-height: 10000px).
Test 21 pokazał jednak, co dzieje się w Fazie 2. Pułapka CSS zdefiniowana jako
@media (min-height: 1000000px) {
body { background: red }
}pozostawała nieaktywna przez 5 sekund, a następnie została uruchomiona. Kolor tła zmienił się na rgb(102, 0, 0), więc reguła zadziałała.
To oznacza, że stan dokumentu HTML w postaci „spłaszczenia” jest utrwalany po zakończeniu rekalkulacji CSS, a nie przed nią. Stan zamrożony w Fazie 3, to stan po ekspansji.
Konsekwencje 100vh
Test z elementem height: 100vh na stronie o wysokości 10 milionów pikseli:
// Mobile (Nexus 5X)
[11ms] load: innerH=732 100vh=732
[5300ms] resize: innerH=10000000 100vh=10000000
// Desktop (Linux x86_64)
[11ms] load: innerH=1024 100vh=1024
[5000ms] resize: innerH=10000000 100vh=10000000CSS jednostka vh jest przeliczana w momencie ekspansji. Element z height: 100vh rośnie z 732px do 10 milionów pikseli, spychając całą treść poniżej.
Ta sekwencja zdarzeń wyjaśnia, dlaczego height: 100vh bez ograniczenia jest problematyczne.
- Faza 1: element hero ma obliczoną wysokość 732px i wygląda normalnie
- Faza 2: po ekspansji, ta sama reguła CSS oblicza wysokość jako 100% z 10 milionów pikseli. Czyli cała treść poniżej zostaje zepchnięta o miliony pikseli w dół.
Rozwiązanie jest trywialne technicznie: max-height: 800px lub height: min(100vh, 800px). Problem polega na tym, że bez znajomości mechaniki renderowania WRS nie ma powodu, żeby takie ograniczenie stosować.
Edge case: strony krótsze niż viewport
Gdy treść strony jest mniejsza niż viewport, mechanizm ekspansji zachowuje się inaczej:
- Desktop: Brak zdarzenia resize – viewport pozostaje 1024×1024
- Mobile: Resize występuje ale zachowanie jest niespójne
- scrollHeight: Dopasowywany do innerHeight, nie do rzeczywistej wysokości treści
Ekspansja nie występuje gdy scrollHeight ≤ innerHeight bo nie ma czego rozciągać.
Okno krytyczne
„Okres karencji” trwający ~100-200ms to jednocześnie szansa i ograniczenie. To jedyny moment, w którym JavaScript może zareagować na fakt, że wszystkie elementy są widoczne.
IntersectionObserver wykorzystuje to okno automatycznie – callbacki są odpalane w momencie ekspansji i mają czas na wykonanie przed odcięciem. Problem pojawia się, gdy callback wykonuje operacje zbyt wolne. Jeśli wewnątrz callbacka IO znajduje się:
- ciężka manipulacja DOM
- zapytanie do API
- skomplikowane obliczenia
To może nie zdążyć przed zatrzymaniem silnika. Główny wątek jest w tym momencie ekstremalnie obciążony, a setki elementów odpala się jednocześnie.
Reguła 5 sekund
Ekspansja viewport następuje w okolicach 5,3 sekundy w każdym przeprowadzonym teście (a zrobiłem ich łącznie 23 w tej grupie). Wszystko co ma znaczenie dla SEO, musi znaleźć się w DOM przed upływem 5 sekund od rozpoczęcia renderowania. Treść ładowana przez setTimeout z opóźnieniem większym niż 5000ms nie zostanie wykonana. Dane z API, które docierają po tym limicie, nie zostaną wyrenderowane.
Model końcowy
| Parametr | Faza 1 | Faza 2 | Faza 3 |
|---|---|---|---|
| Zakres czasowy | 0 – ~5300ms | ~5300 – ~5500ms | ~5500ms+ |
| window.innerHeight | 732px | scrollHeight | JS off |
| matchMedia(min-height: 5000px) | false | true | – |
| IntersectionObserver | viewport-only | wszystko widoczne | – |
| 100vh obliczone | 732px | scrollHeight | zamrożone |
| JavaScript | aktywny | aktywny | zatrzymany |
| Stan | emulacja mobile | ekspansja + rekalkulacja | snapshot |
Googlebot stosuje architekturę trójfazową w sensie czasowym. Przez 5 sekund emuluje urządzenie mobilne z pełną wiernością. Następnie rozciąga viewport, daje skryptom ułamek sekundy na reakcję, zamraża stan i wykonuje snapshot.
Całość trwa około 5,5 sekundy. Po tym czasie strona istnieje już tylko jako statyczny obraz w systemie indeksowania.
Grupa 3 testów: Zdarzenia przeglądarki w środowisku Googlebota
Po ustaleniu mechanizmu ekspansji viewport, kolejnym krokiem było zbadanie jakie zdarzenia JavaScript są emitowane przez WRS. Zaimplementowałem nasłuchiwanie na: scroll, resize, load, DOMContentLoaded oraz visualViewport.resize.
Testy ujawniły dokładne wymiary viewport dla obu wariantów Googlebota:
| Parametr | Mobile | Desktop |
|---|---|---|
| innerWidth | 412px | 1024px |
| innerHeight (initial) | 732px | 1024px |
| Proporcje | ~1:1.78 (portret) | 1:1 (kwadrat) |
| devicePixelRatio | 2.625 | 1 |
| outerWidth / outerHeight (wstępnie) | 412×732 | 1×1 (!) |
| outerWidth / outerHeight (po ekspansji) | 1×1 | 1×1 |
Interesującą obserwacją jest kwadratowy viewport desktop (1024×1024) oraz anomalia outerWidth/outerHeight = 1px dla desktop, co sugeruje że WRS nie emuluje pełnego okna przeglądarki.
Po ekspansji outerWidth/outerHeight zmienia się na 1×1 również dla mobile, co też jest ciekawym odkryciem.
resize
Zdarzenie resize jest emitowane w momencie ekspansji viewport. To odkrycie zmienia rozumienie mechaniki WRS.
// Desktop
[10ms] DOMContentLoaded | 1024×1024
[11ms] load | 1024×1024
[5000ms] RESIZE | 1024×1024 → 1024×50000
// Mobile
[10ms] DOMContentLoaded | 412×732
[11ms] load | 412×732
[5300ms] RESIZE | 412×732 → 412×50000Ekspansja dotyczy wyłącznie wysokości. Szerokość pozostaje stała przez cały cykl renderowania. Desktop ekspanduje ~300ms szybciej niż mobile.
Praktyczne zastosowanie
JavaScript może wykryć moment ekspansji viewport:
let expansionDetected = false;
window.addEventListener('resize', () => {
if (!expansionDetected && window.innerHeight > 10000) {
expansionDetected = true;
console.log('Wykryta ekspansja Googlebota');
}
});Mechanizm ten może służyć do:
- Detekcji środowiska WRS bez analizy User-Agent
- Dynamicznego ładowania treści w momencie ekspansji
- Debugowania problemów z renderowaniem dla Googlebota
scroll
Zdarzenie scroll nie występuje w żadnym momencie cyklu renderowania. Po rozciągnięciu viewport do pełnej wysokości dokumentu, scrollY zawsze wynosi 0 i nie ma czego przewijać. To potwierdza powszechnie znaną (ale udowodnioną?) informację, że Googlebot nie scrolluje w tradycyjnym sensie
Visual Viewport API
WRS obsługuje window.visualViewport i emituje zdarzenie visualViewport.resize równolegle ze standardowym resize:
// Mobile - dodatkowe zdarzenie ~100ms (inicjalizacja API)
[100ms] visualViewport.resize | 412×732 (bez zmian)
[5301ms] visualViewport.resize | 412×50000 (ekspansja)Konsekwencje dla kodu JavaScript
Część powszechnie stosowanych wzorców jest niewidoczna dla Googlebota:
| Wzorzec | Status dla WRS | Alternatywa |
|---|---|---|
| Lazy loading przez scroll event | ❌ Nie działa | IntersectionObserver |
| Infinite scroll | ❌ Nie działa | Paginacja / IO |
| Parallax na scroll | ❌ Statyczny | Brak |
| Sticky header po scroll | ❌ Nie przykleja | position: fixed |
| Detekcja przez resize | ✅ Działa | – |
IntersectionObserver pozostaje zalecanym mechanizmem detekcji widoczności elementów, ponieważ reaguje na stan geometryczny – czyli na fakt znalezienia się elementu w viewport – który po ekspansji jest prawdziwy (true) dla wszystkich elementów.
Podsumowanie zdarzeń
| Zdarzenie | Występuje? | Kiedy |
|---|---|---|
| DOMContentLoaded | ✅ TAK | ~10ms |
| load | ✅ TAK | ~11ms |
| resize | ✅ TAK | ~5000-5300ms (ekspansja) |
| visualViewport.resize | ✅ TAK | ~100ms + przy ekspansji |
| scroll | ❌ NIE | nigdy |
Wpływ meta viewport na Mobile Googlebot
Tag <meta name="viewport"> ma bezpośredni wpływ na wymiary viewport Mobile Googlebota:
| Konfiguracja | Mobile viewport | Desktop viewport |
|---|---|---|
width=device-width, initial-scale=1.0 | 412×732 | 1024×1024 |
| Brak meta viewport | 981×1742 | 1024×1024 |
Brak meta viewport powoduje, że Mobile Googlebot zachowuje się jak przeglądarka bez obsługi responsive – przyjmuje domyślną szerokość ~980px i skaluje stronę. Desktop Googlebot ignoruje meta viewport całkowicie.
Zalecenie: Zawsze stosuj <meta name="viewport" content="width=device-width, initial-scale=1.0"> aby uzyskać przewidywalne wymiary viewport dla testów i renderowania.
Kompletna specyfikacja viewport WRS
| Parametr | Mobile (z meta) | Mobile (bez meta) | Desktop |
|---|---|---|---|
| innerWidth | 412px | 981px | 1024px |
| innerHeight (wstępnie) | 732px | 1742px | 1024px |
| devicePixelRatio | 2.625 | 2.625 | 1 |
| User-Agent device | Nexus 5X | Nexus 5X | Linux x86_64 |
| Czas ekspansji | ~5300ms | ~5300ms | ~5000ms |
| innerHeight (final) | scrollHeight | scrollHeight | scrollHeight |
Grupa 4 testów: Gwarancja responsywności
Odkrycie mechanizmu viewport expansion w pionie postawiło pytanie: czy ekstremalne wydłużenie strony wpływa na jej szerokość? Czy długa strona mobilna może zostać błędnie zinterpretowana jako desktopowa, co zniszczyłoby wynik Mobile-First Indexing?
Zbadałem zachowanie obu instancji Googlebota na stronach o wysokości od 50,000 do 10 milionów pikseli i logi pokazały, że szerokość pozostaje absolutnie stała przez cały cykl renderowania:
// Strona 10,000,000px wysokości
// Mobile (Nexus 5X)
[11ms] load: 412×732 → widthDelta: 0
[5300ms] resize: 412×10000000 → widthDelta: 0
// Desktop (Linux x86_64)
[11ms] load: 1024×1024 → widthDelta: 0
[5000ms] resize: 1024×10000000 → widthDelta: 0Googlebot Mobile (Nexus 5X):
innerWidth: 412px (stałe przez cały cykl)devicePixelRatio: 2.625- Media Query
@media (max-width: 799px): zawszetrue
Googlebot Desktop (Linux x86_64):
innerWidth: 1024px (stałe przez cały cykl)devicePixelRatio: 1- Media Query
@media (min-width: 800px): zawszetrue
Ekspansja viewport jest wyłącznie wertykalna
Niezależnie od wysokości treści, warunki dla Media Queries (min-width, max-width) pozostają nienaruszone. Strona mobilna zawsze będzie renderowana z użyciem stylów mobilnych. Nie istnieje ryzyko, że silnik renderujący, próbując zmieścić treść w pionie, przypadkowo rozszerzy kontener w poziomie.
Konsekwencje dla obrazków responsywnych
Mechanizm wyboru obrazka w srcset i <picture> opiera się na szerokości viewport i devicePixelRatio. Dzięki ich stałości:
- Mobile Googlebot zawsze pobierze wersję obrazka dla urządzeń mobilnych
- Desktop Googlebot zawsze pobierze wersję desktopową
- Efektywna szerokość dla mobile: ~1082px (412 × 2.625 DPR)
- Efektywna szerokość dla desktop: 1024px (1024 × 1 DPR)
To gwarantuje poprawne działanie Core Web Vitals (LCP) oraz Mobile-First Indexing niezależnie od długości strony.
Wnioski dla branży SEO
Przeprowadzone eksperymenty wymuszają zmianę paradygmatu optymalizacji stron opartych na JavaScript. Zrozumienie, że Googlebot dokonuje ekspansji viewportu ma krytyczne konsekwencje dla strategii pozycjonowania, a przede wszystkim dla architektury kodu.
1. Mit „Googlebot nie widzi treści below the fold” jest fałszywy
Googlebot widzi całą stronę niezależnie od jej długości. Mechanizm ekspansji viewportu gwarantuje, że każdy element DOM – nawet na głębokości 10 milionów pikseli – zostanie „zobaczony” przez renderer.
Wniosek: Argumenty sprzedażowe typu „przenieś treść wyżej, bo Googlebot jej nie zobaczy” są technicznie nieprawdziwe. Pozycja treści na stronie nie wpływa na jej widoczność dla crawlera, co nie zmienia faktu, że użytkownik powinien treść zobaczyć jak najszybciej.
2. Reguła 5 sekund
Wszystko co ma znaczenie dla indeksowania musi znaleźć się w DOM przed upływem ~5 sekund od załadowania strony. To nie jest „zalecenie” – to techniczny limit działania silnika JavaScript w WRS.
Wniosek: Audyty techniczne powinny mierzyć czas do renderowania treści, a nie tylko metryki typu LCP czy FCP. Strona może mieć doskonałe Core Web Vitals ale jeśli treść ładuje się po 6 sekundach – Googlebot jej nie zobaczy.
3. JavaScript nie jest problemem – czas jest problemem
WRS w pełni wykonuje JavaScript, obsługuje fetch, Promise, async/await, IntersectionObserver. Problem leży w tym, czy JavaScript zdąży się wykonać.
Wnioski: Dyskusja „SSR vs CSR dla SEO” powinna być przeformułowana na „czy renderowanie zajmuje mniej niż 5 sekund”. SPA z szybkim hydration może być lepsza niż wolny SSR.
4. Responsive design jest bezpieczny
Ekspansja viewportu dotyczy wyłącznie wysokości. Szerokość pozostaje stała (412px mobile, 1024px desktop) przez cały cykl renderowania. Media Queries oparte na szerokości działają identycznie jak w prawdziwej przeglądarce.
Wnioski: Nie ma ryzyka, że Googlebot „pomyli” wersję mobilną z desktopową. Mobile-First Indexing działa przewidywalnie.
5. Lazy loading przez IntersectionObserver działa
IO otrzymuje callbacki dla wszystkich elementów w momencie ekspansji i ma ~100-200ms na ich obsługę. Obrazy ładowane przez nowoczesny lazy loading zostaną pobrane.
Wnioski: Natywne loading="lazy" oraz biblioteki oparte na IO (np. vanilla-lazyload, lozad.js) są bezpieczne dla SEO. Problem dotyczy tylko implementacji opartych na zdarzeniu scroll.
Rekomendacje dla branży SEO
✅ Co robić
| Praktyka | Dlaczego działa |
|---|---|
Używaj loading="lazy" na obrazach | IO odpala callback przy ekspansji |
| Stosuj IO do lazy loadingu | Jedyny mechanizm detekcji widoczności działający w WRS |
| Ładuj krytyczną treść synchronicznie | Musi być w DOM przed upływem 5 sekund |
Stosuj <meta name="viewport"> | Gwarantuje przewidywalne wymiary 412×732 |
Ogranicz 100vh przez max-height | Zapobiega rozciągnięciu elementu do milionów pikseli |
| Testuj czas renderowania treści | Bardzo ważna metryka, ważniejsza niż CWV dla indeksowania |
❌ Czego unikać
| Antywzorzec | Dlaczego nie działa |
|---|---|
Lazy loading przez scroll event | Scroll nigdy nie występuje w WRS, chociaż są wyjątki |
| Infinite scroll bez fallbacku | Bez scroll event kolejne strony się nie załadują |
| Treść ładowana z opóźnieniem >5s | JavaScript zostaje zatrzymany przed jej wyrenderowaniem |
height: 100vh bez ograniczenia | Po ekspansji element ma wysokość całego dokumentu |
| Detekcja bota przez scroll behavior | Bot nie scrolluje – false positive |
Podsumowując
Googlebot daje stronie maksymalnie 5 sekund na wyrenderowanie treści, potem rozciąga viewport na całą wysokość dokumentu, dodatkowo JavaScript ma jeszcze ~200ms na reakcję, po czym zamraża DOM i robi snapshot do indeksowania. Czyli wszystko co istotne dla SEO musi wydarzyć się w tym oknie czasowym. Reszta to szczegóły implementacyjne 🙂
- Intersection Observer pozwala deweloperom na łatwe i wydajne wykrywanie, kiedy element DOM przechodzi przez określone „obszary widoczności” ↩︎
- Kompozytor – W nowoczesnych przeglądarkach proces renderowania strony jest podzielony na warstwy i wątki. W tym kontekście „kompozytor” oznacza wewnętrzny mechanizm silnika przeglądarki odpowiedzialny za składanie warstw, obsługę scrolla, zoomu, czy zmian viewportu w ramach renderowania w silniku przeglądarki (browser rendering pipeline). ↩︎
