Telles Logo
Pular para o conteúdo
Ver todos os artigos
dez 12, 2023
·
views
·
likes
·
7 min

JavaScript: Seu Bundle Está Pesado Demais

Como analisar o tamanho do seu bundle, implementar code splitting, tree shaking e usar React.lazy com Suspense para reduzir o JS que chega ao usuário.

JavaScript é a categoria de recurso mais cara do frontend — não só em bytes, mas em tempo de processamento. Um arquivo de 300KB de imagem e um de 300KB de JS não são equivalentes: a imagem é apenas decodificada, enquanto o JS é baixado, parseado, compilado e executado.

"JavaScript é o recurso mais caro que você pode enviar para dispositivos móveis." — Addy Osmani

O Custo Real do JS

📡Download

Limitado pela rede. Em 4G, 300KB gzipped ~1s. Em 3G, pode ser 3-5s.

📖Parse

O browser lê o texto e constrói a AST. Linear com o tamanho do arquivo.

⚙️Compile

Converte AST em bytecode. Em dispositivos de entrada, pode ser 5-10x mais lento que em MacBook Pro.

▶️Execute

Roda o código. Long tasks bloqueiam interações do usuário durante a execução.

A regra dos 100KB

Um bundle de 100KB gzipped pode ser 300-400KB após descompressão. Em dispositivos lentos, parsear e compilar esses 300-400KB pode levar 2-3 segundos, bloqueando qualquer interação. Mantenha o JS crítico (para a rota atual) abaixo de 100-150KB gzipped.

Calculadora de Custo de JS

Antes de otimizar, entenda o impacto real do seu bundle em diferentes dispositivos. Arraste o slider para ver o custo estimado de parse + compile:

Calculadora de custo de JavaScript

Bundle size (gzipped)250 KB
10 KB500 KB1000 KB

Descomprimido: ~750 KB — parse + compile ocorre sobre o tamanho descomprimido

💻MacBook Pro
~540ms

⚠️ Atenção — usuários em dispositivos lentos sentem isso

📱Mid-range
~1.8s

Crítico — bloqueia o thread por mais de 600ms

📟Moto G4
~4.5s

Crítico — bloqueia o thread por mais de 600ms

Estimativas baseadas nos dados de pesquisa do Chrome team (Addy Osmani, 2019–2023)

Analisando seu Bundle

Antes de otimizar, você precisa ver o que tem no bundle.

# Next.js
npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});
ANALYZE=true npm run build

Abre um treemap interativo mostrando cada módulo e seu tamanho. Os vilões mais comuns: moment.js (70KB), lodash completo, @mui/icons-material, bibliotecas duplicadas.

Tree Shaking

Tree shaking elimina código não utilizado durante o build. Para funcionar, a biblioteca precisa usar ESM (import/export), não CommonJS (require).

// ❌ Importa o lodash inteiro — ~70KB gzipped
import _ from 'lodash';
const result = _.groupBy(items, 'category');
 
// ✅ Só o que usa — ~1KB
import groupBy from 'lodash/groupBy';
 
// ✅ date-fns tem tree shaking completo
import { format, parseISO } from 'date-fns';
Tree shaking quebrado

Algumas bibliotecas têm tree shaking quebrado apesar de usar ESM. O bundle analyzer revela isso — se você importa 2 funções e vê a biblioteca inteira no treemap, tree shaking não está funcionando. Verifique se a biblioteca tem sideEffects: false no package.json.

Code Splitting

Code splitting divide o bundle em chunks menores, carregando apenas o necessário para a rota atual.

import { lazy, Suspense, useState } from "react";

// Sem code splitting — importado sempre, mesmo sem precisar
// import HeavyChart from "./HeavyChart";

