Ajouter la navigation instantanée et des transitions de page fluides à une application Web

1. Avant de commencer

Cet atelier de programmation explique comment ajouter la navigation instantanée et des transitions de page fluides à une application Web d'exemple à l'aide des dernières API compatibles en natif avec Google Chrome.

L'application Web d'exemple recense les valeurs nutritionnelles de fruits et légumes populaires. Les pages de liste et d'informations sur les fruits sont compilées sous la forme d'une application à page unique (SPA, pour Single Page Application), tandis que les pages concernant les légumes apparaissent dans une application traditionnelle à pages multiples (MPA, pour Multiple Page Application).

Capture d'écran de l'application exemple sur un appareil mobile Capture d'écran de l'application exemple sur un appareil mobile

Plus spécifiquement, il s'agira d'implémenter le préchargement, le cache amélioré (bfcache, pour back-forward cache) et le proxy de préchargement privé pour permettre la navigation instantanée, et les transitions des éléments partagés/racines pour obtenir des transitions de page fluides. Vous implémentez le préchargement et le cache amélioré pour les pages MPA, et des transitions d'éléments partagés pour les pages SPA.

La vitesse du site représente toujours un aspect important de l'expérience utilisateur. C'est pourquoi Google a lancé Signaux Web essentiels, un ensemble de métriques qui évaluent la performance de chargement, l'interactivité et la stabilité visuelle des pages Web, afin d'évaluer l'expérience utilisateur réelle. Les dernières API vous aideront à améliorer le score Signaux Web essentiels de votre site Web, et notamment ses performances de chargement.

Images animées montrant l'accélération du chargement grâce au cache amélioré

Source de la démo : Mindvalley

Les utilisateurs sont habitués aux transitions, qui rendent la navigation et les changements d'état particulièrement intuitifs dans les applications mobiles natives. Il n'est malheureusement pas simple de répliquer ces expériences sur le Web. Même si vous pouvez obtenir des effets similaires avec les API des plates-formes Web actuelles, le développement peut être trop difficile ou complexe, surtout par rapport aux équivalents dans les applications Android ou iOS. Les API fluides sont conçues pour combler l'écart entre l'expérience utilisateur/développeur sur application et sur le Web.

Démonstration de l'API Shared Element Transitions par pixiv Démonstration de l'API Shared Element Transitions par Tokopedia

Source des démos : pixiv et Tokopedia

Conditions préalables

Maîtrise de :

Points abordés :

Implémentation de :

  • Préchargement
  • Cache amélioré (bfcache)
  • Proxy de préchargement privé
  • Transitions des éléments partagés/racines

Ce que vous allez faire

Implémenter une application Web d'exemple développée avec Next.js et intégrant les dernières fonctionnalités de navigation instantanée et fluides :

  • Navigation quasi instantanée avec préchargement
  • Cache amélioré pour des chargements instantanés lorsque les boutons Précédent et Suivant du navigateur sont utilisés
  • Excellentes premières impressions en navigation inter-origines grâce au proxy de préchargement privé ou à l'échange signé (SXG)
  • Transitions de page fluides avec la transition d'éléments partagés/racines

Ce dont vous avez besoin

  • Chrome 101 ou version ultérieure

2. Commencer

Activer les indicateurs Chrome

  1. Accédez à about://flags, puis activez les indicateurs d'exécution Prerender2 et documentTransition API.
  2. Redémarrez votre navigateur.

Obtenir le code

  1. Ouvrez le code de ce dépôt GitHub dans l'environnement de développement de votre choix :
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
  1. Installez les dépendances nécessaires à l'exécution du serveur :
npm install
  1. Démarrez le serveur sur le port 3000 :
npm run dev
  1. Accédez à http://localhost:3000 dans votre navigateur.

Vous pouvez à présent modifier et améliorer votre application. À chaque modification, l'application s'actualise et vos modifications sont visibles directement.

3. Intégrer le préchargement

Dans le cadre de cette démonstration, un délai arbitraire côté serveur ralentit considérablement le chargement de la page d'informations sur les légumes dans l'application exemple. Vous éliminerez ce délai grâce au préchargement.

Pour ajouter des boutons de préchargement à la page de liste des légumes, et permettre à ces boutons de déclencher le préchargement suite à un clic de l'utilisateur :

  1. Créez un composant de bouton qui insère la balise de script "speculation-rules" de manière dynamique :

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. Importez le composant PrerenderButton dans le fichier 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. Créez un composant pour ajouter l'API Speculation Rules.

