Spójne przejścia między dokumentami

Problem

Przejścia między widokami dokumentów animują elementy między 2 stronami podczas nawigacji w tej samej domenie. Przeglądarka robi zrzut starej strony, przechodzi na nową stronę, a następnie animuje przejście ze zrzutu do nowej strony. Jeśli nowa strona nie zakończyła wczytywania zasobów krytycznych – arkuszy stylów, skryptów układu lub kluczowych elementów DOM – przejście zostanie animowane do stanu niekompletnego lub bez stylu. Powoduje to błędy wizualne, takie jak zmiana pozycji elementów, przeformatowanie treści w trakcie animacji lub miganie czcionek zastępczych po zakończeniu przejścia.

Rozwiązanie

Użyj blocking="render" w przypadku kluczowych elementów <link> i <script> w sekcji <head> nowej strony oraz <link rel="expect">, aby zablokować renderowanie do czasu przeanalizowania określonych elementów DOM. Dzięki temu przeglądarka nie rozpocznie animacji przejścia widoku, dopóki stan wizualny nowej strony nie będzie stabilny. Przeglądarka nadal analizuje kod HTML w tle – odroczone jest tylko renderowanie.

Strategia wdrażania

  1. OBOWIĄZKOWE: Wyraź zgodę na przejścia między dokumentami za pomocą reguły CSS @view-transition na obu stronach.
  2. OBOWIĄZKOWO: upewnij się, że krytyczne arkusze stylów znajdują się w sekcji <head>. Arkusze stylów w elemencie <head> domyślnie blokują renderowanie. Arkusze stylów wstawiane dynamicznie wymagają jawnego określenia blocking="render".
  3. UŻYWAJ blocking="render" w elementach <script>, które muszą zostać wykonane przed animacją przejścia (np. skrypty, które stosują motyw lub wpływają na układ).
  4. UŻYWAJ <link rel="expect" href="#element-id" blocking="render">, aby blokować renderowanie do momentu przeanalizowania treści widocznych na ekranie bez przewijania. Dotyczy to wszystkich typów przejść: pełnostronicowych przejść z zanikaniem (aby uniknąć animacji na pustej stronie), animacji przekształcania (aby upewnić się, że nazwane elementy istnieją w DOM) i układów zależnych od skryptu (aby upewnić się, że stylizowana treść jest analizowana).
  5. NIE blokuj renderowania treści niekrytycznych. Blokuj tylko zasoby i elementy, które wpływają na początkowy widoczny obszar. Blokowanie zbyt dużej ilości treści opóźnia przejście i pogarsza postrzeganą wydajność.

Przewodnik po implementacji

Krok 1. Wyraź zgodę na przejścia widoków między dokumentami

OBOWIĄZKOWO: zarówno strona źródłowa, jak i strona docelowa muszą zawierać regułę at-rule @view-transition. Bez tego nie nastąpi przejście między dokumentami.

/*
  MANDATORY: Include this rule in every page that participates
  in cross-document view transitions.
  `navigation: auto` enables transitions for standard navigations
  (link clicks, form submissions, back/forward).
*/
@view-transition {
  navigation: auto;
}

/* MANDATORY Copy-Paste Safety: Disable cross-document view transitions for users requesting reduced motion */
@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}

Krok 2. Blokuj renderowanie do czasu wykonania skryptów o znaczeniu krytycznym

Jeśli skrypt nieblokujący w <head> musi zostać uruchomiony przed animacją przejścia (np. aby zastosować klasę motywu lub wpłynąć na układ), oznacz go za pomocą blocking="render". Bez tego skrypty async, defer lub type="module" mogą zostać wykonane po rozpoczęciu przejścia.

<head>
  <!--
    DO: Mark layout-critical scripts with blocking="render".
  -->
  <script type=module blocking="render">
    // Example: apply a stored theme before the page renders,
    // so the transition snapshot reflects the correct theme.
    document.documentElement.dataset.theme =
      localStorage.getItem('theme') || 'light';
  </script>
</head>

