Sofortige Navigation und nahtlose Seitenübergänge zu einer Web-App hinzufügen

1. Hinweis

In diesem Codelab erfahren Sie, wie Sie einer Beispiel-Web-App mit den neuesten, nativ von Google Chrome unterstützten APIs eine sofortige Navigation und nahtlose Seitenübergänge hinzufügen.

Die Beispiel-Web-App prüft die Nährwerte von beliebten Obst- und Gemüsesorten. Die Seiten mit der Obstliste und den Obstdetails werden als Single-Page-App (SPA) erstellt.

Screenshot der Beispiel-App auf einem Mobilgerät Screenshot der Beispiel-App auf einem Mobilgerät

Genauer gesagt implementieren Sie Pre-Rendering, Back-Forward-Cache (bfcache) und privaten Prefetch-Proxy für die sofortige Navigation sowie Übergänge von Stamm-/gemeinsam genutzten Elementen für nahtlose Seitenübergänge. Sie implementieren Pre-Rendering und bfcache für die MPA-Seiten sowie Übergänge mit gemeinsam genutzten Elementen für die SPA-Seiten.

Die Websitegeschwindigkeit ist immer ein wichtiger Aspekt für die Nutzerfreundlichkeit. Deshalb hat Google die Core Web Vitals eingeführt. Diese Messwerte messen die Ladeleistung, Interaktivität und visuelle Stabilität von Webseiten, um die Nutzerfreundlichkeit in der realen Welt zu beurteilen. Mit den neuesten APIs können Sie den Core Web Vitals-Wert Ihrer Website verbessern, insbesondere im Hinblick auf die Ladeleistung.

Demobild, wie bfcache die Ladezeit verbessert

Demo von Mindvalley

Nutzer sind auch an die Verwendung von Übergängen gewöhnt, um Navigationen und Statusänderungen in mobilen nativen Apps äußerst intuitiv zu gestalten. Leider ist die Replikation dieser Nutzererfahrungen im Web nicht ganz einfach. Auch wenn Sie mit den aktuellen Web-Plattform-APIs ähnliche Effekte erzielen können, ist die Entwicklung möglicherweise zu schwierig oder zu komplex, insbesondere im Vergleich zu Funktions-Gegenstücken in Android- oder iOS-Apps. Nahtlose APIs wurden entwickelt, um die Lücke zwischen App und Web zu schließen.

Shared Element Transitions API-Demo von Pixiv Shared Element Transitions API-Demo von Tokopedia

Demos von pixiv und Tokopedia

Vorbereitung

Kenntnisse in:

Die Themen:

Implementierung:

  • Pre-Rendering
  • bfcache
  • Privater Prefetch-Proxy
  • Übergänge von Stamm-/gemeinsam genutzten Elementen

Aufgaben

Eine mit Next.js erstellte Beispiel-Web-App, die mit den neuesten Instant- und nahtlosen Browserfunktionen angereichert ist:

  • Nahezu sofortige Navigation mit Pre-Rendering
  • bfcache zum sofortigen Laden über die Vor- und Zurück-Schaltflächen des Browsers
  • Hervorragende erste Impressionen durch ursprungsübergreifende Navigation mit Private Prefetch Proxy oder Signed Exchange (SXG)
  • Nahtloser Wechsel zwischen Seiten mit Wechsel zwischen Stamm- und gemeinsam genutzten Elementen

Voraussetzungen

  • Chrome-Version 101 oder höher

2. Erste Schritte

Chrome-Flags aktivieren

  1. Rufen Sie about://flags auf und aktivieren Sie die Laufzeit-Flags Prerender2 und documentTransition API.
  2. Starten Sie den Browser neu.

Code abrufen

  1. Öffnen Sie den Code aus diesem GitHub-Repository in Ihrer bevorzugten Entwicklungsumgebung:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
  1. Installieren Sie die Abhängigkeiten, die zum Ausführen des Servers erforderlich sind:
npm install
  1. Starten Sie den Server auf Port 3000:
