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).
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.
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.
Source des démos : pixiv et Tokopedia
Conditions préalables
Maîtrise de :
- HTML
- CSS
- JavaScript
- Outils pour les développeurs Google Chrome
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
- Accédez à about://flags, puis activez les indicateurs d'exécution
Prerender2
etdocumentTransition API
. - Redémarrez votre navigateur.
Obtenir le code
- 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
- Installez les dépendances nécessaires à l'exécution du serveur :
npm install
- Démarrez le serveur sur le port 3000 :
npm run dev
- 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 :
- 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>
)
}
- Importez le composant
PrerenderButton
dans le fichierlist-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>
)
}
- 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])
}
- 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
- 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).
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énementsunload
par celui du gestionnaire d'événementspagehide
.
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 :
- Accédez à la page dans Chrome.
- 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é.
Si l'opération réussit, le panneau indique que la page a été restaurée à partir du cache amélioré.
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.
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.
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" :
- 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
.
- 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).
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 :
- 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
}
- Appelez le hook personnalisé
usePageTransitionPrep()
sur la page de liste, puis appelez la fonction asynchrone pour déclencher la méthodetransition.start()
dans l'événementclick
.
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>
)
}
- Sur la page d'informations, appelez le hook
usePageTransition()
pour terminer la fonction de rappeltransition.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.
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
- Repenser le préchargement
- Proposer des chargements de page instantanés sur navigateur grâce au préchargement spéculatif
- quicklink
Cache amélioré (bfcache)
Préchargement intersite
Échanges signés
Transitions des éléments partagés/racines
- Shared Element Transitions
- Transitions de page simples et fluides avec l'API Shared Element Transitions
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.