Aggiungi una navigazione istantanea e transizioni di pagina perfette in un'app web

1. Prima di iniziare

Questo codelab ti insegna ad aggiungere navigazione istantanea e transizioni di pagina fluide a un'app web di esempio con le API più recenti supportate in modo nativo da Google Chrome.

L'app web di esempio controlla i valori nutrizionali di frutta e verdura più comuni. Le pagine elenco frutta e dettagli frutta sono create come app a pagina singola (SPA), mentre le pagine elenco verdura e dettagli verdura sono create come app multipagina (MPA) tradizionale.

Screenshot dell'app di esempio su dispositivo mobile Screenshot dell'app di esempio su dispositivo mobile

In particolare, implementi il prerendering, la cache back-forward (bfcache) e il proxy di precaricamento privato per la navigazione istantanea e le transizioni di elementi radice/condivisi per le transizioni di pagina fluide. Implementi il prerendering e la bfcache per le pagine MPA e le transizioni degli elementi condivisi per le pagine SPA.

La velocità del sito è sempre un aspetto importante dell'esperienza utente, motivo per cui Google ha introdotto i Core Web Vitals, un insieme di metriche che misurano le prestazioni di caricamento, l'interattività e la stabilità visiva delle pagine web per valutare l'esperienza utente reale. Le API più recenti ti aiutano a migliorare il punteggio Core Web Vitals del tuo sito web sul campo, in particolare per quanto riguarda il rendimento del caricamento.

l'immagine demo che mostra come la bfcache migliora il tempo di caricamento

Demo di Mindvalley

Gli utenti sono anche abituati all'uso delle transizioni per rendere la navigazione e le modifiche dello stato estremamente intuitive nelle app native mobile. Purtroppo, la replica di queste esperienze utente non è semplice sul web. Anche se potresti ottenere effetti simili con le API della piattaforma web attuali, lo sviluppo potrebbe essere troppo difficile o complesso, soprattutto se confrontato con le funzionalità equivalenti nelle app per Android o iOS. Le API fluide sono progettate per colmare il divario tra l'esperienza utente e quella degli sviluppatori tra app e web.

Demo dell'API Shared Element Transitions di pixiv Demo dell'API Shared Element Transitions di Tokopedia

Demo di pixiv e Tokopedia

Prerequisiti

Conoscenza di:

Cosa imparerai:

Come implementare:

  • Prerendering
  • bfcache
  • Proxy di precaricamento privato
  • Transizioni degli elementi radice/condivisi

Cosa creerai

Un'app web di esempio creata con Next.js e arricchita con le funzionalità del browser più recenti, istantanee e senza interruzioni:

  • Navigazione quasi istantanea con il prerendering
  • bfcache per caricamenti istantanei con i pulsanti Indietro e Avanti del browser
  • Ottime prime impressioni dalla navigazione cross-origin con Private Prefetch Proxy o signed exchange (SXG)
  • Una transizione fluida tra le pagine con la transizione degli elementi radice/condivisi

Che cosa ti serve

  • Chrome versione 101 o successive

2. Inizia

Attivare i flag di Chrome

  1. Vai a about://flags e attiva i flag di runtime Prerender2 e documentTransition API.
  2. Riavvia il browser.

Ottieni il codice

  1. Apri il codice di questo repository GitHub nel tuo ambiente di sviluppo preferito:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
  1. Installa le dipendenze necessarie per eseguire il server:
npm install
  1. Avvia il server sulla porta 3000:
npm run dev
  1. Vai alla pagina http://localhost:3000 nel browser.

Ora puoi modificare e migliorare la tua app. Ogni volta che apporti modifiche, l'app viene ricaricata e le modifiche sono visibili direttamente.

3. Integra il prerendering

Ai fini di questa demo, il tempo di caricamento della pagina dei dettagli delle verdure nell'app di esempio è molto lento a causa di un ritardo arbitrario sul lato server. Puoi eliminare questo tempo di attesa con il prerendering.

Per aggiungere pulsanti di prerendering alla pagina dell'elenco di verdure e attivarli dopo che l'utente fa clic:

  1. Crea un componente pulsante che inserisce dinamicamente il tag di script 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>
  )
}
  1. Importa il componente PrerenderButton nel file 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. Crea un componente per aggiungere l'API Speculation Rules.