Le composant SpeculationRules insère dynamiquement une balise de script dans la page lorsque l'état de l'application est 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. Intégrez les composants à l'application.

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. Cliquez sur Prerender (Précharger).

Vous pouvez constater une amélioration significative du chargement. Dans le cas d'utilisation réel, la page préchargée est celle que l'utilisateur est le plus susceptible de consulter ensuite (la détermination se fait par méthode heuristique).

Démonstration vidéo du préchargement dans l'application exemple

Analyse

Par défaut, le fichier analytics.js de l'application Web exemple envoie un événement "page vue" lorsque l'événement DOMContentLoaded se produit. Ce n'est cependant pas souhaitable, car cet événement se déclenche pendant la phase de préchargement.

Pour introduire un événement document.prerendering et prerenderingchange afin de résoudre ce problème :

  • Réécrivez le fichier 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()
  })
  ...

Parfait ! Vous avez modifié votre code Analytics pour le rendre compatible avec le préchargement. Les délais des journaux de pages vues devraient être corrigés dans la console du navigateur.

4. Supprimer les bloqueurs de cache amélioré

Supprimer le gestionnaire d'événements unload

Les événements unload inutiles sont une erreur très courante. Cette pratique n'est plus recommandée. Elle n'est pas fiable, et elle empêche le fonctionnement du cache amélioré. Par exemple, l'événement ne se déclenche pas toujours sur mobile ou Safari.

Au lieu d'un événement unload, utilisez l'événement pagehide, qui se déclenche dans tous les cas lors d'un événement unload et lorsqu'une page est mise en cache amélioré.

Pour supprimer le gestionnaire d'événements unload :

  • Dans le fichier analytics.js, remplacez le code du gestionnaire d'événements unload par celui du gestionnaire d'événements 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')
})

Mettre à jour l'en-tête "cache-control"

Les pages diffusées avec un en-tête HTTP Cache-control: no-store ne bénéficient pas de la fonctionnalité bfcache du navigateur. Il est donc recommandé de limiter l'utilisation de cet en-tête. Plus spécifiquement, si la page ne contient pas d'informations personnalisées ou essentielles, telles que l'état de connexion, l'en-tête HTTP Cache-control: no-store n'est probablement pas nécessaire.

Pour mettre à jour l'en-tête cache-control de l'application exemple :

  • Modifiez le code 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')
  ...

Déterminer si une page a été restaurée à partir du cache amélioré

L'événement pageshow se déclenche immédiatement après l'événement load, lors du chargement initial de la page et chaque fois que celle-ci est restaurée à partir du cache amélioré. L'événement pageshow comporte une propriété persisted, dont la valeur est "true" si la page a été restaurée à partir du cache amélioré, et "false" dans le cas contraire. La propriété persisted permet de distinguer les chargements de page des restaurations depuis le cache amélioré. Les principaux services d'analyse devraient reconnaître le cache amélioré, mais vous pouvez également vérifier si une page a été restaurée via bfcache et transmettre les événements manuellement.

Pour déterminer si une page a été restaurée à partir du cache amélioré :

  • Ajoutez ce code au fichier 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()
    }
  })

Déboguer une page Web

Les outils pour les développeurs Chrome permettent de tester vos pages afin de vérifier qu'elles sont optimisées pour bfcache, et d'identifier les problèmes susceptibles de les rendre incompatibles.

Pour tester une page spécifique :

  1. Accédez à la page dans Chrome.
  2. Dans les outils pour les développeurs Chrome, cliquez sur Application > Back-forward Cache > Run Test (Application > Cache amélioré > Exécuter le test).

L'outil tentera de quitter la page, puis d'y revenir pour déterminer si elle peut être restaurée à partir du cache amélioré.

49bf965af35d5324.png

Si l'opération réussit, le panneau indique que la page a été restaurée à partir du cache amélioré.

47015a0de45f0b0f.png

Si l'opération échoue, le panneau indique que la page n'a pas été restaurée et explique pourquoi. Le panneau indique également si le problème peut être résolu par un développeur.

dcf0312c3fc378ce.png

5. Activer le préchargement intersite

