Adicionar a navegação instantânea e transições suaves entre páginas a um app da Web

1. Antes de começar

Este codelab mostra como adicionar a navegação instantânea e as transições suaves entre páginas a um app da Web de exemplo usando as APIs mais recentes com suporte nativo do Google Chrome.

O app da Web que vamos analisar verifica os valores nutricionais das frutas e dos legumes mais conhecidos. A página que mostra a lista de frutas e a página dos detalhes delas são criadas como um app de página única (SPA), já a página que mostra a lista de legumes e a página dos detalhes deles são criadas como um app tradicional de várias páginas (MPA, na sigla em inglês).

Captura de tela do app de exemplo em dispositivos móveis Captura de tela do app de exemplo em dispositivos móveis

Mais especificamente, vamos implementar a pré-renderização, o cache de avanço e retorno (bfcache) e o proxy de pré-busca particular para a navegação instantânea e as transições de elementos compartilhados ou raiz para as transições suaves entre páginas (links em inglês). A pré-renderização e o bfcache são implementados nas páginas de MPA e as transições de elementos compartilhados nas páginas de SPA.

A velocidade de um site é sempre um aspecto importante da experiência do usuário. Por esse motivo, o Google lançou as Principais métricas da Web, um conjunto de métricas que analisam o desempenho de carregamento, a interatividade e a estabilidade visual de páginas da Web, com o objetivo de avaliar a experiência real dos usuários. As APIs mais recentes ajudam a melhorar a pontuação do seu site em relação às Principais métricas da Web quando ele é usado, especialmente no que diz respeito ao desempenho de carregamento.

Imagem demonstrando como o bfcache melhora o tempo de carregamento

Demonstração do site Mindvalley (em inglês)

Os usuários também estão acostumados com o uso de transições que fazem com que a navegação e as mudanças de estado sejam extremamente intuitivas em apps nativos para dispositivos móveis. Infelizmente, replicar esse tipo de experiência do usuário não é uma tarefa simples na Web. Embora seja possível conseguir efeitos semelhantes usando as APIs atuais de plataforma da Web, o desenvolvimento pode ser muito difícil ou complexo, principalmente se comparado a recursos equivalentes em apps Android ou iOS. As APIs foram criadas para resolver essa diferença entre a experiência do usuário e do desenvolvedor em apps e na Web.

Demonstração da API Shared Element Transitions no Pixiv Demonstração da API Shared Element Transitions na Tokopedia

Demonstrações do Pixiv (em inglês) e da Tokopedia (em malaio)

Pré-requisitos

Conhecimento sobre:

O que você vai aprender

Como implementar:

  • Pré-renderização
  • bfcache
  • Proxy de pré-busca particular
  • Transições de elementos compartilhados ou raiz

O que você vai criar

Um app da Web de exemplo desenvolvido com Next.js, que implemente os recursos de navegador integrados e instantâneos mais recentes:

  • Navegação quase instantânea com a pré-renderização.
  • Uso do bfcache para carregamentos instantâneos dos botões "Voltar" e "Avançar" do navegador.
  • Uma ótima primeira impressão sobre a navegação de origem cruzada com o proxy de pré-busca particular ou a troca assinada (SXG).
  • Transição suave entre páginas com uso da transição de elementos compartilhados ou raiz.

O que é necessário

  • Chrome versão 101 ou mais recente.

2. Primeiros passos

Ativar sinalizações do Chrome

  1. Acesse about://flags e ative as sinalizações de execução Prerender2 e documentTransition API.
  2. Reinicie o navegador.

Acessar o código

  1. Abra o código deste repositório do GitHub (em inglês) no ambiente de desenvolvimento que preferir:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
  1. Instale as dependências necessárias para executar o servidor:
npm install
  1. Inicie o servidor na porta 3000:
npm run dev
  1. Acesse http://localhost:3000 no navegador.

Agora você pode editar e melhorar o app. Sempre que fizer alguma mudança, o app vai ser recarregado e as mudanças ficarão imediatamente visíveis.

3. Integrar a pré-renderização

Para os fins desta demonstração, a lentidão no tempo de carregamento da página de detalhes dos legumes no app de exemplo é causada por um atraso arbitrário no servidor. Esse tempo de espera pode ser eliminado com a pré-renderização.