Il componente SpeculationRules inserisce dinamicamente un tag di script nella pagina quando l'app aggiorna lo stato 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])
}
  1. Integra i componenti con l'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. Fai clic su Prerender.

Ora puoi notare il miglioramento significativo del caricamento. Nello scenario di utilizzo reale, il prerendering viene attivato per la pagina che l'utente probabilmente visiterà successivamente in base ad alcune euristiche.

Video dimostrativo dell&#39;app di esempio per il prerendering

Analytics

Per impostazione predefinita, il file analytics.js nell'app web di esempio invia un evento visualizzazione di pagina quando si verifica l'evento DOMContentLoaded. Purtroppo, non è consigliabile perché questo evento viene attivato durante la fase di prerendering.

Per introdurre un evento document.prerendering e prerenderingchange per risolvere il problema:

  • Riscrivi il file 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()
  })
  ...

Ottimo, hai modificato correttamente le analisi in modo che siano compatibili con il prerendering. Ora puoi visualizzare i log di visualizzazione di pagina con la tempistica corretta nella console del browser.

4. Rimuovere i blocchi della bfcache

Rimuovi il gestore di eventi unload

Avere un evento unload non necessario è un errore molto comune che non è più consigliato. Non solo impedisce il funzionamento della bfcache, ma non è nemmeno affidabile. Ad esempio, non viene sempre attivato sui dispositivi mobili e su Safari.

Anziché un evento unload, utilizzi l'evento pagehide, che si attiva in tutti i casi in cui si attiva l'evento unload e quando una pagina viene inserita nella bfcache.

Per rimuovere il gestore di eventi unload:

  • Nel file analytics.js, sostituisci il codice del gestore di eventi unload con il codice del gestore di eventi 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')
})

Aggiorna l'intestazione cache-control

Le pagine pubblicate con un'intestazione HTTP Cache-control: no-store non sfruttano la funzionalità bfcache del browser, pertanto è buona norma utilizzare questa intestazione con parsimonia. In particolare, se la pagina non contiene informazioni personalizzate o critiche, come lo stato di accesso, probabilmente non è necessario pubblicarla con l'intestazione HTTP Cache-control: no-store.

Per aggiornare l'intestazione cache-control dell'app di esempio:

  • Modifica il codice 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')
  ...

Determinare se una pagina viene ripristinata dalla bfcache

L'evento pageshow viene attivato subito dopo l'evento load al caricamento iniziale della pagina e ogni volta che la pagina viene ripristinata dalla bfcache. L'evento pageshow ha una proprietà persisted, che è true se la pagina è stata ripristinata dalla bfcache e false in caso contrario. Puoi utilizzare la proprietà persisted per distinguere i caricamenti di pagine regolari dai ripristini della cache back-forward. I principali servizi di analisi dovrebbero essere a conoscenza della bfcache, ma puoi verificare se la pagina viene ripristinata dalla bfcache e inviare gli eventi manualmente.

Per determinare se una pagina viene ripristinata dalla bfcache:

  • Aggiungi questo codice al file 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()
    }
  })

Eseguire il debug di una pagina web

Gli Strumenti per sviluppatori di Chrome possono aiutarti a testare le pagine per assicurarti che siano ottimizzate per la bfcache e identificare eventuali problemi che potrebbero renderle non idonee.

Per testare una pagina specifica:

  1. Vai alla pagina in Chrome.
  2. In Strumenti per sviluppatori di Chrome, fai clic su Applicazione > cache back-forward > Esegui test.

Gli Strumenti per sviluppatori di Chrome tentano di uscire dalla pagina e di tornarci per determinare se è possibile ripristinarla dalla bfcache.

49bf965af35d5324.png

In caso di esito positivo, il riquadro indica che la pagina è stata ripristinata dalla cache back-forward:

47015a0de45f0b0f.png

In caso di esito negativo, il riquadro indica che la pagina non è stata ripristinata e il motivo. Se il motivo è qualcosa che puoi risolvere in qualità di sviluppatore, il riquadro ti informa anche di questo.

dcf0312c3fc378ce.png

5. Attivare il precaricamento cross-site

Il prefetching inizia i recuperi in anticipo in modo che i byte siano già nel browser quando l'utente naviga, il che accelera la navigazione. È un modo semplice per migliorare i Core Web Vitals e compensare parte dell'attività di rete prima della navigazione. Ciò accelera direttamente il Largest Contentful Paint (LCP) e offre più spazio per il First Input Delay (FID) e il Cumulative Layout Shift (CLS) durante la navigazione.