// Com code splitting — só carrega quando necessário
const HeavyChart = lazy(() =>
  new Promise((resolve) =>
    setTimeout(
      () =>
        resolve({
          default: function Chart() {
            return (
              <div style={{ padding: 24, background: "#1e1e2e", borderRadius: 8 }}>
                <h3 style={{ color: "#cba6f7" }}>📊 Gráfico Carregado</h3>
                <p style={{ color: "#cdd6f4" }}>
                  Este chunk só foi baixado quando o usuário clicou.
                  Sem code splitting, estaria no bundle inicial.
                </p>
                <div style={{ display: "flex", gap: 4, marginTop: 12 }}>
                  {[40, 65, 30, 80, 55, 90, 45].map((h, i) => (
                    <div
                      key={i}
                      style={{
                        width: 24,
                        height: h,
                        background: "#cba6f7",
                        borderRadius: 4,
                        alignSelf: "flex-end",
                      }}
                    />
                  ))}
                </div>
              </div>
            );
          },
        }),
      1200
    )
  )
);

export default function App() {
  const [show, setShow] = useState(false);

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <p style={{ color: "#a6e3a1", marginBottom: 12 }}>
        ✅ Bundle inicial menor — gráfico carregado sob demanda
      </p>
      <button
        onClick={() => setShow(true)}
        style={{
          padding: "8px 16px",
          background: "#cba6f7",
          border: "none",
          borderRadius: 6,
          cursor: "pointer",
          fontWeight: "bold",
          marginBottom: 16,
        }}
      >
        Carregar Gráfico
      </button>
      {show && (
        <Suspense
          fallback={
            <div style={{ color: "#fab387", padding: 16 }}>
              ⏳ Baixando chunk do gráfico...
            </div>
          }
        >
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

Dynamic imports para bibliotecas pesadas

// ❌ Importa o chart.js para todos os usuários
import Chart from 'chart.js';
 
// ✅ Só baixa quando o usuário precisar
async function showChart() {
  const { Chart } = await import('chart.js');
  new Chart(ctx, config);
}

Estratégia de chunking no Vite

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],  // cache independente do app
          charts: ['recharts', 'd3'],
          forms: ['react-hook-form', 'zod'],
        },
      },
    },
  },
};

Chunks vendor com immutable no cache ficam cacheados mesmo quando o código do app muda — o usuário não baixa novamente React e React DOM a cada deploy.

Rendering e Critical Rendering Path

O browser precisa construir DOM + CSSOM antes de renderizar qualquer pixel. Cada recurso que bloqueia esse processo atrasa o FCP e o LCP.

🎨CSS não bloqueia DOM

Mas bloqueia renderização. CSS externo no head deve ser crítico ou carregado assincronamente via preload trick

🟡JS bloqueia parser

Scripts síncronos param o parsing do HTML. Use defer (mantém ordem) ou async (executa ao carregar, sem garantia de ordem)

📐Layout Thrashing

Ler e escrever DOM no mesmo frame força reflows repetidos. Agrupe leituras antes das escritas usando requestAnimationFrame

🔴Main Thread livre

Tasks longas (>50ms) bloqueiam interação. Quebre em chunks com scheduler.yield() ou use Web Workers para processamento pesado

defer vs async vs type="module"

  • defer — download paralelo, executa após parse

    Mantém a ordem de execução entre scripts. Use para scripts que dependem do DOM ou uns dos outros.

  • async — download paralelo, executa ao carregar

    Sem garantia de ordem. Use para scripts completamente independentes: analytics, pixels de ad, chat widgets.

  • type="module" — download paralelo, executa após parse

    Comporta-se como defer por padrão. Use para ES Modules modernos. Também habilita top-level await.

Otimizações de Runtime

import { useTransition, useState } from 'react';
 
function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();
 
  function handleSearch(e) {
    setQuery(e.target.value); // urgente — atualiza input imediatamente
 
    startTransition(() => {
      setResults(filterResults(e.target.value)); // pode ser interrompido
    });
  }
 
  return (
    <>
      <input value={query} onChange={handleSearch} />
      {isPending && <Spinner />}
      <ResultsList results={results} />
    </>
  );
}

O React pode interromper o update de results se o usuário continuar digitando. Impacto direto no INP.


No próximo artigo, tratamos de CSS performance — critical path, layout thrashing, animações eficientes e como o browser pinta a tela.

As minhas notas

Selecione um trecho e use Destacar no menu (somente neste aparelho).

Artigos relacionados