Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Passo 1: Instale e configure o analisador de bundle
// npm install -D @next/bundle-analyzer
// next.config.ts
import withBundleAnalyzer from "@next/bundle-analyzer";
const nextConfig = {
// sua configuração
};
export default withBundleAnalyzer({
enabled: process.env.ANALYZE === "true",
})(nextConfig);
// Passo 2: Analise
// ANALYZE=true npm run build
// Abre um treemap mostrando cada módulo e seu tamanho
// Passo 3: Importação dinâmica para componentes pesados do cliente
import dynamic from "next/dynamic";
const Chart = dynamic(() => import("@/components/Chart"), {
loading: () => <div className="h-64 animate-pulse bg-gray-100 rounded" />,
ssr: false, // Pula SSR para componentes que só funcionam no cliente
});
// Passo 4: Importações nomeadas para tree-shaking
import { format } from "date-fns"; // 4KB - faz tree-shake
// NÃO: import dateUtils from "date-fns"; // 72KB - importa tudoQuando usar isso: Quando sua página de destino carrega mais de 150KB de JavaScript compactado com gzip, quando o shell do seu aplicativo excede 300KB compactado com gzip, ou quando o Lighthouse sinaliza "Reduzir JavaScript não utilizado".
// ---- ANTES: Bundle inchado - 487KB de JS do cliente compactado com gzip ----
// page.tsx - tudo carregado antecipadamente
import { Chart } from "chart.js/auto"; // +180KB compactado com gzip
import moment from "moment"; // +72KB compactado com gzip
import _ from "lodash"; // +71KB compactado com gzip
import { Editor } from "@monaco-editor/react"; // +120KB compactado com gzip
import { motion } from "framer-motion"; // +44KB compactado com gzip
export default function DashboardPage() {
const [showEditor, setShowEditor] = useState(false);
const [data, setData] = useState(fetchDashboardData());
// lodash usado para uma função
const sortedData = _.sortBy(data.items, "date");
// moment usado para formatação
const dateStr = moment(data.lastUpdated).format("MMM DD, YYYY");
return (
<div>
<h1>Dashboard - Última atualização: {dateStr}</h1>
<Chart data={sortedData} />
<motion.div animate={{ opacity: 1 }}>
<p>Conteúdo animado</p>
</motion.div>
{showEditor && <Editor language="json" value={JSON.stringify(data)} />}
<button onClick={() => setShowEditor(true)}>Abrir Editor</button>
</div>
);
}
// ---- DEPOIS: Otimizado - 89KB de JS do cliente compactado com gzip (redução de 82%) ----
// page.tsx - Server Component por padrão (zero JS do cliente para busca de dados)
import { format } from "date-fns"; // 4KB - substitui 72KB de moment
import { DashboardClient } from "./DashboardClient";
export default async function DashboardPage() {
// Busca no lado do servidor - zero JS do cliente
const data = await fetchDashboardData();
// date-fns com importação nomeada - faz tree-shake para 4KB
const dateStr = format(data.lastUpdated, "MMM dd, yyyy");
// Ordena com JS nativo - substitui 71KB de lodash
const sortedData = [...data.items].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
return (
<div>
<h1>Dashboard - Última atualização: {dateStr}</h1>
<DashboardClient sortedData={sortedData} />
</div>
);
}
// DashboardClient.tsx - Componente cliente mínimo
"use client";
import dynamic from "next/dynamic";
// Importação dinâmica: Chart carregado apenas quando visível (economiza 180KB da carga inicial)
const Chart = dynamic(() => import("@/components/Chart"), {
loading: () => <div className="h-64 animate-pulse bg-gray-100 rounded" />,
ssr: false,
});
// Importação dinâmica: Editor carregado apenas quando o usuário clica no botão (economiza 120KB)
const Editor = dynamic(() => import("@monaco-editor/react").then((m) => m.Editor), {
loading: () => <div className="h-96 animate-pulse bg-gray-100 rounded" />,
ssr: false,
});
// Animação leve - CSS em vez de framer-motion (economiza 44KB)
// Ou: import { LazyMotion, domAnimation, m } from "framer-motion"
// LazyMotion carrega apenas 5KB em vez de 44KB
export function DashboardClient({ sortedData }: { sortedData: DataItem[] }) {
const [showEditor, setShowEditor] = useState(false);
return (
<>
<Chart data={sortedData} />
<div className="animate-fadeIn">
<p>Conteúdo animado</p>
</div>
{showEditor && <Editor language="json" value={JSON.stringify(sortedData)} />}
<button onClick={() => setShowEditor(true)}>Abrir Editor</button>
</>
);
}O que isso demonstra:
moment (72KB) substituído por importação nomeada date-fns (4KB) - economia de 68KBlodash (71KB) substituído por .sort() nativo - economia de 71KBframer-motion substituído por animação CSS - economia de 44KBclient (navegador), server (Node.js) e edge. Concentre-se no bundle do cliente, pois ele afeta o desempenho voltado para o usuário.next/dynamic criam chunks separados que são carregados sob demanda. O componente não é incluído no bundle inicial e é buscado quando renderiza pela primeira vez. O componente loading é exibido enquanto o chunk carrega.import { x } from "mod", mas não com CommonJS require(). Importações nomeadas permitem que o bundler analise estaticamente quais exports são usados.index.ts que re-exporta de vários módulos) podem impedir o tree-shaking se o bundler não conseguir provar que os efeitos colaterais estão ausentes. O campo sideEffects: false em package.json ajuda, mas evitar arquivos barrel para bibliotecas grandes é mais seguro.(marketing) e (app) criam bundles separados, garantindo que as páginas de marketing não carreguem código específico do aplicativo.React.lazy para projetos não-Next.js:
import { lazy, Suspense } from "react";
const HeavyChart = lazy(() => import("./HeavyChart"));
function Dashboard() {
return (
<Suspense fallback={<div className="h-64 animate-pulse bg-gray-100" />}>
<HeavyChart data={data} />
</Suspense>
);
}Importação dinâmica condicional baseada na viewport:
"use client";
import dynamic from "next/dynamic";
import { useInView } from "react-intersection-observer";
const HeavyWidget = dynamic(() => import("@/components/HeavyWidget"), {
ssr: false,
});
function LazySection() {
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "200px" });
return (
<div ref={ref}>
{inView ? <HeavyWidget /> : <div className="h-96" />}
</div>
);
}Analisando custos de dependências específicas:
# Verifique o custo de qualquer pacote npm antes de adicioná-lo
npx bundle-phobia-cli lodash
# lodash: 71.5KB minificado, 25.3KB compactado com gzip
# Alternativa: use o site bundlephobia.com
# https://bundlephobia.com/package/lodashSubstituições comuns para reduzir o tamanho do bundle:
| Biblioteca Pesada | Tamanho (compactado com gzip) | Alternativa Mais Leve | Tamanho (compactado com gzip) | Economia |
|---|---|---|---|---|
| moment | 72KB | date-fns (importações nomeadas) | 4KB | 68KB |
| lodash | 25KB | lodash-es (importações nomeadas) ou JS nativo | 0-2KB | 23KB+ |
| chart.js | 65KB | lightweight-charts ou importação dinâmica | 0KB inicial | 65KB adiado |
| framer-motion | 44KB | Animações CSS ou LazyMotion | 0-5KB | 39KB+ |
| axios | 13KB | Fetch nativo | 0KB | 13KB |
dynamic(() => import("./Component")) infere as props do componente a partir do export padrão do módulo importado.dynamic(() => import("./module").then((m) => m.NamedComponent)).loading recebe { error, isLoading, pastDelay } para personalização.Importações dinâmicas adicionam requisições de rede - Cada componente importado dinamicamente se torna uma requisição HTTP separada. Muitas importações dinâmicas em uma única página podem criar uma cascata de requisições. Correção: Agrupe componentes relacionados em um único chunk dinâmico, ou use prefetching.
SSR: false oculta conteúdo de rastreadores - Componentes com ssr: false são invisíveis para rastreadores de mecanismos de busca e durante a renderização inicial do HTML. Correção: Use ssr: false apenas para componentes verdadeiramente interativos (editores, canvas) que não podem renderizar no servidor.
Tree-shaking requer módulos ES - Bibliotecas CommonJS (require/module.exports) não podem ser submetidas a tree-shaking. Correção: Use a variante ES module quando disponível (lodash-es em vez de lodash, date-fns em vez de moment).
Re-exports de arquivos barrel - Um index.ts que faz export * from "./heavy-module" força o bundler a incluir o módulo inteiro, mesmo que você importe apenas uma função. Correção: Importe diretamente do arquivo de origem: import { fn } from "./lib/specific-module" em vez de import { fn } from "./lib".
Análise de bundle mostra apenas tamanho não comprimido - O treemap mostra tamanhos brutos dos módulos. O tamanho real da transferência depende da compressão gzip/brotli. Código rico em texto comprime bem; código binário ou minificado não. Correção: Verifique tanto o treemap quanto a aba Network para os tamanhos reais de transferência.
Code splitting prematuro - Dividir um componente de 5KB em uma importação dinâmica adiciona complexidade e uma requisição de rede para economia mínima. Correção: Importe dinamicamente apenas componentes que tenham pelo menos 30KB compactados com gzip ou que estejam atrás de interação do usuário (modais, editores, painéis de configurações).
| Abordagem | Compromisso |
|---|---|
next/dynamic | Integrado ao Next.js; lida com SSR, estados de carregamento; apenas Next.js |
React.lazy + Suspense | Agnóstico ao framework; sem suporte a SSR sem configuração extra |
| Splitting baseado em rota | Automático no Next.js; sem controle por componente |
| Server Components | Elimina JS do cliente inteiramente para componentes não interativos |
| Module federation | Compartilha código entre micro-frontends; configuração complexa |
| Import maps | Resolução de módulos nativa do navegador; suporte limitado do navegador |
Instale @next/bundle-analyzer, adicione-o ao next.config.ts e execute:
ANALYZE=true npm run buildIsso abre um treemap interativo mostrando cada módulo e seu tamanho em bundles do cliente, servidor e edge. Concentre-se no bundle do cliente.
next/dynamic: integrado ao Next.js, lida com SSR, fornece opções loading e ssrReact.lazy + Suspense: agnóstico ao framework, sem suporte a SSR sem configuração extraUse next/dynamic em projetos Next.js; use React.lazy em aplicativos React não-Next.js.
Apenas para componentes verdadeiramente interativos, que só funcionam no cliente, como editores de canvas, editores de código ou widgets de mapa que não podem renderizar no servidor. Componentes com ssr: false são invisíveis para rastreadores de mecanismos de busca e durante a renderização inicial do HTML.
Um index.ts que faz export * from "./heavy-module" força o bundler a incluir o módulo inteiro, mesmo que você use apenas uma função.
Correção: Importe diretamente do arquivo de origem:
import { fn } from "./lib/specific-module"; // Bom
import { fn } from "./lib"; // RuimDividir um componente com menos de 30KB adiciona complexidade e uma requisição de rede para economia mínima. Importe dinamicamente apenas componentes que tenham pelo menos 30KB compactados com gzip ou que estejam atrás de interação do usuário (modais, editores, painéis de configurações).
Tree-shaking requer a sintaxe ES module import/export para análise estática. CommonJS require/module.exports é dinâmico e não pode ser analisado estaticamente.
Correção: Use variantes ES module: lodash-es em vez de lodash, date-fns em vez de moment.
moment (72KB) -> date-fns importações nomeadas (4KB)lodash (25KB) -> lodash-es importações nomeadas ou JS nativo (0-2KB)axios (13KB) -> fetch nativo (0KB)framer-motion (44KB) -> Animações CSS ou LazyMotion (0-5KB)O Next.js cria automaticamente um chunk separado para cada página. Grupos de rotas como (marketing) e (app) criam bundles separados, garantindo que as páginas de marketing não carreguem código específico do aplicativo. Nenhuma configuração é necessária.
const Editor = dynamic(
() => import("@monaco-editor/react").then((m) => m.Editor),
{ ssr: false }
);O TypeScript infere as props do componente a partir do export nomeado do módulo importado.
import dynamic from "next/dynamic";
import { useInView } from "react-intersection-observer";
const HeavyWidget = dynamic(() => import("./HeavyWidget"), { ssr: false });
function LazySection() {
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "200px" });
return (
<div ref={ref}>
{inView ? <HeavyWidget /> : <div className="h-96" />}
</div>
);
}O treemap mostra tamanhos brutos (não compactados) dos módulos. O tamanho real da transferência depende da compressão gzip/brotli. Verifique a aba Network nas Ferramentas do Desenvolvedor para os tamanhos reais de transferência. Código rico em texto comprime bem; código binário ou minificado não.
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