Bundle analysis e code splitting estratégico
1. Fundamentos do Bundle Analysis
A análise de bundle é o primeiro passo para entender o que realmente está sendo enviado ao navegador. Sem essa visibilidade, otimizações são feitas no escuro, muitas vezes resultando em esforço desperdiçado. O objetivo principal é responder perguntas como: "Por que meu bundle tem 2MB?", "Essa biblioteca de 500KB está sendo usada de fato?" e "O tree-shaking está funcionando corretamente?".
As ferramentas mais relevantes para essa análise são:
- Webpack Bundle Analyzer: Gera um mapa interativo do bundle, mostrando o tamanho relativo de cada módulo
- Source Map Explorer: Permite navegar pelo código original mapeado no bundle final
- Rollup Plugin Visualizer: Equivalente para projetos que usam Rollup ou Vite
As métricas-chave que devemos monitorar incluem: tamanho total do bundle (gzipped e não comprimido), dependências duplicadas entre chunks, e a eficácia do tree-shaking (módulos mortos que ainda são incluídos).
2. Configurando Análise de Bundle no Projeto
Integrar o Webpack Bundle Analyzer em um projeto React é direto:
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false,
}),
],
};
Para definir orçamentos de bundle e falhar o build se excedidos, usamos size-limit:
// package.json
{
"size-limit": [
{
"path": "dist/main.js",
"limit": "200 KB",
"running": false
},
{
"path": "dist/chunk-vendors.js",
"limit": "300 KB"
}
]
}
Ao analisar o relatório gerado, os "low-hanging fruits" mais comuns são:
- Bibliotecas inteiras importadas quando apenas funções específicas são usadas (ex: import moment from 'moment' vs import { format } from 'date-fns')
- Componentes visuais pesados (chart libraries, editores rich text) carregados em todas as páginas
- Polyfills desnecessários para navegadores modernos
3. Code Splitting Automático com React.lazy e Suspense
O React.lazy permite dividir componentes pesados em chunks separados que são carregados sob demanda:
import React, { Suspense } from 'react';
const HeavyChart = React.lazy(() => import('./HeavyChart'));
const DataTable = React.lazy(() => import('./DataTable'));
function Dashboard() {
return (
<Suspense fallback={<div className="skeleton-chart" />}>
<HeavyChart />
<Suspense fallback={<div className="skeleton-table" />}>
<DataTable />
</Suspense>
</Suspense>
);
}
Boas práticas importantes:
- Use fallbacks específicos (skeletons, spinners) em vez de um loader genérico
- Evite aninhar muitos Suspense sem necessidade — cada um adiciona overhead de renderização
- Para evitar "flash de loading", considere pré-carregar componentes críticos após o primeiro render:
import React, { useEffect } from 'react';
const HeavyChart = React.lazy(() => import('./HeavyChart'));
function Dashboard() {
useEffect(() => {
// Pré-carrega o componente após o carregamento inicial
import('./HeavyChart');
}, []);
return <Suspense fallback={<Spinner />}><HeavyChart /></Suspense>;
}
4. Code Splitting Baseado em Rota (Route-based Splitting)
Em aplicações React Router v6, o code splitting por rota é a estratégia mais impactante:
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Reports = lazy(() => import('./pages/Reports'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/reports" element={<Reports />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Para melhorar a experiência do usuário, podemos usar IntersectionObserver para pré-carregar rotas previstas:
import { useEffect, useRef } from 'react';
function usePreloadRoute(linkRef, routeImporter) {
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
routeImporter(); // Inicia o carregamento do chunk
observer.disconnect();
}
}, { rootMargin: '200px' });
if (linkRef.current) {
observer.observe(linkRef.current);
}
return () => observer.disconnect();
}, [linkRef, routeImporter]);
}
5. Code Splitting por Componente e Biblioteca
Bibliotecas pesadas como moment.js (330KB) ou lodash (530KB) podem ser carregadas apenas quando necessário:
// Em vez de import estático
// import moment from 'moment';
// Import dinâmico sob demanda
async function formatDate(dateString) {
const { format } = await import('date-fns');
return format(new Date(dateString), 'dd/MM/yyyy');
}
// Para Chart.js em um modal
function ChartModal({ isOpen, data }) {
const [ChartComponent, setChartComponent] = useState(null);
useEffect(() => {
if (isOpen) {
import('react-chartjs-2').then(mod => {
setChartComponent(() => mod.Chart);
});
}
}, [isOpen]);
if (!ChartComponent) return null;
return <ChartComponent type="line" data={data} />;
}
Para vendor chunk splitting no Webpack:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
name: 'vendor-stable',
chunks: 'all',
priority: 10,
},
chartLibs: {
test: /[\\/]node_modules[\\/](chart\.js|d3|echarts)[\\/]/,
name: 'chart-libs',
chunks: 'async',
priority: 5,
},
},
},
},
};
6. Técnicas Avançadas de Splitting no Node.js
No backend Node.js com Express, podemos aplicar lazy loading similar:
const express = require('express');
const app = express();
app.get('/api/reports/generate', async (req, res) => {
// Só carrega o módulo pesado quando a rota é chamada
const pdfGenerator = await import('./services/pdfGenerator.js');
const report = await pdfGenerator.generateReport(req.query);
res.setHeader('Content-Type', 'application/pdf');
res.send(report);
});
app.post('/api/images/process', async (req, res) => {
const sharp = await import('sharp');
const processed = await sharp(req.file.buffer)
.resize(800, 600)
.jpeg({ quality: 80 })
.toBuffer();
res.send(processed);
});
Para caching inteligente de módulos divididos:
const moduleCache = new Map();
async function getLazyModule(modulePath) {
if (moduleCache.has(modulePath)) {
return moduleCache.get(modulePath);
}
const module = await import(modulePath);
moduleCache.set(modulePath, module);
// Warm-up: executa funções de inicialização
if (module.warmUp) {
await module.warmUp();
}
return module;
}
7. Monitoramento Contínuo e Otimização Iterativa
Para automatizar a análise no CI/CD:
// .github/workflows/bundle-check.yml
name: Bundle Size Check
on: [pull_request]
jobs:
bundle-size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run build
- name: Check bundle size
run: npx size-limit --json > bundle-report.json
- name: Fail if over budget
run: |
if grep -q '"passed": false' bundle-report.json; then
echo "Bundle size exceeds budget!"
exit 1
fi
Para validar o impacto real no usuário, meça Web Vitals antes e depois:
// No navegador
import { onLCP, onFCP, onCLS } from 'web-vitals';
onLCP(console.log);
onFCP(console.log);
onCLS(console.log);
Em um caso real, aplicamos code splitting em gráficos Chart.js e reduzimos o bundle inicial de 1.2MB para 720KB (40% de redução). O LCP (Largest Contentful Paint) caiu de 4.2s para 2.1s, e a primeira interação ficou 60% mais rápida.
Referências
- Webpack Bundle Analyzer - Documentação Oficial — Guia completo de instalação, configuração e interpretação dos relatórios interativos de bundle.
- React.lazy e Suspense - Documentação React — Documentação oficial sobre lazy loading de componentes e boas práticas de Suspense.
- Code Splitting com React Router v6 — Tutorial oficial mostrando implementação de lazy loading por rota com React Router.
- size-limit - Bundle Size Budgets — Ferramenta para definir orçamentos de bundle e falhar builds automaticamente quando excedidos.
- Web Vitals - Medindo Performance Real — Guia do Google sobre métricas de performance (LCP, FCP, CLS) e como usá-las para validar otimizações.
- Dynamic Import em Node.js — Documentação oficial sobre import() dinâmico no Node.js para lazy loading de módulos.
- Tree Shaking no Webpack — Guia prático sobre como configurar tree-shaking e identificar módulos não utilizados no bundle.