Krok 3. Zablokuj renderowanie do czasu przeanalizowania kluczowych elementów DOM

Arkusze stylów i skrypty blocking="render" w sekcji <head> gwarantują tylko, że sekcja <head> została w pełni przetworzona. Nie czekają na przeanalizowanie treści <body>. Bez dodatkowego blokowania przeglądarka może zrobić zrzut nowej strony, zanim elementy widoczne na ekranie bez przewijania pojawią się w DOM. W efekcie animacja przejścia będzie prowadzić do pustej lub częściowo wyrenderowanej strony.

<link rel="expect"> rozwiązuje ten problem, blokując renderowanie do momentu przeanalizowania określonego elementu (identyfikowanego przez id). Wartość href musi być identyfikatorem fragmentu (np. #hero) pasującym do atrybutu id elementu docelowego. Po przeanalizowaniu tagu zamykającego tego elementu blok renderowania zostaje zwolniony. Pamiętaj, że <link rel="expect"> gwarantuje tylko, że docelowy element DOM istnieje w dokumencie – nie czeka na pobranie ani zdekodowanie zewnętrznych zasobów podrzędnych (takich jak piksele <img>).

UŻYWAJ <link rel="expect"> we wszystkich tych sytuacjach:

Przypadek użycia 1. Pełnoekranowe przenikanie

Nawet jeśli żaden z elementów nie ma atrybutu view-transition-name, domyślne przejście root powoduje przenikanie całej strony. Jeśli zrzut nowej strony zostanie wykonany przed przeanalizowaniem treści widocznych na ekranie bez przewijania, animacja przejścia będzie prowadzić ze starej strony do pustej lub niekompletnej strony. Zablokuj renderowanie elementu, który oznacza koniec widocznej zawartości strony widocznej na ekranie.

<head>
  <link rel="stylesheet" href="/css/styles.css">

  <!--
    DO: Block rendering until the main content area is parsed,
    even for a simple cross-fade. Without this, the browser may
    snapshot the page before visible content exists in the DOM,
    causing the cross-fade to reveal a blank or partial page.
  -->
  <link rel="expect" href="#main-content" blocking="render">
</head>
<body>
  <header>...</header>
  <main id="main-content">
    <h1>Page Title</h1>
    <p>Above-the-fold content the user should see immediately.</p>
  </main>
  <!-- Content below the fold does NOT need to be blocked on -->
  <section>...</section>
</body>

Przypadek użycia 2. Animacje przekształcania między określonymi elementami

Gdy elementy na obu stronach mają ten sam view-transition-name, przeglądarka płynnie przekształca je podczas nawigacji. Jeśli element docelowy nie został przeanalizowany, gdy rozpoczyna się przejście, przeglądarka nie może go znaleźć – przekształcenie jest zastępowane osobnymi animacjami wyjścia i wejścia. Blokuj renderowanie do momentu przeanalizowania elementu z atrybutem view-transition-name.

<link rel="expect"> wstrzymuje renderowanie tylko do momentu przeanalizowania docelowego elementu DOM i nie czeka na pobranie ani zdekodowanie danych obrazu, dlatego nieumieszczony w pamięci podręcznej element <img> może zostać zarejestrowany jako pusty prostokąt w migawce nowej strony. Aby zapobiec pustym migawkom lub przesunięciom układu podczas przekształcania obrazów, wstępnie wczytaj lub zdekoduj kluczowe obrazy przejścia (np. za pomocą <link rel="preload" as="image"> lub przez renderowanie wstępne strony docelowej) i podaj wyraźne wartości zastępcze rozmiaru (atrybuty width i height lub CSS aspect-ratio oraz kolor tła obiektu zastępczego).

<head>
  <link rel="stylesheet" href="/css/styles.css">

  <!--
    DO: Preload critical transition images so their pixels are
    available to decode before the new-page snapshot is captured.
    <link rel="expect"> only waits for the DOM element to be parsed,
    NOT for image pixels to download or decode.
  -->
  <link rel="preload" as="image" href="/img/product.webp" fetchpriority="high">

  <!--
    DO: Block rendering until the element participating in the
    morph animation has been parsed. Without this, the browser
    may start the transition before #hero exists, causing the
    morph to degrade to a fade-out/fade-in.
  -->
  <link rel="expect" href="#hero" blocking="render">

  <!--
    When multiple blocking="render" resources are present,
    rendering is blocked until ALL of them are satisfied.
    Here, the browser waits for both the script to execute
    AND the #hero element to be parsed — whichever comes last.
  -->
  <script async blocking="render" src="/js/transition-setup.js"></script>
</head>
<body>
  <header>...</header>
  <section id="hero">
    <h1 style="view-transition-name: page-title">Product Name</h1>
    <!--
      DO: Provide explicit dimensions (width/height or aspect-ratio)
      as a sizing fallback so the snapshot captures the correct layout
      box even if image decoding is still in progress.
    -->
    <img
      style="view-transition-name: hero-image"
      src="/img/product.webp"
      width="800"
      height="600"
      alt="Product"
    >
  </section>
</body>

Krok 4. Użyj zapytań o media do blokowania renderowania responsywnego

W zależności od rozmiaru widocznego obszaru nad częścią widoczną na ekranie może być wyświetlana różna ilość treści. Użyj atrybutu media w tagu <link rel="expect">, aby zablokować renderowanie tylko w przypadku treści widocznych przy danej szerokości widocznego obszaru.

<head>
  <!--
    DO: Use media queries to conditionally block rendering.
    On wide screens, both the hero and the sidebar are visible,
    so block until both are parsed. On narrow screens, only the
    hero is visible initially.
  -->
  <link
    rel="expect"
    href="#hero"
    blocking="render"
    media="screen and (width <= 768px)"
  >
  <link
    rel="expect"
    href="#sidebar"
    blocking="render"
    media="screen and (width > 768px)"
  >
</head>

Krok 5. Użyj pagereveal do przejść zależnych od kontekstu (opcjonalnie)

Zdarzenie pagereveal nie jest wymagane w przypadku podstawowej strategii blokowania renderowania. Jest on potrzebny tylko wtedy, gdy wartości view-transition-name muszą być przypisywane dynamicznie na podstawie miejsca, z którego użytkownik przeszedł – na przykład przekształcanie konkretnego elementu listy w nagłówek strony z informacjami.

Jeśli wartości view-transition-name są przypisane statycznie w CSS lub używasz tylko domyślnego efektu przenikania na całej stronie, pomiń ten krok.

<head>
  <!--
    MANDATORY: The pagereveal listener must be registered before
    the page renders. Inline small setup scripts with blocking="render"
    to avoid an extra network round-trip on cold loads, or use an
    external async script with blocking="render" so the listener is
    registered early without blocking HTML parsing.
    If the listener is registered too late (e.g., in a deferred
    script), the event may have already fired.
  -->
  <script async blocking="render" src="/js/transition-setup.js"></script>
</head>
// transition-setup.js
window.addEventListener('pagereveal', async (event) => {
  if (!event.viewTransition) return;

  const from = navigation.activation?.from;
  if (!from) return;

  const fromUrl = new URL(from.url);

  // DO: Assign view-transition-name based on navigation context.
  // This enables a morph animation from the product card on the
  // list page to the heading on the detail page.
  if (fromUrl.pathname === '/products/') {
    const heading = document.querySelector('main h1');
    if (!heading) return;

    heading.style.viewTransitionName = 'product-title';

    try {
      await event.viewTransition.ready;
    } catch (error) {
      // Skipped view transitions reject `ready` with InvalidStateError.
      if (error.name !== 'InvalidStateError') {
        throw error;
      }
    } finally {
      // MANDATORY: Remove the temporary name after the transition
      // finishes. Stale names interfere with subsequent navigations
      // and prevent the page from entering the bfcache.
      await event.viewTransition.finished;
      heading.style.viewTransitionName = '';
    }
  }
});

Sprawdzone metody

  • TAK – jeśli to możliwe, przypisuj view-transition-name za pomocą CSS. Przypisanie JavaScriptu (za pomocą pagereveal) zachowaj na przypadki, gdy nazwa zależy od kontekstu nawigacji.
  • ZALECANE: skrypty blokujące renderowanie powinny być małe i szybkie. Przeglądarka ma wbudowany limit czasu (około 4 sekund), po którym przejście jest całkowicie pomijane (odrzucenie viewTransition.ready z InvalidStateError, jeśli przejście zostało zainicjowane).
  • NIE używaj <link rel="expect"> do blokowania elementów znajdujących się głęboko na stronie, które nie są widoczne w początkowym obszarze widoku. Opóźnia to przejście bez żadnych korzyści wizualnych.
  • NIE oznaczaj za pomocą blocking="render" modułów do wczytywania narzędzi analitycznych, sieci reklamowych, menedżerów tagów ani innych narzędzi firm zewnętrznych, nawet jeśli wstrzykują one treści do początkowego widocznego obszaru. Opóźnienie sieciowe jest wliczane do limitu czasu przejścia, a ich dane wyjściowe nie są częścią stabilnego stanu strony.
  • NIE przypisuj tego samego atrybutu view-transition-name do wielu elementów na tej samej stronie. Zduplikowane nazwy powodują pominięcie całego przejścia.
  • Wpływ na czas działania technologii wspomagających osoby z niepełnosprawnością: użycie blocking="render" opóźnia aktualizacje wizualne i początkowe renderowanie. Zapobiega to błędom wizualnym u użytkowników widzących, ale może powodować opóźnienia w przetwarzaniu lub odroczoną inicjalizację czytników ekranu i innych technologii wspomagających osoby z niepełnosprawnością, które zależą od wyrenderowanych drzew ułatwień dostępu. Porównaj korzyści związane z ciągłością wizualną z początkowym opóźnieniem odczytu dla użytkowników, którzy nie widzą, i upewnij się, że skrypty blokujące renderowanie są minimalne i bardzo zoptymalizowane.

Strategie zastępcze

Przejścia między dokumentami to doskonały kandydat do progresywnego ulepszania. W przeglądarkach, które nie obsługują tych reguł, reguła @view-transition jest ignorowana, a standardowe nawigacje w ramach tej samej domeny odbywają się dokładnie tak, jakby tej funkcji nie było. W obsługiwanych przeglądarkach przejścia są płynne, a w pozostałych – standardowe. Ograniczona obsługa przeglądarek nie jest powodem, dla którego nie należy wdrażać tej technologii.

Wszystkie przeglądarki, które obsługują przejścia między dokumentami, obsługują też blocking="render" i <link rel="expect">, więc nie jest potrzebne osobne rozwiązanie zastępcze dla funkcji blokujących renderowanie opisanych w tym przewodniku.

Inne rzeczy, które warto wziąć pod uwagę

  1. Wpływ na wydajność: każdy zasób blokujący renderowanie opóźnia rozpoczęcie animacji przejścia widoku. Ogranicz liczbę skryptów blokujących renderowanie i używaj <link rel="expect"> tylko w przypadku elementów widocznych na ekranie bez przewijania. Wstępnie renderuj strony docelowe za pomocą interfejsu Speculation Rules API, aby całkowicie wyeliminować opóźnienia wczytywania.
  2. Zachowanie po przekroczeniu limitu czasu: jeśli łączny czas blokowania renderowania przekracza około 4 sekundy, przeglądarka pomija przejście (a wszystkie aktywne obietnice viewTransition.ready są odrzucane z błędem InvalidStateError). Upewnij się, że zasoby krytyczne wczytują się w tym oknie.
  3. Zgodność z bfcache: tymczasowe przypisania view-transition-name, które nie zostaną wyczyszczone po przejściu, mogą uniemożliwić stronie korzystanie z bfcache. Zawsze usuwaj dynamicznie przypisane nazwy w wywołaniu zwrotnym finished.