npm run dev
  1. Rufen Sie im Browser http://localhost:3000 auf.

Du kannst deine App jetzt bearbeiten und verbessern. Wenn Sie Änderungen vornehmen, wird die App neu geladen und Ihre Änderungen sind direkt sichtbar.

3. Pre-Rendering einbinden

Für diese Demo ist die Ladezeit der Seite mit den Gemüsedetails in der Beispiel-App aufgrund einer willkürlichen Verzögerung auf der Serverseite sehr langsam. Diese Wartezeit wird durch das Pre-Rendering vermieden.

So fügen Sie der Seite mit der Gemüseliste Pre-Rendering-Schaltflächen hinzu, die das Pre-Rendering auslösen können, nachdem der Nutzer darauf geklickt hat:

  1. Erstellen Sie eine Schaltflächenkomponente, die das Skript-Tag speculation-rules dynamisch einfügt:

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>
  )
}
  1. Importieren Sie die Komponente PrerenderButton in die Datei list-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>
  )
}
  1. Erstellen Sie eine Komponente, um die Speculation Rules API hinzuzufügen.

Die Komponente „SpeculationRules“ fügt dynamisch ein Script-Tag in die Seite ein, wenn die App den Status „prerenderURL“ aktualisiert.

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])
}
  1. Integrieren Sie die Komponenten in die App.

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
  1. Klicken Sie auf Pre-Rendering.

Jetzt sehen Sie die deutliche Verbesserung der Ladezeiten. Im realen Anwendungsfall wird aufgrund bestimmter Heuristiken das Pre-Rendering für die Seite ausgelöst, die der Nutzer wahrscheinlich als Nächstes besuchen wird.

Demovideo für die Beispiel-App zum Pre-Rendering

Analytics

Standardmäßig sendet die Datei analytics.js in der Beispiel-Web-App ein Seitenaufrufereignis, wenn das DOMContentLoaded-Ereignis eintritt. Das ist leider nicht sinnvoll, da dieses Ereignis während der Pre-Rendering-Phase ausgelöst wird.

So führen Sie ein document.prerendering- und prerenderingchange-Ereignis ein, um dieses Problem zu beheben:

  • Schreiben Sie die Datei analytics.js um:

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()
  })
  ...

Super! Sie haben Ihre Analysen so geändert, dass sie mit Pre-Rendering kompatibel sind. Jetzt können Sie die Seitenaufruf-Logs zum richtigen Zeitpunkt in der Browserkonsole aufrufen.

4. bfcache-Blocker entfernen

unload-Event-Handler entfernen

Unnötige unload-Ereignisse sind ein sehr häufiger Fehler, der nicht mehr empfohlen wird. Es verhindert nicht nur, dass bfcache funktioniert, sondern ist auch unzuverlässig. Sie wird beispielsweise nicht immer auf Mobilgeräten und Safari ausgelöst.

Anstelle eines unload-Ereignisses verwenden Sie das pagehide-Ereignis, das in allen Fällen ausgelöst wird, wenn das unload-Ereignis ausgelöst und eine Seite in den bfcache abgelegt wird.

So entfernen Sie den unload-Event-Handler:

  • Ersetzen Sie in der Datei analytics.js den Code für den Event-Handler unload durch den Code für den Event-Handler pagehide:

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')
})

Cache-Control-Header aktualisieren

Seiten, die mit einem Cache-control: no-store-HTTP-Header bereitgestellt werden, profitieren nicht von der bfcache-Funktion des Browsers. Daher empfiehlt es sich, diesen Header sparsam zu verwenden. Wenn die Seite keine personalisierten oder wichtigen Informationen enthält, wie z. B. den Status „Angemeldet“, musst du sie wahrscheinlich nicht mit dem HTTP-Header Cache-control: no-store bereitstellen.

So aktualisieren Sie den Cache-Control-Header der Beispielanwendung:

  • Ändern Sie den getServerSideProps-Code:

/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')
  ...

