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

1. Prima di iniziare

Questo codelab ti insegna come aggiungere una navigazione istantanea e transizioni di pagina perfette 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 della lista della frutta e dei dettagli della frutta sono costruite come un'app a pagina singola (SPA), mentre le pagine dell'elenco di ortaggi e dei dettagli della frutta sono realizzate come un'app tradizionale a più pagine (MPA).

Lo screenshot dell'app di esempio su dispositivo mobile Lo screenshot dell'app di esempio su dispositivo mobile

In particolare, implementerai il prerendering, la cache back/forward (bfcache) e il proxy di precaricamento privato per la navigazione istantanea e le transizioni di elementi principali/condivisi per le transizioni di pagina senza interruzioni. Vengono implementati il prerendering e la memorizzazione nella cache 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 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 le prestazioni di caricamento.

l'immagine demo di come bfcache migliora il tempo di caricamento

Demo di Mindvalley

Gli utenti sono inoltre abituati a usare le transizioni per rendere le navigazioni e le modifiche di stato estremamente intuitive nelle app native per dispositivi mobili. Purtroppo, la replica di queste esperienze utente non è semplice sul web. Sebbene sia possibile ottenere effetti simili con le attuali API web, lo sviluppo potrebbe essere troppo difficile o complesso, soprattutto se confrontato con le controparti delle funzionalità nelle app per Android o iOS. Le API senza interruzioni sono progettate per colmare questo divario nell'esperienza utente e sviluppatore tra le app e il web.

Demo dell'API Shared Element Transiziones da pixiv Demo dell'API Shared Element Transiziones di Tokopedia

Demo da pixiv e Tokopedia

Prerequisiti

Conoscenza di:

Cosa imparerai:

Come implementare:

  • Prerendering
  • bfcache
  • Proxy di precaricamento privato
  • Transizioni di elementi condivisi/principali

Cosa creerai

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

  • Navigazione quasi istantanea con prerendering
  • bfcache per caricamenti istantanei con i pulsanti Avanti e Indietro del browser
  • Ottime prime impressioni dalla navigazione multiorigine con il proxy di precaricamento privato o Signed Exchange (SXG)
  • Una transizione perfetta tra le pagine con la transizione degli elementi principali/condivisi

Che cosa ti serve

  • Chrome 101 o versioni successive

2. Inizia

Abilita i flag di Chrome

  1. Vai all'indirizzo about://flags e abilita i flag di runtime Prerender2 e documentTransition API.
  2. Riavvia il browser.

Ottieni il codice

  1. Apri il codice da 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 a http://localhost:3000 nel tuo browser.

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

3. Integra il prerendering

Ai fini di questa demo, il tempo di caricamento della pagina dei dettagli vegetali nell'app di esempio è molto lento a causa di un ritardo arbitrario sul lato server. Questo tempo di attesa viene eliminato con il prerendering.

Per aggiungere pulsanti di prerendering alla pagina dell'elenco vegetale e consentire loro di attivare il prerendering dopo il clic dell'utente:

  1. Crea un componente del pulsante che inserisce dinamicamente il tag script delle regole di speculazione:

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

pagine/_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 Prerendering.

Ora puoi vedere il miglioramento significativo del caricamento. Nel caso d'uso reale, viene attivato il prerendering per la pagina che è probabile che l'utente visiti successivamente in base a una serie di 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 di visualizzazione di pagina quando si verifica l'evento DOMContentLoaded. Purtroppo, non è una buona strategia perché questo evento si attiva durante la fase di prerendering.

Per introdurre un evento document.prerendering e prerenderingchange in modo da 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()
  })
  ...

Fantastico, hai modificato correttamente i tuoi dati e analisi in modo che siano compatibili con il prerendering. Ora puoi vedere i log delle visualizzazioni di pagina con la tempistica corretta nella console del browser.

4. Rimuovi i blocchi bfcache

Rimuovi il gestore di eventi unload

Avere un evento unload non necessario è un errore molto comune e non più consigliato. Non solo impedisce il funzionamento di bfcache, ma è anche inaffidabile. Ad esempio, non sempre si attiva sui dispositivi mobili e su Safari.

Anziché un evento unload, puoi utilizzare l'evento pagehide, che si attiva in tutti i casi quando viene attivato l'evento unload e quando una pagina viene inserita nella cache 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 beneficiano della funzionalità bfcache del browser, pertanto è consigliabile non utilizzare questa intestazione. In particolare, se la pagina non contiene informazioni personalizzate o critiche, ad esempio 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:

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

