Agrega la navegación instantánea y transiciones de página fluidas a una aplicación web

1. Antes de comenzar

En este codelab, aprenderás a agregar la navegación instantánea y transiciones de página fluidas a una aplicación web de ejemplo. Lo harás a través de las API más recientes que admite Google Chrome de forma nativa.

La aplicación web de ejemplo verifica los valores nutricionales de frutas y verduras populares. Las páginas con listas y detalles de frutas fueron creadas como una app de una sola página (SPA). Por otro lado, las páginas con listas y detalles de vegetales fueron creadas como una app tradicional de varias páginas (MPA).

Captura de pantalla de la app de ejemplo en dispositivos móviles Captura de pantalla de la app de ejemplo en dispositivos móviles

Específicamente, implementarás la renderización previa, la Memoria caché atrás/adelante (bfcache) y Private Prefetch Proxy para la navegación instantánea, además de las transiciones de elementos raíz y compartidos para realizar transiciones de páginas fluidas. Implementarás el procesamiento previo y la bfcache para las páginas MPA, además de las transiciones de elementos compartidos para las páginas SPA.

La velocidad del sitio siempre es un aspecto importante de la experiencia del usuario. Por eso, Google introdujo las Métricas web esenciales. Estas son un conjunto de métricas que miden el rendimiento de la carga, la interactividad y la estabilidad visual de las páginas web para monitorear la experiencia del usuario en el mundo real. Las API más recientes sirven para mejorar la puntuación de las Métricas web esenciales de tu sitio web en el campo, en especial, para el rendimiento de la carga.

La imagen de demostración enseña cómo la bfcache mejora el tiempo de carga

Demostración de Mindvalley

Los usuarios también están acostumbrados a usar transiciones para que las navegaciones y los cambios de estado sean extremadamente intuitivos en las aplicaciones nativas para dispositivos móviles. Desafortunadamente, no es sencillo replicar estas experiencias del usuario en la Web. Si bien es posible que logres efectos similares con las API actuales de la plataforma web, el desarrollo puede ser demasiado difícil o complejo, en especial cuando se compara con funciones equivalentes en apps para Android o iOS. Existe una brecha entre la app y la Web respecto de la experiencia que tienen los usuarios y desarrolladores. Para zanjarla, se diseñaron las API fluidas.

Demostración de la API de Shared Element Transitions de pixiv Demostración de la API de Shared Element Transitions de Tokopedia

Demostraciones de pixiv y Tokopedia

Requisitos previos

Conocimiento sobre estos temas:

Qué aprenderás:

Cómo hacer la implementación:

  • Renderización previa
  • bfcache
  • Private Prefetch Proxy
  • Transiciones de elementos raíz y compartidos

Qué compilarás

Este es un ejemplo de una aplicación web compilada con Next.js que incluye las funciones instantáneas más recientes y sin inconvenientes de los navegadores:

  • Navegación prácticamente instantánea con renderización previa
  • bfcache para cargas instantáneas con los botones "hacia atrás" y "hacia adelante" del navegador
  • Excelentes primeras impresiones de la navegación de origen cruzado con Private Prefetch Proxy o intercambio firmado (SXG)
  • Una transición fluida entre páginas con la transición de elementos raíz y compartidos

Requisitos

  • Chrome 101 o una versión posterior

2. Comenzar

Habilita funciones experimentales de Chrome

  1. Navega a about://flags y, luego, habilita las marcas de tiempo de ejecución Prerender2 y documentTransition API.
  2. Reinicia el navegador.

Obtén el código

  1. Abre el código desde este repositorio de GitHub en tu entorno de desarrollo favorito:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
  1. Instala las dependencias necesarias para ejecutar el servidor:
npm install
  1. Inicia el servidor en el puerto 3000:
npm run dev
  1. Desde tu navegador, ve a http://localhost:3000.

Ahora puedes editar y mejorar tu app. Cada vez que realices cambios, la app se volverá a cargar y los cambios se verán directamente.

3. Integra la renderización previa

A los efectos de esta demostración, retrasamos de manera arbitraria el servidor para que el tiempo de carga de la página de detalles sobre los vegetales en la app de ejemplo sea muy lento. Puedes eliminar este tiempo de espera con la renderización previa.

