1. Zanim zaczniesz
Z tego ćwiczenia dowiesz się, jak dodać błyskawiczną nawigację i płynne przejścia między stronami do przykładowej aplikacji internetowej za pomocą najnowszych interfejsów API, które są natywnie obsługiwane przez Google Chrome.
Przykładowa aplikacja internetowa sprawdza wartości odżywcze popularnych owoców i warzyw. Strony fruit-list i fruit-details są zbudowane jako aplikacja jednostronicowa (SPA), a strony vegetable-list i vegetable-details jako tradycyjna aplikacja wielostronicowa (MPA).

W szczególności implementujesz renderowanie wstępne, pamięć podręczną stanu strony internetowej (bfcache) i prywatny serwer proxy do wstępnego pobierania, aby zapewnić natychmiastową nawigację, oraz przejścia elementów głównych lub wspólnych, aby zapewnić płynne przejścia między stronami. W przypadku stron MPA wdrażasz renderowanie wstępne i pamięć podręczną stanu strony internetowej, a w przypadku aplikacji jednostronicowych – przejścia elementów wspólnych.
Szybkość działania witryny jest zawsze ważnym aspektem wygody użytkowników, dlatego wprowadziliśmy Core Web Vitals, czyli zestaw wskaźników, które mierzą wydajność wczytywania, interaktywność i stabilność wizualną stron internetowych, aby ocenić rzeczywiste wrażenia użytkowników. Najnowsze interfejsy API pomagają poprawić wyniki Core Web Vitals w przypadku Twojej witryny, zwłaszcza w zakresie wydajności ładowania.

Wersja demonstracyjna od Mindvalley
Użytkownicy są też przyzwyczajeni do używania przejść, które sprawiają, że nawigacja i zmiany stanu w natywnych aplikacjach mobilnych są bardzo intuicyjne. Niestety odtworzenie takich doświadczeń użytkownika w internecie nie jest proste. Podobne efekty można uzyskać za pomocą obecnych interfejsów API platformy internetowej, ale ich opracowanie może być zbyt trudne lub złożone, zwłaszcza w porównaniu z odpowiednikami w aplikacjach na Androida lub iOS. Interfejsy API Seamless mają na celu wypełnienie tej luki w zakresie wrażeń użytkowników i deweloperów między aplikacją a internetem.