pagine/verdure/[nome].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')
  ...

Determina se una pagina viene ripristinata da bfcache

L'evento pageshow viene attivato subito dopo l'evento load quando la pagina viene caricata inizialmente e ogni volta che la pagina viene ripristinata da bfcache. L'evento pageshow ha una proprietà persisted, che è vera se la pagina è stata ripristinata da bfcache e false in caso contrario. Puoi utilizzare la proprietà persisted per distinguere i caricamenti di pagina regolari dai ripristini bfcache. I principali servizi di analisi dovrebbero conoscere bfcache, ma puoi verificare se la pagina viene ripristinata da bfcache e inviare gli eventi manualmente.

Per determinare se una pagina viene ripristinata da 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 tue pagine per assicurarti che siano ottimizzate per bfcache e per identificare eventuali problemi che potrebbero renderle non idonee.

Per testare una pagina specifica:

  1. Vai alla pagina in Chrome.
  2. Negli 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 poi di tornare indietro per determinare se è possibile ripristinare la pagina da bfcache.

49bf965af35d5324.png

Se l'operazione ha esito positivo, il riquadro indica che la pagina è stata ripristinata dalla cache back-forward:

47015a0de45f0b0f.png

In caso contrario, il riquadro indica che la pagina non è stata ripristinata e il motivo del ripristino. Il riquadro te lo comunica anche se il motivo è un problema che puoi affrontare in qualità di sviluppatore.

dcf0312c3fc378ce.png

5. Attiva precaricamento tra siti

Il precaricamento viene avviato prima, in modo che i byte siano già all'interno del browser quando l'utente naviga, il che accelera la navigazione. È un modo semplice per migliorare Core Web Vitals e compensare alcune attività di rete prima della navigazione. In questo modo si accelera direttamente la metrica Largest Contentful Paint (LCP) e si ottiene più spazio per First Input Delay (FID) e Cumulative Layout Shift (CLS) durante la navigazione.

Il proxy di precaricamento privato abilita il precaricamento tra siti, ma non rivela informazioni private sull'utente al server di destinazione.

Come funziona il proxy di precaricamento privato

Attiva il precaricamento tra siti con il proxy di precaricamento privato

I proprietari di siti web mantengono il controllo del precaricamento tramite una nota risorsa traffic-advice, 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. Attualmente, i proprietari di siti web possono consigliare all'agente di non consentire 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 di tipo JSON:

pubblico/.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 è un campo per controllare la frazione dei precaricamenti richiesti che il proxy di precaricamento privato invia.

I consigli 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. API Integrate Shared Element Transiziones

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 appare la nuova. Questa esperienza utente in sequenza e disconnessa è disorientante e determina un carico cognitivo più elevato perché l'utente è costretto a mettere insieme come è arrivato 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 fluido riducono il carico cognitivo perché gli utenti rimangono nel contesto mentre navigano tra le pagine e riducono la latenza percepita del caricamento perché nel frattempo gli utenti vedono qualcosa di coinvolgente e piacevole. Per questi motivi, la maggior parte delle piattaforme fornisce primitive di facile utilizzo che consentono agli sviluppatori di creare transizioni fluide, ad esempio Android, iOS, MacOS e Windows.

L'API Shared Element Transiziones fornisce agli sviluppatori la stessa funzionalità sul web, indipendentemente dal fatto che le transizioni siano tra documenti (MPA) o intra-documenti (APS).

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

Demo da pixiv e Tokopedia

Per integrare l'API Shared Element Transizione 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.

pagine/frutta/[nome].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 dell'elenco e dei dettagli e perfettamente collegati nel passaggio della pagina. Puoi anche personalizzare l'animazione per renderla più creativa con gli pseudo-elementi CSS.

Il video dimostrativo dell&#39;app di esempio senza transizione degli elementi condivisi Il video dimostrativo dell&#39;app di esempio con transizione degli elementi condivisi

7. Complimenti

Complimenti! Hai creato un'app web immediata e fluida con un'esperienza utente intuitiva, coinvolgente e semplice.

Scopri di più

Prerendering

bfcache

Precaricamento tra siti

Piattaforme di scambio firmate

Transizioni di elementi condivisi/principali

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