Para adicionar botões de pré-renderização à página que mostra a lista de legumes e permitir que eles acionem a pré-renderização após um clique do usuário:

  1. Crie um componente de botão, que insere a tag de script das regras de especulação 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. Importe o componente PrerenderButton no arquivo 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. Crie um componente para adicionar a API Speculation Rules.

O componente SpeculationRules insere uma tag de script na página de forma dinâmica quando o app atualiza o 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. Integre os componentes ao 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. Clique em Pré-renderizar.

Você vai ver uma melhora significativa no tempo de carregamento. No caso de uso real, a pré-renderização é acionada para a página que o usuário provavelmente vai acessar em seguida, definida com base em métodos heurísticos.

Vídeo demonstrando a pré-renderização no app de exemplo

Análise

Por padrão, o arquivo analytics.js no app da Web de exemplo envia um evento de visualização de página quando o evento DOMContentLoaded ocorre. Essa não é a melhor opção, porque esse evento é acionado durante a fase de pré-renderização.

Para incluir um evento prerenderingchange e document.prerendering (link em inglês) e corrigir esse problema:

  • Mude o arquivo analytics.js para que fique assim:

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

Parabéns! Você modificou a análise de dados para que ela seja compatível com a pré-renderização. Agora é possível ver os registros de visualização de página com a marcação de tempo correta no console do navegador.

4. Remover bloqueadores do bfcache

Remover o manipulador de eventos unload

Ter um evento unload desnecessário é um erro muito comum. O uso desse evento não é mais recomendado porque, além de impedir que o bfcache funcione, ele não é confiável. Esse tipo de evento nem sempre é acionado em dispositivos móveis e no Safari, por exemplo (links em inglês).

Em vez de implementar um evento unload, use o evento pagehide, que é acionado em todos os casos em que o evento unload é disparado e quando uma página é colocada no bfcache.

Para remover o manipulador de eventos unload, faça o seguinte:

  • No arquivo analytics.js, substitua o código do manipulador de eventos unload pelo código do manipulador 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')
})

Atualizar o cabeçalho de controle de cache

As páginas veiculadas com um cabeçalho HTTP Cache-control: no-store não se beneficiam do recurso bfcache do navegador. É recomendável evitar o uso desse cabeçalho. Se a página não incluir informações personalizadas ou essenciais, como o estado conectado, provavelmente não vai ser necessário veicular essa página com o cabeçalho HTTP Cache-control: no-store.

Para atualizar o cabeçalho de controle de cache do app de exemplo:

  • Modifique o 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')
  ...

Determinar se uma página é restaurada do bfcache

O evento pageshow é acionado logo depois do load quando a página é carregada pela primeira vez e todas as vezes que a página é restaurada do bfcache. O evento pageshow tem uma propriedade persisted, que é "true" (verdadeira) caso a página tenha sido restaurada do bfcache e "false" (falsa) caso contrário. É possível usar a propriedade persisted para diferenciar entre os carregamentos de página normais e as restaurações do bfcache. Os principais serviços de análise precisam ser informados sobre o uso do bfcache, mas é possível verificar se a página foi restaurada do bfcache e enviar eventos manualmente.

Para determinar se uma página foi restaurada do bfcache:

  • Adicione o código abaixo ao arquivo 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()
    }
  })

Depurar uma página da Web

As Ferramentas para desenvolvedores do Chrome ajudam a testar as páginas a fim de garantir que elas sejam otimizadas para o uso do bfcache e identificar problemas que possam as desqualificar.

Para testar uma página específica, siga estas etapas:

  1. Acesse a página no Chrome.
  2. Nas Ferramentas para desenvolvedores do Chrome, clique em Aplicativo > Cache de avanço e retorno > Executar teste.

As Ferramentas para desenvolvedores do Chrome vão tentar acessar outra página e voltar à página em questão para determinar se ela pode ser restaurada do bfcache.

49bf965af35d5324.png

Se tudo der certo, o painel vai mostrar a informação de que a página foi restaurada do cache de avanço e retorno:

47015a0de45f0b0f.png

Caso não seja possível restaurar a página, o painel vai informar que ela não foi restaurada e o motivo que impediu a restauração. Se for possível resolver o problema que impediu a restauração, isso também vai ser informado no painel.

dcf0312c3fc378ce.png

5. Habilitar a pré-busca entre sites