Wersje demonstracyjne od pixiv i Tokopedia
Wymagania wstępne
Znajomość:
- HTML
- CSS
- JavaScript
- Narzędzia deweloperskie w Chrome
Czego się dowiesz:
Jak to zrobić:
- Renderowanie wstępne
- bfcache
- Prywatny serwer proxy pobierania wstępnego
- Przejścia elementów głównych lub udostępnionych
Co utworzysz
Przykładowa aplikacja internetowa utworzona za pomocą Next.js, która korzysta z najnowszych funkcji przeglądarki zapewniających natychmiastowe i płynne działanie:
- Niemal natychmiastowa nawigacja dzięki wstępnemu renderowaniu
- pamięć podręczna stanu strony internetowej, która umożliwia błyskawiczne wczytywanie za pomocą przycisków Wstecz i Dalej w przeglądarce;
- Dobre pierwsze wrażenie dzięki nawigacji między domenami z użyciem prywatnego serwera proxy do wstępnego pobierania lub technologii Signed Exchange (SXG)
- Płynne przechodzenie między stronami dzięki przejściu elementów głównych lub udostępnionych
Czego potrzebujesz
- Chrome w wersji 101 lub nowszej
2. Rozpocznij
Włączanie flag Chrome
- Otwórz about://flags, a następnie włącz flagi środowiska wykonawczego
Prerender2idocumentTransition API. - Ponownie uruchom przeglądarkę.
Pobierz kod
- Otwórz kod z tego repozytorium GitHub w ulubionym środowisku programistycznym:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
- Zainstaluj zależności wymagane do uruchomienia serwera:
npm install
- Uruchom serwer na porcie 3000:
npm run dev
- W przeglądarce otwórz adres http://localhost:3000.
Teraz możesz edytować i ulepszać aplikację. Za każdym razem, gdy wprowadzisz zmiany, aplikacja zostanie ponownie załadowana i będą one od razu widoczne.
3. Integracja renderowania wstępnego
Na potrzeby tej wersji demonstracyjnej czas wczytywania strony vegetable-details w przykładowej aplikacji jest bardzo długi ze względu na arbitralne opóźnienie po stronie serwera. Dzięki renderowaniu wstępnemu możesz wyeliminować ten czas oczekiwania.
Aby dodać przyciski wstępnego renderowania do strony vegetable-list i umożliwić im wywoływanie wstępnego renderowania po kliknięciu przez użytkownika:
- Utwórz komponent przycisku, który dynamicznie wstawia tag skryptu speculation-rules:
components/prerender-button.js
import { useContext } from 'react'
import ResourceContext from './resource-context'
// You use resource context to manage global states.
// In the PrerenderButton component, you update the prerenderURL parameter when the button is clicked.
export default function PrerenderButton() {
const { dispatch } = useContext(ResourceContext)
const handleClick = (e) => {
e.preventDefault()
e.stopPropagation()
const parent = e.target.closest('a')
if (!parent) {
return
}
const href = parent.getAttribute('href')
dispatch({ type: 'update', prerenderURL: href })
}
return (
<button className='ml-auto bg-gray-200 hover:bg-gray-300 px-4 rounded' onClick={handleClick}>
Prerender
</button>
)
}
- Zaimportuj komponent
PrerenderButtonw plikulist-item.js.
components/list-item.js
// Codelab: Add a PrerenderButton component.
import PrerenderButton from './prerender-button'
...
function ListItemForMPA({ item, href }) {
return (
<a href={href} className='block flex items-center'>
<Icon src={item.image} />
<div className='text-xl'>{item.name}</div>
{/* Codelab: Add PrerenderButton component. */}
<PrerenderButton />
</a>
)
}
- Utwórz komponent, aby dodać interfejs Speculation Rules API.
Komponent SpeculationRules dynamicznie wstawia tag skryptu na stronę, gdy aplikacja aktualizuje stan prerenderURL.
components/speculationrules.js
import Script from 'next/script'
import { useContext, useMemo } from 'react'
import ResourceContext from './resource-context'
export default function SpeculationRules() {
const { state } = useContext(ResourceContext)
const { prerenderURL } = state
return useMemo(() => {
return (
<>
{prerenderURL && (
<Script id='speculationrules' type='speculationrules'>
{`
{
"prerender":[
{
"source": "list",
"urls": ["${prerenderURL}"]
}
]
}
`}
</Script>
)}
</>
)
}, [prerenderURL])
}
- Zintegruj komponenty z aplikacją.
pages/_app.js
// Codelab: Add the SpeculationRules component.
import SpeculationRules from '../components/speculationrules'
function MyApp({ Component, pageProps }) {
useAnalyticsForSPA()
return (
<ResourceContextProvider>
<Layout>
<Component {...pageProps} />
</Layout>
{/* Codelab: Add SpeculationRules component */}
<SpeculationRules />
<Script id='analytics-for-mpa' strategy='beforeInteractive' src='/analytics.js' />
</ResourceContextProvider>
)
}
export default MyApp
- Kliknij Wstępne renderowanie.
Teraz możesz zobaczyć znaczną poprawę szybkości wczytywania. W rzeczywistym przypadku użycia renderowanie wstępne jest wywoływane w przypadku strony, którą użytkownik prawdopodobnie odwiedzi w następnej kolejności, na podstawie pewnych heurystyk.

