Capítulo 46 de 456

Instant navigation

Core Idea

Guia prático para estruturar rotas de forma que o navegador comece a renderizar a próxima página no instante do clique (conteúdo estático/cacheado/fallback aparecendo imediatamente), usando Cache Components + Partial Prefetching, validação automática em dev, DevTools de inspeção e testes e2e com instant().

Key Concepts

  • Instant navigation: navegação em que o browser começa a renderizar a nova página no clique, com conteúdo estático/cacheado/fallback imediato enquanto o servidor faz stream do resto. Assume caches quentes.
  • Static shell: HTML retornado em visitas diretas (a partir da raiz do documento), tipicamente de um CDN.
  • App Shell: gerado por rota para navegações client-side; é o prefetch padrão de <Link> sob Partial Prefetching.
  • prefetch={true} (per-link prefetching): resolve params/searchParams/URL completa de um link específico antes do clique; não substitui ter o App Shell instantâneo primeiro.
  • validationLevel: opção em experimental.instantInsights; 'warning' (padrão) valida toda Page/Default em dev; 'manual-warning' só valida segmentos que exportam instant explicitamente.
  • instant = false: route segment config que opta um segmento fora da validação (a rota ainda pode navegar instantaneamente se a estrutura suportar, só não gera insights).
  • Navigation Inspector: painel do Next.js DevTools que congela a página no estado inicial de loading (shell estático em visita direta, shell prefetchado em navegação client-side); toggle "Pause on navigations".
  • instant() helper (@next/playwright): escopa asserções à UI imediatamente disponível na navegação, para testes e2e de regressão.
  • exposeTestingApiInProductionBuild: flag experimental que expõe a testing API do instant() também em next start (build de produção), não só em next dev.
  • "use cache: private": variante de cache para funções que leem cookies()/headers(); cache só no browser, não pode fazer parte do static shell.

Code Examples

// next.config.ts
const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
}
  • O que demonstra: setup mínimo para habilitar instant navigation.
// app/products/[slug]/page.tsx — depois do fix
async function ProductInfo({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const res = await fetch(`https://next-recipe-api.vercel.dev/products/${slug}`)
  const product = await res.json()
  return <><h1>{product.name}</h1><p>${product.price}</p></>
}

async function getFeatured() {
  'use cache'
  const res = await fetch('https://next-recipe-api.vercel.dev/products?limit=3')
  return res.json()
}

export default async function ProductPage(props: PageProps<'/products/[slug]'>) {
  const featured = await getFeatured()
  return (
    <div>
      <FeaturedSection items={featured} />
      <Suspense fallback={<p>Loading product...</p>}>
        <ProductInfo params={props.params} />
      </Suspense>
    </div>
  )
}
  • O que demonstra: os dois fixes canônicos de bloqueio de navegação: extrair leitura dependente de slug para dentro de <Suspense>, e cachear a leitura independente de URL com 'use cache'.
// e2e/navigation.test.ts
import { instant } from '@next/playwright'

test('is instant on a client navigation', async ({ page }) => {
  await page.goto('/store/shoes')
  await instant(page, async () => {
    await page.click('a[href="/store/hats"]')
    await page.waitForURL((url) => url.pathname === '/store/hats')
    await expect(page.locator('h1')).toContainText('Baseball Cap')
    await expect(page.getByText('In stock')).toHaveCount(0)
  })
  await expect(page.getByText('In stock')).toBeVisible()
})
  • O que demonstra: padrão de teste e2e que verifica UI imediata dentro do escopo instant() e o dado dinâmico chegando depois.

Anti-patterns

  • Colocar <Suspense> só no topo da página para "passar" na validação: satisfaz a validação, mas substitui quase toda a página por um único fallback em cada navegação; prefira fallbacks pequenos e localizados.
  • Esperar <Suspense> do root layout cobrir navegação client-side: só cobre visita direta (page load); navegação client-side só re-renderiza abaixo do layout compartilhado.
  • Testar sem esperar a URL de destino em navegação client-side: um seletor compartilhado pode casar com a página de origem antes do destino comitar, mascarando falhas.
  • Usar "use cache: private" esperando que entre no static shell: esse cache é só de browser; não é servido no App Shell.

Key Takeaways

  1. Visita direta e navegação client-side podem produzir UI inicial diferente porque cobrem escopos diferentes de re-render (raiz vs. abaixo do layout compartilhado).
  2. prefetch={true} só resolve dados por-link; primeiro é preciso tornar a rota instantânea com App Shell — per-link prefetching não conserta uma rota que bloqueia sem ele.
  3. A validação automática do dev overlay simula tanto page load quanto client navigation separadamente; passar num não garante passar no outro.
  4. instant() em testes e2e fecha a lacuna que a validação estrutural não cobre: garante que o conteúdo certo (não só a existência de um shell) aparece na navegação.
  5. instant = false desliga só o feedback de validação, não a capacidade da rota de navegar instantaneamente se a estrutura já suportar.

Connects To

  • ISR with Cache Components (ch045): App Shell e upgrade em background são o mesmo mecanismo usado aqui para navegações instantâneas.
  • Interactive apps (ch048): Step 8 usa exatamente os primitivos desta página ('use cache', prefetch={true}) para tornar navegação repetida instantânea.
  • Optimizing prefetching: aprofunda per-link prefetching e "use cache: private".