A pré-busca inicia as buscas com antecedência para que os bytes já estejam no navegador quando o usuário acessar a página. Isso acelera a navegação. Essa é uma maneira fácil de melhorar as Principais métricas da Web e compensar a atividade de rede antes da navegação. Ela acelera diretamente a Maior exibição de conteúdo (LCP) e oferece mais espaço para a latência na primeira entrada (FID) e a Mudança de layout cumulativa (CLS) na navegação.

O proxy de pré-busca particular (link em inglês) permite a pré-busca entre diferentes sites, mas não revela informações particulares do usuário para o servidor de destino.

Como o proxy de pré-busca particular funciona

Ativar a pré-busca entre sites com o proxy de pré-busca particular

Os proprietários dos sites mantêm o controle da pré-busca usando um recurso conhecido de orientações de tráfego (em inglês), semelhante ao /robots.txt para rastreadores da Web. Isso permite que um servidor HTTP declare que os agentes de implementação precisam aplicar a orientação correspondente. No momento, os proprietários de sites podem orientar o agente a proibir ou limitar as conexões de rede. No futuro, vai ser possível adicionar outras orientações.

Para hospedar um recurso de orientação de tráfego:

  1. Adicione este arquivo de estilo JSON:

public/.well-known/traffic-advice

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

O campo google_prefetch_proxy_eap é específico para o programa de acesso antecipado, e o campo fraction controla a fração de pré-buscas solicitadas que vão ser enviadas pelo proxy de pré-busca particular.

As orientações de tráfego precisam ser retornadas usando o tipo MIME application/trafficadvice+json.

  1. No arquivo next.config.js, configure o cabeçalho de resposta:

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. Integrar a API Shared Elements Transitions

Quando um usuário navega de uma página para outra na Web, o conteúdo mostrado muda de modo repentino e inesperado à medida que a primeira página desaparece e a nova é mostrada. Essa experiência sequenciada e desconectada pode ser confusa para o usuário, resultando em uma maior carga cognitiva, já que o usuário é forçado a tentar compreender como chegou até o ponto em que se encontra. Além disso, essa experiência aumenta o nível de percepção dos usuários em relação ao carregamento da página enquanto aguardam a exibição do novo destino.

As animações de carregamento suave reduzem a carga cognitiva, já que os usuários permanecem em um mesmo contexto durante a navegação entre as diferentes páginas, e reduzem a latência de carregamento percebida, já que os usuários veem algo interessante e agradável enquanto aguardam o carregamento. Por esses motivos, a maioria das plataformas, como Android, iOS, MacOS e Windows, oferecem primitivos fáceis de usar que permitem que os desenvolvedores criem transições suaves.

A API Shared Element Transitions (link em inglês) oferece aos desenvolvedores o mesmo recurso para uso na Web, seja para transições entre vários documentos (MPA) ou em um mesmo documento (SPA).

Demonstração da API Shared Element Transitions no Pixiv Demonstração da API Shared Element Transitions na Tokopedia

Demonstrações do Pixiv (em inglês) e da Tokopedia (em malaio)

Para integrar a API Shared Element Transitions na parte SPA do app de exemplo:

  1. Crie um hook personalizado para gerenciar a transição no arquivo 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. Chame o hook usePageTransitionPrep() personalizado na página de lista e, em seguida, chame a função assíncrona para acionar o método transition.start() no evento click.

Dentro da função, os elementos da classe shared-element vão ser coletados e registrados como elementos compartilhados.

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. Na página de detalhes, chame o hook usePageTransition() para concluir a função de callback transition.start().

Nesse callback, os elementos compartilhados na página de detalhes também vão ser registrados.

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

Agora, é possível ver que os elementos de imagem são compartilhados entre as páginas de lista e de detalhes e são perfeitamente conectados durante a transição entre as páginas. Você também pode personalizar a animação para deixá-la mais sofisticada usando os pseudoelementos CSS (em inglês).

Vídeo de demonstração do app de exemplo sem a API Shared Element Transition Vídeo de demonstração do app de exemplo com a API Shared Element Transition

7. Parabéns

Parabéns! Você criou um app da Web instantâneo e integrado com uma experiência do usuário simples, envolvente e intuitiva.

Saiba mais

Pré-renderização (links em inglês)

bfcache (links em inglês)

Pré-busca entre sites (link em inglês)

Trocas assinadas

Transições de elementos compartilhados ou raiz

Essas APIs ainda estão nos estágios iniciais de desenvolvimento. Compartilhe seu feedback em crbug.com (link em inglês) ou relate os problemas enfrentados no repositório do GitHub das APIs em questão.