Analytics
Domyślnie plik analytics.js w przykładowej aplikacji internetowej wysyła zdarzenie wyświetlenia strony, gdy wystąpi zdarzenie DOMContentLoaded. Nie jest to jednak mądre rozwiązanie, ponieważ to zdarzenie jest wywoływane w fazie wstępnego renderowania.
Aby wprowadzić zdarzenie document.prerendering i prerenderingchange i rozwiązać ten problem:
- Zmodyfikuj plik
analytics.js:
public/analytics.js
const sendEvent = (type = 'pageview') => {
// Codelab: Make analytics prerendering compatible.
// The pageshow event could happen in the prerendered page before activation.
// The prerendered page should be handled by the prerenderingchange event.
if (document.prerendering) {
return
}
console.log(`Send ${type} event for MPA navigation.`)
fetch(`/api/analytics?from=${encodeURIComponent(location.pathname)}&type=${type}`)
}
...
// Codelab: Make analytics prerendering compatible.
// The prerenderingchange event is triggered when the page is activated.
document.addEventListener('prerenderingchange', () => {
console.log('The prerendered page was activated.')
sendEvent()
})
...
Świetnie, udało Ci się zmodyfikować statystyki, aby były zgodne z wstępnym renderowaniem. W konsoli przeglądarki możesz teraz wyświetlać dzienniki wyświetleń stron z prawidłowym czasem.
4. Usuwanie blokad bfcache
Usuń moduł obsługi zdarzeń unload.
Niepotrzebne zdarzenie unload to bardzo częsty błąd, którego nie zalecamy. Nie tylko uniemożliwia działanie pamięci podręcznej stanu strony internetowej, ale też jest zawodne. Na przykład nie zawsze jest on uruchamiany na urządzeniach mobilnych i w Safari.
Zamiast zdarzenia unload używasz zdarzenia pagehide, które jest wywoływane we wszystkich przypadkach, w których wywoływane jest zdarzenie unload, oraz gdy strona jest umieszczana w pamięci podręcznej typu „wstecz/dalej”.
Aby usunąć procedurę obsługi zdarzeń unload:
- W pliku
analytics.jszastąp kod obsługi zdarzeniaunloadkodem obsługi zdarzeniapagehide:
public/analytics.js
// Codelab: Remove the unload event handler for bfcache.
// The unload event handler prevents the content from being stored in bfcache. Use the pagehide event instead.
window.addEventListener('pagehide', () => {
sendEvent('leave')
})
Aktualizowanie nagłówka cache-control
Strony wyświetlane z nagłówkiem HTTP Cache-control: no-store nie korzystają z funkcji bfcache przeglądarki, dlatego warto używać tego nagłówka oszczędnie. W szczególności, jeśli strona nie zawiera spersonalizowanych ani krytycznych informacji, takich jak stan zalogowania, prawdopodobnie nie musisz wyświetlać jej z nagłówkiem HTTP Cache-control: no-store.
Aby zaktualizować nagłówek cache-control w przykładowej aplikacji:
- Zmodyfikuj kod
getServerSideProps:
pages/vegetables/index.js
export const getServerSideProps = middleware(async (ctx) => {
const { req, res } = ctx
// Codelab: Modify the cache-control header.
res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59')
...
pages/vegetables/[name].js
export const getServerSideProps = middleware(async (ctx) => {
const { req, res, query } = ctx
// Codelab: Modify the cache-control header.
res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59')
...
Sprawdzanie, czy strona została przywrócona z pamięci podręcznej stanu strony internetowej
Zdarzenie pageshow jest wywoływane bezpośrednio po zdarzeniu load podczas początkowego wczytywania strony i za każdym razem, gdy strona jest przywracana z pamięci podręcznej stanu strony internetowej. Zdarzenie pageshow ma właściwość persisted, która przyjmuje wartość true, jeśli strona została przywrócona z pamięci podręcznej typu „wstecz/do przodu”, a wartość false, jeśli nie. Właściwość persisted umożliwia odróżnienie zwykłego wczytywania strony od przywracania z pamięci podręcznej stanu strony internetowej. Większość głównych usług analitycznych powinna rozpoznawać bfcache, ale możesz sprawdzić, czy strona została przywrócona z tej pamięci, i ręcznie wysyłać zdarzenia.
Aby sprawdzić, czy strona została przywrócona z pamięci podręcznej typu „wstecz/do przodu”:
- Dodaj ten kod do pliku
analytics.js.
public/analytics.js
// Codelab: Use the pageshow event handler for bfcache.
window.addEventListener('pageshow', (e) => {
// If the persisted flag exists, the page was restored from bfcache.
if (e.persisted) {
console.log('The page was restored from bfcache.')
sendEvent()
}
})
Debugowanie strony internetowej
Narzędzia deweloperskie w Chrome mogą pomóc Ci przetestować strony, aby upewnić się, że są zoptymalizowane pod kątem pamięci podręcznej typu „wstecz/do przodu”, i wykryć problemy, które mogą uniemożliwiać ich używanie.
Aby przetestować konkretną stronę:
- Otwórz stronę w Chrome.
- W Narzędziach deweloperskich w Chrome kliknij Aplikacja > Pamięć podręczna „wstecz-dalej” > Uruchom test.
Narzędzia deweloperskie w Chrome próbują przejść do innej strony, a potem wrócić, aby sprawdzić, czy można przywrócić stronę z pamięci podręcznej stanu strony internetowej.

Jeśli operacja się uda, w panelu pojawi się informacja, że strona została przywrócona z pamięci podręcznej stanu strony internetowej:

Jeśli przywracanie się nie powiedzie, w panelu pojawi się informacja, że strona nie została przywrócona, wraz z przyczyną. Jeśli przyczyną jest coś, co możesz rozwiązać jako deweloper, panel również Cię o tym poinformuje.

5. Włącz pobieranie z wyprzedzeniem w wielu witrynach
Wstępne pobieranie rozpoczyna pobieranie wcześniej, dzięki czemu bajty są już w przeglądarce, gdy użytkownik przechodzi do innej strony, co przyspiesza nawigację. To prosty sposób na poprawę Core Web Vitals i zrównoważenie części aktywności sieciowej przed nawigacją. Bezpośrednio przyspiesza to największe wyrenderowanie treści (LCP) i zwiększa zakres opóźnienia przy pierwszym działaniu (FID) oraz skumulowanego przesunięcia układu (CLS) podczas nawigacji.
Prywatny serwer proxy wstępnego pobierania umożliwia wstępne pobieranie danych z różnych witryn, ale nie ujawnia serwerowi docelowemu informacji prywatnych o użytkowniku.

Włączanie pobierania z wyprzedzeniem w różnych witrynach za pomocą prywatnego serwera proxy do pobierania z wyprzedzeniem
Właściciele witryn zachowują kontrolę nad wstępnym pobieraniem za pomocą dobrze znanego zasobu traffic-advice, analogicznego do /robots.txt w przypadku robotów indeksujących, który umożliwia serwerowi HTTP zadeklarowanie, że implementujące agenty powinny stosować odpowiednie zalecenia. Obecnie właściciele witryn mogą poinformować agenta, że nie zezwalają na połączenia sieciowe lub że mają być one ograniczane. W przyszłości możemy dodać inne porady.
Aby hostować zasób z informacjami o ruchu:
- Dodaj ten plik podobny do JSON:
public/.well-known/traffic-advice
[
{
"user_agent": "prefetch-proxy",
"google_prefetch_proxy_eap": {
"fraction": 1
}
}
]
Pole google_prefetch_proxy_eap to pole specjalne dla programu wcześniejszego dostępu, a pole fraction służy do kontrolowania ułamka żądanych wstępnych pobrań, które wysyła prywatny serwer proxy wstępnego pobierania.
Informacje o ruchu powinny być zwracane z typem MIME application/trafficadvice+json.
- W pliku
next.config.jsskonfiguruj nagłówek odpowiedzi:
next.config.js
const nextConfig = {
// Codelab: Modify content-type for traffic advice file.
async headers() {
return [
{
source: '/.well-known/traffic-advice',
headers: [
{
key: 'Content-Type',
value: 'application/trafficadvice+json',
},
],
},
]
},
}
module.exports = nextConfig
6. Integrowanie interfejsu Shared Element Transitions API
Gdy użytkownik przechodzi w internecie z jednej strony na drugą, wyświetlane treści zmieniają się nagle i nieoczekiwanie – pierwsza strona znika, a pojawia się nowa. Ta sekwencyjna, odłączona ścieżka użytkownika jest dezorientująca i powoduje większe obciążenie poznawcze, ponieważ użytkownik musi sam ustalić, jak dotarł do danego miejsca. Dodatkowo zwiększa to postrzeganie przez użytkowników szybkości wczytywania strony podczas oczekiwania na załadowanie docelowego miejsca.
Płynne animacje wczytywania zmniejszają obciążenie poznawcze, ponieważ użytkownicy pozostają w kontekście podczas przechodzenia między stronami, a także zmniejszają odczuwalne opóźnienie wczytywania, ponieważ w tym czasie widzą coś interesującego i przyjemnego. Z tego powodu większość platform udostępnia łatwe w użyciu elementy podstawowe, które umożliwiają deweloperom tworzenie płynnych przejść, np. Android, iOS, macOS i Windows.
Interfejs Shared Element Transitions API zapewnia programistom te same możliwości w internecie, niezależnie od tego, czy przejścia dotyczą różnych dokumentów (MPA) czy jednego dokumentu (SPA).

Wersje demonstracyjne od pixiv i Tokopedia
Aby zintegrować interfejs Shared Element Transitions API z częścią SPA przykładowej aplikacji:
- Utwórz niestandardowy hook do zarządzania przejściem w pliku
use-page-transition.js:
utils/use-page-transition.js
import { useEffect, useContext, useRef, useCallback } from 'react'
import ResourceContext from '../components/resource-context'
// Call this hook on this first page before you start the page transition. For Shared Element Transitions, you need to call the transition.start() method before the next page begins to render, and you need to do the Document Object Model (DOM) modification or setting of new shared elements inside the callback so that this hook returns the promise and defers to the callback resolve.
export const usePageTransitionPrep = () => {
const { dispatch } = useContext(ResourceContext)
return (elm) => {
const sharedElements = elm.querySelectorAll('.shared-element')
// Feature detection
if (!document.createDocumentTransition) {
return null
}
return new Promise((resolve) => {
const transition = document.createDocumentTransition()
Array.from(sharedElements).forEach((elm, idx) => {
transition.setElement(elm, `target-${idx}`)
})
transition.start(async () => {
resolve()
await new Promise((resolver) => {
dispatch({ type: 'update', transition: { transition, resolver } })
})
})
})
}
}
// Call this hook on the second page. Inside the useEffect hook, you can refer to the actual DOM element and set them as shared elements with the transition.setElement() method. When the resolver function is called, the transition is initiated between the captured images and newly set shared elements.
export const usePageTransition = () => {
const { state, dispatch } = useContext(ResourceContext)
const ref = useRef(null)
const setRef = useCallback((node) => {
ref.current = node
}, [])
useEffect(() => {
if (!state.transition || !ref.current) {
return
}
const { transition, resolver } = state.transition
const sharedElements = ref.current.querySelectorAll('.shared-element')
Array.from(sharedElements).forEach((elm, idx) => {
transition.setElement(elm, `target-${idx}`)
})
resolver()
return () => {
dispatch({ type: 'update', transition: null })
}
})
return setRef
}
- Wywołaj niestandardowy hook
usePageTransitionPrep()na stronie z listą, a następnie wywołaj funkcję asynchroniczną, aby wywołać metodętransition.start()w zdarzeniuclick.
W funkcji elementy klasy shared-element są zbierane i rejestrowane jako elementy wspólne.
components/list-item.js
// Codelab: Add the Shared Element Transitions API.
import { usePageTransitionPrep } from '../utils/use-page-transition'
...
function ListItemForSPA({ item, href }) {
// Codelab: Add Shared Element Transitions.
const transitionNextState = usePageTransitionPrep()
const handleClick = async (e) => {
const elm = e.target.closest('a')
await transitionNextState(elm)
}
return (
<Link href={href}>
<a className='block flex items-center' onClick={handleClick}>
<Icon src={item.image} name={item.name} className='shared-element' />
<div className='text-xl'>{item.name}</div>
</a>
</Link>
)
}
- Na stronie z informacjami wywołaj punkt zaczepienia
usePageTransition(), aby zakończyć wywołanie zwrotnetransition.start().
W tym wywołaniu zwrotnym rejestrowane są też udostępnione elementy na stronie szczegółów.
pages/fruits/[name].js
// Codelab: Add the Shared Element Transitions API.
import { usePageTransition } from '../../utils/use-page-transition'
const Item = ({ data }) => {
const { name, image, amountPer, nutrition } = data
// Codelab: Add the Shared Element Transitions API.
const ref = usePageTransition()
return (
<div className={'flex flex-col items-center justify-center py-4 px-4 sm:flex-row'} ref={ref}>
<div className='flex flex-col items-center sm:w-2/4'>
<Image
className='object-cover border-gray-100 border-2 rounded-full shared-element'
src={image}
width='240'
height='240'
alt={`picture of ${name}`}
/>
<h1 className='text-4xl font-bold mt-4'>{name}</h1>
</div>
<div className='sm:w-2/4 w-full'>
<Nutrition amountPer={amountPer} nutrition={nutrition} />
</div>
</div>
)
...
}
Teraz widać, że elementy obrazu są udostępniane na stronach listy i szczegółów oraz płynnie połączone podczas przejścia między stronami. Możesz nawet dostosować animację, aby była bardziej efektowna, za pomocą pseudoelementów CSS.

7. Gratulacje
Gratulacje! Utworzono natychmiastową i płynną aplikację internetową z angażującym i intuicyjnym interfejsem użytkownika.
Więcej informacji
Renderowanie wstępne
- Ulepszone renderowanie wstępne
- Natychmiastowe wczytywanie stron w przeglądarce dzięki spekulacyjnemu renderowaniu wstępnemu
- szybki link
bfcache
Wstępne pobieranie z innych witryn
Signed Exchange
Przejścia elementów głównych lub udostępnionych
- Przejścia między elementami udostępnionymi
- Płynne i proste przejścia między stronami dzięki interfejsowi Shared Element Transitions API
Te interfejsy API są wciąż na wczesnym etapie rozwoju, więc podziel się swoją opinią na stronie crbug.com lub w postaci zgłoszeń w repozytorium Github odpowiednich interfejsów API.