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 🙂

viewport googlebota

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/17

Przez 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 → 9999040px

Natychmiast 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: 5301ms

Desktop 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.innerHeight732pxNexus 5X viewport
window.innerWidth412pxNexus 5X viewport
devicePixelRatio2.625Nexus 5X DPR
matchMedia('(max-height: 1000px)’)trueStandard mobile
matchMedia('(min-height: 5000px)’)falseBrak 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 = false

Czas 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/11

CSS 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 = 100020px

Elementy 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=10000000

CSS 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

ParametrFaza 1Faza 2Faza 3
Zakres czasowy0 – ~5300ms~5300 – ~5500ms~5500ms+
window.innerHeight732pxscrollHeightJS off
matchMedia(min-height: 5000px)falsetrue
IntersectionObserverviewport-onlywszystko widoczne
100vh obliczone732pxscrollHeightzamrożone
JavaScriptaktywnyaktywnyzatrzymany
Stanemulacja mobileekspansja + rekalkulacjasnapshot

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:

ParametrMobileDesktop
innerWidth412px1024px
innerHeight (initial)732px1024px
Proporcje~1:1.78 (portret)1:1 (kwadrat)
devicePixelRatio2.6251
outerWidth / outerHeight (wstępnie)412×7321×1 (!)
outerWidth / outerHeight (po ekspansji)1×11×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×50000

Ekspansja 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:

WzorzecStatus dla WRSAlternatywa
Lazy loading przez scroll event❌ Nie działaIntersectionObserver
Infinite scroll❌ Nie działaPaginacja / IO
Parallax na scroll❌ StatycznyBrak
Sticky header po scroll❌ Nie przyklejaposition: 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ń

ZdarzenieWystępuje?Kiedy
DOMContentLoaded✅ TAK~10ms
load✅ TAK~11ms
resize✅ TAK~5000-5300ms (ekspansja)
visualViewport.resize✅ TAK~100ms + przy ekspansji
scroll❌ NIEnigdy

Wpływ meta viewport na Mobile Googlebot

Tag <meta name="viewport"> ma bezpośredni wpływ na wymiary viewport Mobile Googlebota:

KonfiguracjaMobile viewportDesktop viewport
width=device-width, initial-scale=1.0412×7321024×1024
Brak meta viewport981×17421024×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

ParametrMobile (z meta)Mobile (bez meta)Desktop
innerWidth412px981px1024px
innerHeight (wstępnie)732px1742px1024px
devicePixelRatio2.6252.6251
User-Agent deviceNexus 5XNexus 5XLinux x86_64
Czas ekspansji~5300ms~5300ms~5000ms
innerHeight (final)scrollHeightscrollHeightscrollHeight

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: 0

Googlebot Mobile (Nexus 5X):

  • innerWidth: 412px (stałe przez cały cykl)
  • devicePixelRatio: 2.625
  • Media Query @media (max-width: 799px): zawsze true

Googlebot Desktop (Linux x86_64):

  • innerWidth: 1024px (stałe przez cały cykl)
  • devicePixelRatio: 1
  • Media Query @media (min-width: 800px): zawsze true

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ć

PraktykaDlaczego działa
Używaj loading="lazy" na obrazachIO odpala callback przy ekspansji
Stosuj IO do lazy loadinguJedyny mechanizm detekcji widoczności działający w WRS
Ładuj krytyczną treść synchronicznieMusi być w DOM przed upływem 5 sekund
Stosuj <meta name="viewport">Gwarantuje przewidywalne wymiary 412×732
Ogranicz 100vh przez max-heightZapobiega rozciągnięciu elementu do milionów pikseli
Testuj czas renderowania treściBardzo ważna metryka, ważniejsza niż CWV dla indeksowania

❌ Czego unikać

AntywzorzecDlaczego nie działa
Lazy loading przez scroll eventScroll nigdy nie występuje w WRS, chociaż są wyjątki
Infinite scroll bez fallbackuBez scroll event kolejne strony się nie załadują
Treść ładowana z opóźnieniem >5sJavaScript zostaje zatrzymany przed jej wyrenderowaniem
height: 100vh bez ograniczeniaPo ekspansji element ma wysokość całego dokumentu
Detekcja bota przez scroll behaviorBot 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 🙂

  1. Intersection Observer pozwala deweloperom na łatwe i wydajne wykrywanie, kiedy element DOM przechodzi przez określone „obszary widoczności” ↩︎
  2. 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). ↩︎

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *