Gerenciamento de loading no Next.js: guia prático de loading.tsx e Suspense

Você talvez já tenha passado por isso: o usuário clica em um link, a página fica em branco por 3 segundos inteiros e não mostra nenhum feedback. A pessoa começa a desconfiar: “Travou?” Aí aperta F5 sem parar e, quando a página finalmente carrega, é recarregada de novo…
Antes, eu também tratava loading desse jeito. Toda vez que criava uma página nova, colocava algo assim dentro do componente:
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
setLoading(true);
fetchData()
.then(setData)
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />;
O código ficava repetitivo e comprido. Pior: cada página precisava escrever a mesma lógica de novo. E, em equipe, cada pessoa acabava tratando loading de um jeito: alguém usava estado global, alguém usava Context. A manutenção virava um pesadelo.
Até que um dia, lendo a documentação oficial do Next.js, percebi que o Next.js já tinha uma solução de loading mais elegante embutida: loading.tsx e Suspense.
Depois de usar, ficou claro: gerenciar estados de loading pode ser simples. O código caiu quase pela metade e a experiência do usuário subiu um nível. Neste artigo, vou compartilhar a prática dessa abordagem.
Por que usar loading.tsx e Suspense
As dores da abordagem tradicional
Primeiro, um exemplo bem real. Imagine que precisamos criar uma página de listagem de posts. A forma tradicional seria mais ou menos assim:
// app/blog/page.tsx
'use client';
import { useState, useEffect } from 'react';
export default function BlogPage() {
const [loading, setLoading] = useState(true);
const [posts, setPosts] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/api/posts')
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
if (loading) {
return <div className="spinner">Carregando...</div>;
}
if (error) {
return <div>Erro: {error.message}</div>;
}
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
Parece aceitável, certo? O problema é:
- Código redundante: cada página precisa escrever esse bloco de gerenciamento de estado.
- Estado fragmentado:
loading,dataeerrorficam espalhados em três states, o que facilita bugs de sincronização. - Precisa ser Client Component: ao usar
useStateeuseEffect, o componente inteiro roda no cliente e perde vantagens da renderização no servidor. - Experiência ruim: entre o clique e a exibição do loading, pode existir uma tela branca perceptível.
E se você já participou de Code Review, sabe como isso aparece na prática. Cada desenvolvedor escreve loading de um jeito. Alguns movem o estado para Context, outros usam Zustand global, outros repetem a lógica dentro de cada componente. Quando o projeto cresce, fica difícil sustentar.
A solução do Next.js
O App Router do Next.js entrega três recursos centrais para resolver isso:
1. loading.tsx: convenção antes de configuração
Você só precisa criar um arquivo loading.tsx dentro da pasta da rota. O Next.js usa esse arquivo automaticamente como a UI de loading daquela rota. Não precisa escrever useState, não precisa gerenciar estado e nem mesmo envolver tudo com Suspense manualmente.
2. Suspense: suporte nativo do React 18
O Suspense do React 18 permite controlar loading no nível de componente. Se uma parte dos dados é lenta, você coloca uma fronteira de Suspense só naquela parte. O restante da página continua aparecendo, sem obrigar a tela inteira a esperar.
3. Streaming: carregar e exibir por partes
Com a renderização em streaming do Next.js, a página pode aparecer em blocos. Primeiro o cabeçalho, depois a barra lateral, por fim a parte de dados mais lenta. O usuário não fica olhando uma tela em branco; a experiência melhora muito.
E tem um dado que talvez interesse: depois de usar skeleton screens e Streaming, é comum reduzir visivelmente os tempos de FCP (First Contentful Paint) e LCP (Largest Contentful Paint). A pontuação do Google PageSpeed Insights também pode subir alguns pontos.
Uso básico de loading.tsx
Começo rápido: o primeiro loading.tsx
Sem enrolar, vamos escrever um loading.tsx simples.
Imagine esta estrutura de diretórios:
app/
blog/
page.tsx
Você só precisa adicionar um loading.tsx dentro da pasta blog:
app/
blog/
loading.tsx ← novo arquivo
page.tsx
Depois escreva uma UI simples de loading:
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
<p className="ml-4">Carregando...</p>
</div>
);
}
É só isso: 10 linhas resolvem. Agora, quando o usuário acessar /blog, antes de page.tsx terminar de carregar, o Next.js exibirá automaticamente esse componente de loading.
O ponto principal: você não precisa envolver nada manualmente com Suspense. O Next.js faz isso por você. Na renderização real, ele coloca seu page.tsx dentro de algo como <Suspense fallback={<Loading />}>.
Quando vi isso pela primeira vez, fiquei meio desconfiado: “Só isso? Funciona mesmo?” Testei e funcionou. E o código ficou muito mais limpo, sem aquela pilha de useState em cada página.
O escopo de loading.tsx
loading.tsx tem um conceito importante: segmento de rota (Route Segment). Em termos simples: ele afeta o page.tsx da mesma pasta e todas as rotas filhas.
Exemplo:
app/
blog/
loading.tsx ← afeta /blog e /blog/[id]
page.tsx ← página de lista em /blog
[id]/
page.tsx ← página de detalhes em /blog/123
Esse loading.tsx aparece nestes casos:
- O usuário acessa
/blog, enquanto a página de lista carrega. - O usuário clica em
/blog/123a partir da lista, enquanto a página de detalhes carrega.
Mas atenção: ele não afeta o layout. Se blog/layout.tsx tiver uma barra de navegação, essa barra continua visível. Só a parte do page.tsx é substituída pelo loading.
É isso que a documentação do Next.js quer dizer com “layouts compartilhados permanecem interativos”. Enquanto espera a nova página carregar, o usuário ainda pode clicar na navegação e ir para outra página. A interface inteira não trava.
Uma visualização ajuda:
Layout (sempre visível)
├─ Navegação
└─ Suspense Boundary
├─ Loading UI (visível durante o carregamento dos dados)
└─ Page (visível quando os dados terminam de carregar)
Server Component vs Client Component
Por padrão, loading.tsx é um Server Component. Na maioria dos casos, isso basta: você retorna algum JSX e pronto.
Mas às vezes você quer adicionar uma animação, por exemplo com Framer Motion, ou usar uma biblioteca que precisa de JavaScript no cliente. Aí entra 'use client':
// app/blog/loading.tsx
'use client';
import { motion } from 'framer-motion';
export default function Loading() {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex items-center justify-center min-h-screen"
>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
</motion.div>
);
}
Minha regra geral é: se dá para usar Server Component, uso Server Component. Só adiciono 'use client' quando realmente preciso de interação no cliente. Afinal, Server Component não entra no bundle do navegador, então a página carrega mais rápido.
Skeleton screen na prática
Por que skeleton screen é melhor que spinner
Você certamente já viu aquele spinner girando durante o loading. Do ponto de vista da experiência do usuário, skeleton screen costuma ser muito melhor que spinner.
Por quê? Pesquisas de psicologia do usuário mostram que, quando alguém vê uma skeleton screen, o cérebro antecipa que “o conteúdo está chegando”. A espera parece menor. Já o spinner só diz “está carregando”, sem mostrar o que vem nem quanto tempo falta, o que aumenta a ansiedade.
Além disso, a skeleton screen antecipa a estrutura aproximada da página. Se o usuário vê três blocos horizontais, entende que haverá três posts ali. Essa expectativa reduz a sensação de incerteza.
Três formas de implementar
Há várias formas de criar skeleton screens. Aqui vão três das mais comuns:
Opção 1: CSS puro (a mais leve)
Se você não quer adicionar dependências, CSS puro resolve:
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="max-w-4xl mx-auto p-6">
{[1, 2, 3].map((i) => (
<div key={i} className="mb-8 animate-pulse">
{/* Skeleton do título */}
<div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
{/* Skeleton do resumo */}
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded"></div>
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
</div>
{/* Skeleton dos metadados */}
<div className="flex gap-4 mt-4">
<div className="h-3 bg-gray-200 rounded w-20"></div>
<div className="h-3 bg-gray-200 rounded w-24"></div>
</div>
</div>
))}
</div>
);
}
A vantagem é depender de zero bibliotecas, com ótima performance. A desvantagem é precisar escrever os estilos, o que dá um pouco mais de trabalho.
Opção 2: biblioteca react-loading-skeleton (a mais rápida)
Se você quer resolver rápido e escrever menos estilo, pode usar react-loading-skeleton:
npm install react-loading-skeleton
// app/blog/loading.tsx
'use client';
import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';
export default function Loading() {
return (
<div className="max-w-4xl mx-auto p-6">
{[1, 2, 3].map((i) => (
<div key={i} className="mb-8">
<Skeleton height={32} width="75%" className="mb-4" />
<Skeleton count={2} />
<div className="flex gap-4 mt-4">
<Skeleton width={80} />
<Skeleton width={100} />
</div>
</div>
))}
</div>
);
}
Essa biblioteca é prática e a animação já vem bem resolvida. Em projetos pequenos, eu uso bastante.
Opção 3: shadcn/ui (a mais alinhada ao design system)
Se o projeto já usa shadcn/ui, o componente Skeleton dele é a opção mais conveniente:
npx shadcn-ui@latest add skeleton
// app/blog/loading.tsx
import { Skeleton } from '@/components/ui/skeleton';
export default function Loading() {
return (
<div className="max-w-4xl mx-auto p-6">
{[1, 2, 3].map((i) => (
<div key={i} className="mb-8">
<Skeleton className="h-8 w-3/4 mb-4" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-5/6 mb-4" />
<div className="flex gap-4">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-3 w-24" />
</div>
</div>
))}
</div>
);
}
A vantagem é que o estilo já combina com o design system do projeto, sem ajustes extras.
Princípios de design para skeleton screens
Independentemente da opção escolhida, alguns princípios importam:
-
Combine com o layout real: a estrutura da skeleton screen deve refletir o conteúdo final. Se a lista de posts tem título, resumo e tags, a skeleton também deve representar esses três blocos.
-
Animação sutil: a animação não deve chamar mais atenção que o conteúdo. Uma pulsação discreta é suficiente. Animações exageradas distraem e podem aumentar a sensação de espera.
-
Quantidade razoável: normalmente, 3 a 5 itens de skeleton bastam. Não precisa preencher a tela inteira. Itens demais deixam a interface pesada.
Caso real: implementação completa de uma lista de posts
Agora vamos juntar os pontos anteriores e montar uma página completa de lista de posts.
Primeiro, loading.tsx:
// app/blog/loading.tsx
export default function BlogLoading() {
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="h-12 bg-gray-200 rounded w-1/3 mb-8 animate-pulse"></div>
<div className="space-y-8">
{[1, 2, 3].map((i) => (
<article key={i} className="border-b pb-8 animate-pulse">
<div className="h-8 bg-gray-200 rounded w-3/4 mb-3"></div>
<div className="space-y-2 mb-4">
<div className="h-4 bg-gray-200 rounded"></div>
<div className="h-4 bg-gray-200 rounded w-11/12"></div>
<div className="h-4 bg-gray-200 rounded w-4/5"></div>
</div>
<div className="flex gap-3">
<div className="h-6 bg-gray-200 rounded-full w-16"></div>
<div className="h-6 bg-gray-200 rounded-full w-20"></div>
</div>
</article>
))}
</div>
</div>
);
}
Depois, o page.tsx real, usando Server Component:
// app/blog/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store' // Garante que os dados sejam buscados novamente a cada vez
});
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">Posts do blog</h1>
<div className="space-y-8">
{posts.map((post) => (
<article key={post.id} className="border-b pb-8">
<h2 className="text-2xl font-semibold mb-3">
<a href={`/blog/${post.slug}`} className="hover:text-blue-600">
{post.title}
</a>
</h2>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<div className="flex gap-3">
{post.tags.map((tag) => (
<span key={tag} className="px-3 py-1 bg-gray-100 rounded-full text-sm">
{tag}
</span>
))}
</div>
</article>
))}
</div>
</div>
);
}
Percebeu? page.tsx virou uma função async e busca os dados diretamente dentro do componente. Nada de useState, nada de useEffect. O código fica muito mais limpo.
E como é Server Component, essa lógica roda no servidor e não aumenta o bundle do cliente. O primeiro carregamento fica mais rápido.
Dica de depuração: testar com React DevTools
Durante o desenvolvimento, você talvez queira ver a UI de loading por mais tempo. Mas os dados carregam tão rápido que o loading pisca e some.
Uma dica é usar o React DevTools para alternar manualmente a fronteira de Suspense.
- Instale a extensão React DevTools no navegador.
- Abra as ferramentas de desenvolvedor e vá para a aba Components.
- Encontre o componente
<Suspense>. - Clique com o botão direito e escolha “Suspend this Suspense boundary”.
Assim a UI de loading fica visível até você cancelar o suspend, e dá para ajustar o estilo com calma.
Sendo honesto, só descobri esse recurso depois de tropeçar no problema algumas vezes. Se soubesse antes, teria economizado bastante tempo.
Técnicas avançadas com Suspense
Configurar fronteiras de Suspense manualmente
loading.tsx é muito conveniente, mas às vezes você precisa de controle mais fino. Por exemplo: a página tem várias fontes de dados independentes, e você quer que cada uma mostre seu próprio loading, sem esperar todas ficarem prontas.
Nesse caso, você precisa configurar fronteiras de Suspense manualmente.
Primeiro, um erro comum: muita gente, eu inclusive no começo, coloca Suspense dentro do componente que busca os dados:
// ❌ Exemplo incorreto: Suspense está baixo demais
async function PostList() {
const posts = await fetchPosts();
return (
<Suspense fallback={<Loading />}> {/* Assim não funciona! */}
<div>
{posts.map(post => <Post key={post.id} {...post} />)}
</div>
</Suspense>
);
}
Isso não funciona. Por quê? Porque Suspense precisa estar em uma posição mais alta da árvore de componentes para capturar as operações assíncronas abaixo dele.
A forma correta é colocar Suspense no componente pai:
// ✅ Exemplo correto: Suspense está no componente pai
export default function BlogPage() {
return (
<div>
<h1>Posts do blog</h1>
<Suspense fallback={<PostListSkeleton />}>
<PostList />
</Suspense>
</div>
);
}
// O componente filho busca os dados
async function PostList() {
const posts = await fetchPosts();
return (
<div>
{posts.map(post => <Post key={post.id} {...post} />)}
</div>
);
}
Você pode imaginar Suspense como uma barreira. Ela fica em um ponto da árvore e observa todas as operações assíncronas abaixo. Se algum componente abaixo está esperando dados, a barreira fecha e mostra o fallback. Quando os dados chegam, ela abre e exibe o conteúdo real.
Tratamento especial para rotas dinâmicas
Esse ponto me pegou feio, então vale destacar.
Imagine uma página de produto /products/[id]. O usuário muda do produto A (id=1) para o produto B (id=2). Você percebe que loading.tsx não aparece.
O conteúdo troca diretamente de A para B, sem transição de loading. A experiência fica brusca.
Isso acontece por causa de uma otimização do React: se o tipo do componente é o mesmo, por exemplo ProductPage, ele reutiliza a instância e só atualiza as props. Então o Suspense entende que “o componente não mudou” e não entra novamente em suspend.
A solução é adicionar uma prop key ao Suspense, avisando ao React: “isso é um novo componente; renderize de novo”.
// app/products/[id]/page.tsx
import { Suspense } from 'react';
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<Suspense key={params.id} fallback={<ProductSkeleton />}>
<ProductDetail id={params.id} />
</Suspense>
);
}
async function ProductDetail({ id }: { id: string }) {
const product = await fetchProduct(id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>${product.price}</span>
</div>
);
}
Repare nesta linha: <Suspense key={params.id} ...>.
Agora, quando id muda, o React destrói a instância antiga de Suspense e cria uma nova. A nova instância entra em suspend novamente, e a UI de loading aparece como esperado.
Na época, fiquei meio dia preso nisso. Só depois vi alguém em uma GitHub issue mencionando esse truque com key. Testei e resolveu na hora. Às vezes a solução é simples, mas você precisa saber que ela existe.
Coordenar múltiplos estados de loading
Por fim, um cenário um pouco mais complexo: a página carrega várias fontes de dados ao mesmo tempo.
Imagine um dashboard com informações do usuário, estatísticas e atividades recentes. Cada parte chama uma API. Você tem duas estratégias:
Estratégia 1: mostrar tudo só depois que tudo carregar (um Suspense para tudo)
export default function Dashboard() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<UserInfo /> {/* Chama API 1 */}
<Statistics /> {/* Chama API 2 */}
<RecentActivity /> {/* Chama API 3 */}
</Suspense>
);
}
Vantagem: implementação simples, com exibição completa de uma vez.
Desvantagem: a API mais lenta atrasa tudo. O tempo de espera do usuário vira o tempo da API mais lenta.
Estratégia 2: exibição incremental (várias fronteiras de Suspense)
export default function Dashboard() {
return (
<div>
<Suspense fallback={<UserInfoSkeleton />}>
<UserInfo />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<Statistics />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</div>
);
}
Vantagem: as partes rápidas aparecem primeiro, e o tempo percebido de espera fica menor.
Desvantagem: a página pode “pular”, porque o layout muda à medida que o conteúdo carrega.
Minha escolha costuma depender da importância dos dados:
- Dados centrais, como informações do usuário, ficam dentro de um Suspense para aparecer juntos.
- Dados secundários, como recomendações ou anúncios, usam Suspense separado e carregam de forma assíncrona.
Assim, você preserva a experiência principal sem obrigar o usuário a esperar todos os dados.
Problemas comuns e soluções
O que fazer quando Suspense não funciona
Se Suspense não funciona, cheque estes pontos:
1. A forma de buscar dados é compatível?
Suspense só funciona com formas de busca compatíveis com Suspense. No App Router do Next.js, isso significa:
- ✅
awaitdireto em Server Component, que é a abordagem recomendada. - ✅ Bibliotecas com suporte a Suspense, como SWR e React Query.
- ❌
fetchdentro deuseEffect, que não é compatível. - ❌
Promise.thentradicional, que não é compatível.
2. A posição do componente está correta?
Suspense precisa ficar acima do componente que busca os dados, não dentro dele nem abaixo dele.
3. As versões são compatíveis?
Garanta que você está usando:
- React 18+
- Next.js 13+ com App Router
4. Como depurar
Use o React DevTools para alternar manualmente a fronteira de Suspense. Se nem a alternância manual funcionar, Suspense provavelmente não está ativo de verdade. Volte aos pontos anteriores.
A armadilha do hook useFormStatus
Se você usa Server Actions para envio de formulário, talvez use o hook useFormStatus para exibir o estado de envio.
Aqui há uma pegadinha: useFormStatus só funciona em Client Component.
Mas o form em si deve ser renderizado em Server Component; caso contrário, a Server Action não consegue ser vinculada.
Então a forma correta é: Server Component renderiza o form; Client Component mostra o estado.
// app/actions.ts
'use server';
export async function submitForm(formData: FormData) {
// Processa o formulário...
await saveToDatabase(formData);
}
// app/page.tsx (Server Component)
import { submitForm } from './actions';
import { SubmitButton } from './submit-button';
export default function Page() {
return (
<form action={submitForm}>
<input name="email" type="email" />
<SubmitButton />
</form>
);
}
// app/submit-button.tsx (Client Component)
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Enviando...' : 'Enviar'}
</button>
);
}
Repare: o form está no Server Component, e o button está no Client Component. Assim, a Server Action e o estado de loading funcionam corretamente.
O impacto do prefetch no loading
O componente <Link> do Next.js faz prefetch da página vinculada por padrão, quando o link aparece no viewport.
Por isso, às vezes você clica em um link e o loading pisca muito rápido ou nem aparece. Os dados já foram pré-carregados, então não há muito o que carregar.
Se você quiser testar o efeito de loading, pode desativar o prefetch temporariamente:
<Link href="/blog" prefetch={false}>
Blog
</Link>
Mas em produção, ainda recomendo manter o prefetch ligado, porque a experiência do usuário é melhor. Se você teme que o loading apareça rápido demais para ser percebido, pode adicionar um tempo mínimo de exibição, por exemplo 300ms, ou trocar o spinner por uma skeleton screen.
Resumo
Vamos recapitular os pontos principais:
-
loading.tsx é a melhor prática para loading no nível da rota: coloque o arquivo na pasta da rota e o Next.js cuida do resto. Você abandona o
useStatemanual e corta boa parte do código. -
Skeleton screen é melhor que spinner: ela antecipa o layout e reduz a ansiedade do usuário. Você pode implementar com CSS puro,
react-loading-skeletonou uma biblioteca de UI, dependendo do projeto. -
Suspense deve ficar acima na árvore de componentes: ele funciona como uma barreira que observa operações assíncronas abaixo. Se ficar no lugar errado, não entra em ação.
-
Em rotas dinâmicas, lembre-se da key: caso contrário, o loading pode não aparecer ao trocar o ID. A linha
<Suspense key={params.id}>faz diferença. -
Divida Suspense conforme as fontes de dados: dados centrais podem aparecer juntos; dados secundários podem carregar em paralelo. É um equilíbrio entre experiência e performance.
Sinceramente, sair do useState manual para loading.tsx não aumenta o trabalho. É trabalhar de forma mais inteligente. Menos código, menos bugs e uma experiência melhor para o usuário. Difícil reclamar.
Próximos passos
Se você quer testar agora, minha sugestão é:
Aja agora: pegue um projeto existente, escolha uma página de lista simples e troque o loading manual por loading.tsx. Fazer uma vez com as próprias mãos vale mais do que ler dez artigos.
Aprenda o próximo passo: depois de resolver loading, estude Error Boundaries. Eles formam um par natural com loading: um cuida do estado de carregamento, o outro cuida do estado de erro. Mais adiante pretendo escrever um artigo prático sobre Error Boundaries para continuar essa conversa.
Compartilhe a experiência: como você trata loading nos seus projetos? Qual solução usa? Quais armadilhas já encontrou? Compartilhe nos comentários para a gente trocar ideias.
Referências:
- Documentação oficial do Next.js - loading.js
- Documentação oficial do Next.js - Loading UI e Streaming
- Documentação oficial do React - Suspense
Fluxo completo para gerenciar estados de loading no Next.js
Use loading.tsx e Suspense para criar uma experiência de carregamento profissional e abandonar o useState manual.
⏱️ Estimated time: 1 hr
- 1
Step 1: Criar o arquivo loading.tsx
Crie loading.tsx no diretório da rota:
• app/dashboard/loading.tsx: estado de loading da rota dashboard
• app/products/[id]/loading.tsx: estado de loading de uma rota dinâmica
Conteúdo do arquivo:
export default function Loading() {
return <div>Carregando...</div>
}
O Next.js exibirá esse componente automaticamente enquanto a página carrega. - 2
Step 2: Implementar uma skeleton screen
Crie uma UI de carregamento mais profissional:
• Use um componente Skeleton para simular o layout do conteúdo
• Mantenha uma estrutura parecida com o conteúdo real
• Use animações para melhorar a experiência
Exemplo:
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-full mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
</div>
)
} - 3
Step 3: Usar Suspense para envolver componentes assíncronos
Use Suspense nos componentes:
• Envolva componentes que buscam dados de forma assíncrona
• Configure fallback para mostrar o estado de loading
• Use Suspense aninhado para carregamento granular
Exemplo:
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
Vários componentes podem ser envolvidos separadamente por Suspense:
• Cada componente carrega de forma independente
• Os rápidos aparecem primeiro; os lentos aparecem depois
• A experiência do usuário melhora. - 4
Step 4: Lidar com loading em rotas dinâmicas
Loading em rotas dinâmicas:
• Crie loading.tsx no diretório da rota dinâmica
• O Next.js lida automaticamente com o carregamento quando parâmetros mudam
• Não é preciso gerenciar o estado loading manualmente
Exemplo:
app/products/[id]/
├── loading.tsx # exibido automaticamente quando o parâmetro muda
└── page.tsx
Ao navegar de /products/1 para /products/2,
loading.tsx aparece automaticamente. - 5
Step 5: Otimizar a experiência de loading
Técnicas de otimização:
• Use skeleton screen em vez de um Spinner simples
• Mantenha a UI de loading alinhada ao layout do conteúdo real
• Use animação, como animate-pulse, para melhorar a percepção
• Use Suspense com critério para aproveitar streaming
Evite:
• Usar loading.tsx em todos os lugares sem necessidade
• Criar uma UI de loading complexa demais
• Ignorar tratamento de erros, como error.tsx. - 6
Step 6: Testar e validar
Pontos de teste:
• Teste o estado de loading durante navegação entre páginas
• Teste o loading quando parâmetros de rotas dinâmicas mudam
• Teste a experiência em rede lenta
• Verifique se a UI de loading é fluida
Checklist:
• Todas as rotas têm estados de loading adequados
• A UI de loading combina com o layout real
• Não há piscadas ou deslocamentos de layout
• A experiência do usuário é fluida.
FAQ
Qual é a diferença entre loading.tsx e escrever useState manualmente?
Quando loading.tsx é exibido?
Qual é a diferença entre Suspense e loading.tsx?
Como implementar uma skeleton screen?
Como tratar loading em rotas dinâmicas?
Posso personalizar o estilo do loading?
loading.tsx afeta a performance?
1 min de leitura · Publicado em: 5 jan 2026 · Atualizado em: 14 jul 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Configuração profissional no Next.js: ESLint + Prettier + Husky do zero
Seu PR foi devolvido na sexta à noite por problemas de formatação? O estilo inconsistente do time gera conflitos sem sentido? Este guia mostra como configurar ESLint, Prettier e Husky para automatizar verificações e formatação e tornar a colaboração mais eficiente.
Parte 31 de 51
Próximo
Como personalizar páginas de erro 404 e 500 no Next.js
Aprenda a criar páginas de erro no Next.js com not-found.tsx, error.tsx e global-error.tsx, exemplos completos, boas práticas de design e soluções para problemas comuns.
Parte 33 de 51





Comentários
Entre com GitHub para comentar