Para agregar botones de renderización a la página de la lista de vegetales y permitir que la renderización previa se active cuando el usuario haga clic, haz lo siguiente:

  1. Crea un componente de botón que inserte la etiqueta de secuencia de comandos de reglas de especulación de forma dinámica:

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 el componente PrerenderButton en el archivo 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 para agregar la API de Speculation Rules.

El componente SpeculationRules inserta una etiqueta de secuencia de comandos de forma dinámica en la página cuando la app actualiza el estado 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 los componentes con la 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. Haz clic en Prerender.

Ahora puedes ver una mejora significativa en la carga. En el caso de uso real, la renderización previa se activa para la página que el usuario probablemente visite a continuación por medio de una heurística.

El video de demostración de una app de ejemplo para la renderización previa

Análisis

De forma predeterminada, el archivo analytics.js de la aplicación web de ejemplo envía un evento de vista de página cuando ocurre el evento DOMContentLoaded. Lamentablemente, esto no es recomendable porque este evento se activa durante la fase de renderización previa.

Para introducir un evento document.prerendering y prerenderingchange y solucionar el problema, sigue estos pasos:

  • Vuelve a escribir el archivo 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()
  })
  ...

Perfecto. Modificaste correctamente tus estadísticas para que fueran compatibles con la renderización previa. Ahora puedes ver los registros de vista de página con el tiempo adecuado en la consola del navegador.

4. Quita los bloqueadores de bfcache

Quita el controlador de eventos unload

Tener un evento unload innecesario es un error muy común que ya no se recomienda. Este impide que bfcache funcione y, además, es poco confiable. Por ejemplo, no siempre se activa en dispositivos móviles ni en Safari.

En lugar de un evento unload, se usa el evento pagehide, que se activa en todos los casos en los que se activa el evento unload y cuando una página se coloca en la bfcache.

Para quitar el controlador de eventos unload, sigue estos pasos:

  • En el archivo analytics.js, reemplaza el código del controlador de eventos unload por el código del controlador de eventos 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')
})

Actualiza el encabezado de control de caché

Las páginas que se publican con un encabezado HTTP Cache-control: no-store no se benefician de la función bfcache del navegador. Por lo tanto, se recomienda utilizar este encabezado con moderación. En particular, si la página no contiene información personalizada ni crítica, como el estado de acceso, es probable que no sea necesario que la publiques con el encabezado HTTP Cache-control: no-store.

Para actualizar el encabezado de control de caché de la aplicación de ejemplo, sigue estos pasos:

  • Modifica el código 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')
  ...

Determina si una página se restablece desde bfcache

Cuando la página se carga inicialmente y cada vez que se restablece la página desde bfcache, se activa inmediatamente el evento pageshow después del evento load. El evento pageshow tiene una propiedad persisted. Esta es verdadera si la página se restableció desde bfcache y falsa en caso contrario. Puedes usar la propiedad persisted para distinguir entre las cargas de página normales y los restablecimientos de bfcache. Los principales servicios de estadísticas deben estar al tanto de la bfcache, pero puedes comprobar si la página se restablece desde la bfcache y enviar eventos de forma manual.

Para determinar si una página se restablece desde la bfcache, sigue estos pasos:

  • Agrega este código al archivo 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()
    }
  })

Depura una página web

Las Herramientas para desarrolladores de Chrome pueden ayudarte a probar tus páginas a fin de garantizar que estén optimizadas para la bfcache y detectar cualquier problema que pueda ocasionar que no sean aptas.

Para probar una página en particular, haz lo siguiente:

  1. Navega a la página en Chrome.
  2. En Herramientas para desarrolladores de Chrome, haz clic en Aplicación > Memoria caché atrás-adelante > Ejecutar prueba.

Las Herramientas para desarrolladores de Chrome intentarán salir de la página y luego regresar para determinar si esta se puede restablecer desde la bfcache.

49bf965af35d5324.png

Si la prueba fue exitosa, el panel te indicará que la página se restableció desde la Memoria caché atrás/adelante:

47015a0de45f0b0f.png

De lo contrario, el panel te indicará que la página no se restableció y cuál es el motivo. El panel también te indicará si puedes abordar el motivo como desarrollador.

dcf0312c3fc378ce.png

5. Habilita la carga previa entre sitios