Le préchargement anticipe la navigation de sorte que les données sont acquises par le navigateur avant que l'utilisateur déclenche la transition, ce qui accélère le processus. Cette approche permet d'améliorer facilement les Signaux Web essentiels et de compenser une partie de l'activité réseau avant la navigation. Cela accélère directement le Largest Contentful Paint (LCP), et libère de l'espace pour le First Input Delay (FID) et le Cumulative Layout Shift (CLS) lors de la navigation.

Le proxy de préchargement privé permet le préchargement intersite sans divulguer d'informations privées concernant l'utilisateur au serveur de destination.

Fonctionnement du proxy de préchargement privé

Activer le préchargement intersite avec proxy de préchargement privé

Les propriétaires de sites Web gardent le contrôle du préchargement par l'intermédiaire d'une ressource traffic-advice bien connue, comparable à /robots.txt pour les robots d'exploration. Cette ressource permet à un serveur HTTP de déclarer que l'implémentation des agents doit appliquer l'avis correspondant. Actuellement, les propriétaires de sites Web peuvent demander à l'agent d'interdire ou de limiter les connexions réseau. À l'avenir, d'autres avis pourraient être ajoutés.

Pour héberger une ressource "traffic-advice" :

  1. Ajoutez ce fichier (comparable à un fichier JSON).

public/.well-known/traffic-advice

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

Le champ google_prefetch_proxy_eap est un champ spécial du programme en accès anticipé, tandis que le champ fraction permet de contrôler la fraction des préchargements demandés par le proxy de préchargement privé.

Les avis sur trafic doivent être renvoyés avec le type MIME application/trafficadvice+json.

  1. Dans le fichier next.config.js, configurez l'en-tête de réponse.

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. Intégrer l'API Shared Element Transitions

Lorsqu'un utilisateur navigue sur le Web d'une page à une autre, le contenu affiché change de façon soudaine et imprévisible à mesure que la première page disparaît et que la nouvelle page apparaît. Cette expérience constitue une séquence disjointe susceptible de désorienter et d'imposer une charge cognitive plus lourde, car l'utilisateur est contraint de reconstituer son chemin. Elle souligne également les délais perçus pendant que l'utilisateur attend le chargement des pages.

Un chargement fluide des animations réduit la charge cognitive, car les utilisateurs sont maintenus en contexte pendant leur navigation entre les pages. De plus, l'utilisateur restant exposé à du contenu engageant et intéressant, sa perception de la latence de chargement est atténuée. C'est pourquoi la plupart des plates-formes proposent des primitives faciles à utiliser qui permettent aux développeurs de créer des transitions fluides, comme sur Android, iOS, macOS et Windows.

L'API Shared Element Transitions offre aux développeurs les mêmes fonctionnalités sur le Web, que leurs transitions soient inter ou intra-document (MPA ou SPA).

Démonstration de l'API Shared Element Transitions par pixiv Démonstration de l'API Shared Element Transitions par Tokopedia

Source des démos : pixiv et Tokopedia

Pour intégrer l'API Shared Element Transitions et gérer la partie SPA de l'application exemple :

  1. Créez un hook personnalisé pour gérer la transition dans le fichier 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. Appelez le hook personnalisé usePageTransitionPrep() sur la page de liste, puis appelez la fonction asynchrone pour déclencher la méthode transition.start() dans l'événement click.

Dans la fonction, les éléments de la classe shared-element sont collectés et enregistrés en tant qu'éléments partagés.

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. Sur la page d'informations, appelez le hook usePageTransition() pour terminer la fonction de rappel transition.start().

Dans ce rappel, les éléments partagés de la page d'informations sont également enregistrés.

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

Comme vous pouvez le constater, les éléments image sont partagés sur les pages de liste et d'informations, et s'intègrent parfaitement lors de la transition. Vous pouvez même personnaliser et enjoliver l'animation à l'aide de pseudo-éléments CSS.

Démonstration vidéo de l'application exemple sans Shared Element Transitions Démonstration vidéo de l'application exemple avec Shared Element Transitions

7. Félicitations

Félicitations ! Vous avez créé une application Web offrant une expérience fluide, attrayante et intuitive grâce à des transitions instantanées, sans latence.

En savoir plus

Préchargement

Cache amélioré (bfcache)

Préchargement intersite

Échanges signés

Transitions des éléments partagés/racines

Ces API en sont encore aux premières phases de leur développement. Nous vous invitons à partager vos commentaires, sur crbug.com ou en tant que problèmes dans le dépôt GitHub des API concernées.