seiten/gemüse/[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')
  ...

Bestimmen, ob eine Seite aus bfcache wiederhergestellt wird

Das pageshow-Ereignis wird direkt nach dem load-Ereignis ausgelöst, wenn die Seite erstmals geladen wird und wenn die Seite aus bfcache wiederhergestellt wird. Das pageshow-Ereignis hat eine persisted-Eigenschaft. Der Wert ist „true“, wenn die Seite aus dem bfcache wiederhergestellt wurde, und „false“, wenn dies nicht der Fall war. Mit dem Attribut persisted können Sie zwischen regulären Seitenladevorgängen und bfCache-Wiederherstellungen unterscheiden. Wichtige Analysedienste sollten bfcache kennen. Sie können jedoch prüfen, ob die Seite aus bfcache wiederhergestellt wird, und Ereignisse manuell senden.

So stellen Sie fest, ob eine Seite aus dem bfcache wiederhergestellt wird:

  • Fügen Sie diesen Code in die Datei analytics.js ein.

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()
    }
  })

Fehler auf einer Webseite beheben

Mit den Chrome-Entwicklertools können Sie Ihre Seiten testen, um sicherzustellen, dass sie für den bfcache optimiert sind. Außerdem können Sie Probleme identifizieren, die dazu führen könnten, dass sie nicht zulässig sind.

So testen Sie eine bestimmte Seite:

  1. Rufen Sie die Seite in Chrome auf.
  2. Klicken Sie in den Chrome-Entwicklertools auf Application (Anwendung) > Back-Forward-Cache > Test ausführen.

Die Chrome-Entwicklertools versuchen, eine Seite zu verlassen und dann wieder zurückzugehen, um festzustellen, ob die Seite aus dem bfcache wiederhergestellt werden konnte.

49bf965af35d5324.png

Wenn der Vorgang erfolgreich war, werden Sie im Steuerfeld darüber informiert, dass die Seite aus dem Back-Forward-Cache wiederhergestellt wurde:

47015a0de45f0b0f.png

Falls der Vorgang nicht erfolgreich war, werden Sie im Steuerfeld darüber informiert, dass die Seite nicht wiederhergestellt wurde, sowie den Grund dafür. Wenn Sie als Entwickler auf ein Problem eingehen können, werden Sie im Panel ebenfalls informiert.

dcf0312c3fc378ce.png

5. Websiteübergreifenden Vorabruf aktivieren

Beim Vorabruf wird der Abruf vorzeitig gestartet, sodass sich die Bytes bereits im Browser befinden, wenn der Nutzer eine Seite aufruft. Dadurch wird die Navigation beschleunigt. So können Sie ganz einfach Core Web Vitals verbessern und einige Netzwerkaktivitäten vor der Navigation ausgleichen. Dadurch wird der Largest Contentful Paint (LCP) direkt beschleunigt und es ist mehr Spielraum für First Input Delay (FID) und Cumulative Layout Shift (CLS) bei der Navigation.

Der private Prefetch-Proxy aktiviert den websiteübergreifenden Prefetch, gibt aber keine privaten Informationen über den Nutzer an den Zielserver weiter.

Funktionsweise des privaten Prefetch-Proxys

Websiteübergreifenden Vorabruf mit privatem Prefetch-Proxy aktivieren

Websiteinhaber behalten die Kontrolle über den Vorabruf über eine bekannte Traffic-Beratung-Ressource, ähnlich wie /robots.txt für Web-Crawler, bei denen ein HTTP-Server deklarieren kann, dass die Implementierung von Agents den entsprechenden Rat befolgen sollte. Derzeit können Websiteinhaber den Agent anweisen, Netzwerkverbindungen zu unterbinden oder zu drosseln. Zukünftig werden möglicherweise weitere Ratschläge hinzugefügt.

So hosten Sie eine Traffic-Beratungsressource:

  1. Fügen Sie diese JSON-ähnliche Datei hinzu:

public/.well-known/traffic-advice