Para acelerar la navegación, la carga previa inicia las recuperaciones de forma anticipada para que los bytes ya estén en el navegador cuando el usuario lo utilice. Es una manera sencilla de mejorar las Métricas web esenciales y compensar la actividad de la red antes de la navegación. Esto acelera directamente el Procesamiento de imagen con contenido más grande (LCP). Además, brinda más espacio para el Retraso de primera entrada (FID) y el Cambio de diseño acumulado (CLS) en la navegación.

Private Prefetch Proxy habilita la carga previa entre sitios, pero no revela información privada sobre el usuario en el servidor de destino.

Cómo funciona Private Prefetch Proxy

Habilita la carga previa entre sitios con Private Prefetch Proxy

Los propietarios de sitios web mantienen el control de la carga previa mediante un recurso muy conocido de traffic-advice, análogo a /robots.txt para los rastreadores web. Esto permite que un servidor HTTP declare que los agentes de implementación deben aplicar el consejo correspondiente. Actualmente, los propietarios de sitios web pueden aconsejarle al agente que prohíba o limite las conexiones de red. Es posible que se agreguen más consejos en el futuro.

Para alojar un recurso de traffic-advice, haz lo siguiente:

  1. Agrega este archivo similar a JSON:

public/.well-known/traffic-advice

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

El campo google_prefetch_proxy_eap es especial para el programa de acceso anticipado y el campo fraction es para controlar la fracción de solicitudes de carga previa que envía Private Prefetch Proxy.

La sugerencia de tráfico debe mostrarse con el tipo de MIME application/trafficadvice+json.

  1. En el archivo next.config.js, configura el encabezado de respuesta de la siguiente manera:

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 la API de Shared Element Transitions

Cuando un usuario navega en la Web de una página a otra, a medida que desaparece la primera página y se muestra la nueva, el contenido que ve cambia de forma inesperada y repentina. Esta experiencia secuencial y desconectada es desorientadora para el usuario. Además, le genera una carga cognitiva mayor, ya que se ve obligado a deducir cómo llegó a su ubicación actual. Esta experiencia también aumenta la percepción que tienen los usuarios sobre la manera en que se cargan las páginas mientras esperan que se cargue el destino deseado.

Las animaciones de carga fluida disminuyen la carga cognitiva porque los usuarios se mantienen en contexto mientras navegan entre las páginas. Además, reducen la latencia percibida de la carga porque, mientras tanto, los usuarios ven algo interesante y agradable. Por estos motivos, la mayoría de las plataformas ofrecen primitivas fáciles de usar que les permiten a los desarrolladores crear transiciones fluidas, como Android, iOS, MacOS y Windows.

La API de Shared Element Transitions les ofrece a los desarrolladores la misma capacidad en la Web, sin importar si las transiciones son entre documentos (MPA) o dentro de los documentos (SPA).

Demostración de la API de Shared Element Transitions de pixiv Demostración de la API de Shared Element Transitions de Tokopedia

Demostraciones de pixiv y Tokopedia

A fin de integrar la API de Shared Element Transitions para la parte de SPA de la app de ejemplo, sigue estos pasos:

  1. Crea un hook personalizado para administrar la transición en el archivo 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. Llama al hook personalizado usePageTransitionPrep() en la página de la lista y, luego, llama a la función asíncrona para activar el método transition.start() dentro del evento click.

Dentro de la función, los elementos de clase shared-element se recopilan y se registran como elementos compartidos.

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. En la página de detalles, llama al hook usePageTransition() para finalizar la función de devolución de llamada transition.start().

En esta devolución de llamada, también se registran los elementos compartidos en la página de detalles.

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

Ahora puedes ver que los elementos de la imagen se comparten en las páginas de listas y detalles, y que se conectan sin inconvenientes en la transición de la página. Incluso puedes personalizar la animación para que sea más atractiva con los pseudoelementos CSS.

Video de demostración de la app de ejemplo sin Transiciones de elementos compartidos Video de demostración de la app de ejemplo con Transiciones de elementos compartidos

7. Felicitaciones

Felicitaciones. Creaste una aplicación web instantánea y sin inconvenientes con una experiencia del usuario intuitiva, atractiva y sin fricciones.

Más información

Renderización previa

bfcache

Carga previa entre sitios

Intercambios firmados

Transiciones de elementos raíz y compartidos

Estas API aún se encuentran en la etapa inicial de desarrollo, así que comparte tus comentarios en crbug.com o los problemas que encuentres en el repositorio de GitHub de las API relevantes.