Private Prefetch Proxy consente il prefetch cross-site, ma non rivela informazioni private sull'utente al server di destinazione.

Come funziona il proxy di precaricamento privato

Attivare il prefetching cross-site con il proxy di prefetching privato

I proprietari dei siti web mantengono il controllo del precaricamento tramite una risorsa traffic-advice nota, analoga a /robots.txt per i web crawler, che consente a un server HTTP di dichiarare che gli agenti di implementazione devono applicare il consiglio corrispondente. Al momento, i proprietari dei siti web possono consigliare all'agente di disabilitare o limitare le connessioni di rete. In futuro potrebbero essere aggiunti altri consigli.

Per ospitare una risorsa di consulenza sul traffico:

  1. Aggiungi questo file simile a JSON:

public/.well-known/traffic-advice

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

Il campo google_prefetch_proxy_eap è un campo speciale per il programma di accesso in anteprima, mentre il campo fraction consente di controllare la frazione di precaricamenti richiesti inviati dal proxy di precaricamento privato.

I suggerimenti sul traffico devono essere restituiti con il tipo MIME application/trafficadvice+json.

  1. Nel file next.config.js, configura l'intestazione della risposta:

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. Integra l'API Shared Element Transitions

Quando un utente naviga sul web da una pagina all'altra, i contenuti che vede cambiano improvvisamente e inaspettatamente quando la prima pagina scompare e viene visualizzata la nuova pagina. Questa esperienza utente sequenziale e disconnessa è disorientante e comporta un carico cognitivo più elevato perché l'utente è costretto a ricostruire il percorso che lo ha portato dove si trova. Inoltre, questa esperienza aumenta la percezione del caricamento della pagina da parte degli utenti mentre attendono il caricamento della destinazione desiderata.

Le animazioni di caricamento fluide riducono il carico cognitivo perché gli utenti rimangono nel contesto mentre navigano tra le pagine e riducono la latenza percepita del caricamento perché vedono qualcosa di coinvolgente e piacevole nel frattempo. Per questi motivi, la maggior parte delle piattaforme fornisce primitive facili da usare che consentono agli sviluppatori di creare transizioni fluide, come Android, iOS, macOS e Windows.

L'API Shared Element Transitions offre agli sviluppatori la stessa funzionalità sul web, indipendentemente dal fatto che le transizioni siano tra documenti (MPA) o all'interno di un documento (SPA).

Demo dell&#39;API Shared Element Transitions di pixiv Demo dell&#39;API Shared Element Transitions di Tokopedia

Demo di pixiv e Tokopedia

Per integrare l'API Shared Element Transitions per la parte SPA dell'app di esempio:

  1. Crea un hook personalizzato per gestire la transizione nel file 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
}
  1. Chiama l'hook personalizzato usePageTransitionPrep() nella pagina dell'elenco, quindi chiama la funzione asincrona per attivare il metodo transition.start() all'interno dell'evento click.

All'interno della funzione, gli elementi della classe shared-element vengono raccolti e registrati come elementi condivisi.

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. Nella pagina dei dettagli, chiama l'hook usePageTransition() per completare la funzione di callback transition.start().

In questo callback vengono registrati anche gli elementi condivisi nella pagina dei dettagli.

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

Ora puoi vedere che gli elementi dell'immagine sono condivisi nelle pagine di elenco e di dettagli e sono collegati senza problemi nella transizione di pagina. Puoi anche personalizzare l'animazione per renderla più elaborata con gli pseudo-elementi CSS.

Video dimostrativo dell&#39;app di esempio senza la transizione degli elementi condivisi Video demo dell&#39;app di esempio con transizione degli elementi condivisi

7. Complimenti

Complimenti! Hai creato un'app web istantanea e senza interruzioni con un'esperienza utente coinvolgente, intuitiva e senza attrito.

Scopri di più

Prerendering

bfcache

Prefetching tra siti

Signed Exchange

Transizioni degli elementi radice/condivisi

Queste API sono ancora nelle fasi iniziali di sviluppo, quindi condividi il tuo feedback su crbug.com o come problemi nel repository GitHub delle API pertinenti.