[
  {
    "user_agent": "prefetch-proxy",
    "google_prefetch_proxy_eap": {
      "fraction": 1
    }
  }
]

Das Feld google_prefetch_proxy_eap ist ein spezielles Feld für das Early Access-Programm und das Feld fraction ist ein Feld, um den Anteil der angeforderten Prefetches zu steuern, die der private Prefetch-Proxy sendet.

Der Traffic-Hinweis sollte mit dem MIME-Typ application/trafficadvice+json zurückgegeben werden.

  1. Konfigurieren Sie in der Datei next.config.js den Antwortheader:

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. Shared Element Transitions API einbinden

Wenn ein Nutzer im Web von einer Seite zur anderen navigiert, ändert sich der Inhalt, den er sieht, plötzlich und unerwartet, da die erste Seite verschwindet und die neue Seite angezeigt wird. Diese aufeinanderfolgende, unterbrochene User Experience verwirft und führt zu einer höheren kognitiven Auslastung, da die Nutzenden gezwungen sind, sich ein Bild davon zu machen, wie sie dorthin gekommen sind, wo sie sind. Außerdem nehmen Nutzer so mehr wahr, dass die Seite geladen wird, während sie auf das Laden des gewünschten Ziels warten.

Smooth Ladeanimationen verringern die kognitive Auslastung, da die Nutzer im Kontext bleiben, während sie zwischen den Seiten navigieren, und verringern die empfundene Latenz des Ladevorgangs, da Nutzer in der Zwischenzeit etwas Interessantes und Ansprechendes sehen. Aus diesen Gründen bieten die meisten Plattformen wie Android, iOS, MacOS und Windows nutzerfreundliche Grundelemente, mit denen Entwickler nahtlose Übergänge erstellen können.

Die Shared Element Transitions API bietet Entwicklern dieselben Funktionen im Web, unabhängig davon, ob es sich um dokumentübergreifende oder dokumentenübergreifende Übergänge (SPA) handelt.

Shared Element Transitions API-Demo von Pixiv Shared Element Transitions API-Demo von Tokopedia

Demos von pixiv und Tokopedia

So integrieren Sie die Shared Element Transitions API für den SPA-Teil der Beispiel-App:

  1. Erstellen Sie einen benutzerdefinierten Hook, um den Übergang in der Datei use-page-transition.js zu verwalten:

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
}
  1. Rufen Sie den benutzerdefinierten usePageTransitionPrep()-Hook auf der Listenseite und dann die asynchrone Funktion auf, um die Methode transition.start() innerhalb des click-Ereignisses auszulösen.

Innerhalb der Funktion werden die shared-element-Klassenelemente erfasst und als gemeinsam genutzte Elemente registriert.

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>
  )
}
  1. Rufen Sie auf der Detailseite den Hook usePageTransition() auf, um die Callback-Funktion transition.start() fertigzustellen.

Bei diesem Callback werden auch gemeinsame Elemente auf der Detailseite registriert.

seiten/fruechte/[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>
  )
...
}

Jetzt können Sie sehen, dass die Bildelemente auf der Listen- und der Detailseite geteilt und beim Seitenübergang nahtlos miteinander verbunden werden. Mit CSS-Pseudoelementen können Sie die Animation sogar noch fantasievoller gestalten.

Demovideo der Beispiel-App ohne Umstellung des geteilten Elements Demovideo der Beispiel-App mit der Umstellung auf geteilte Elemente

7. Glückwunsch

Glückwunsch! Sie haben eine sofortige und nahtlose Webanwendung mit einer unkomplizierten, ansprechenden und intuitiven Benutzeroberfläche erstellt.

Weitere Informationen

Pre-Rendering

bfcache

Websiteübergreifender Vorabruf

Signed Exchanges

Übergänge von Stamm-/gemeinsam genutzten Elementen

Diese APIs befinden sich noch in einer frühen Entwicklungsphase. Bitte senden Sie uns Ihr Feedback unter crbug.com oder als Probleme im GitHub-Repository der entsprechenden APIs.