Como Usar Performance de Build TypeScript: esbuild, swc e Otimizações de tsc em Produção Já leu

Performance de Build TypeScript: esbuild, swc e Otimizações de tsc A performance do build é um problema crítico em projetos TypeScript modernos. Quando você está desenvolvendo, minutos perdidos em compilação se transformam em horas perdidas ao longo de um mês. Este artigo explora três ferramentas principais que resolvem esse problema de formas diferentes: tsc (compilador oficial TypeScript), esbuild (builder ultra-rápido) e swc (alternativa Rust-based). Vamos entender quando usar cada uma e como otimizá-las. Entendendo o Problema: Por que tsc é Lento? O Custo do Type Checking O (TypeScript Compiler) faz duas coisas simultaneamente: verifica tipos e compila para JavaScript. A verificação de tipos é cara porque precisa análisar toda a árvore de tipos, resolver imports circulares e validar compatibilidades em tempo de compilação. Para um projeto com 10.000 arquivos TypeScript, essa análise pode levar 30-60 segundos. A compilação não é otimizada por padrão. O tsc faz transpilação simples (remove tipos e deixa JavaScript puro), mas não minifica, não faz tree-shaking

Performance de Build TypeScript: esbuild, swc e Otimizações de tsc

A performance do build é um problema crítico em projetos TypeScript modernos. Quando você está desenvolvendo, minutos perdidos em compilação se transformam em horas perdidas ao longo de um mês. Este artigo explora três ferramentas principais que resolvem esse problema de formas diferentes: tsc (compilador oficial TypeScript), esbuild (builder ultra-rápido) e swc (alternativa Rust-based). Vamos entender quando usar cada uma e como otimizá-las.

Entendendo o Problema: Por que tsc é Lento?

O Custo do Type Checking

O tsc (TypeScript Compiler) faz duas coisas simultaneamente: verifica tipos e compila para JavaScript. A verificação de tipos é cara porque precisa análisar toda a árvore de tipos, resolver imports circulares e validar compatibilidades em tempo de compilação. Para um projeto com 10.000 arquivos TypeScript, essa análise pode levar 30-60 segundos.

// exemplo-complexo.ts - Projeto real com tipos complexos
interface User {
  id: number;
  profile: {
    name: string;
    contacts: {
      email: string;
      phone?: string;
    }[];
  };
}

// Este código simples requer análise profunda de tipos
const user: User = {
  id: 1,
  profile: {
    name: "João",
    contacts: [{ email: "joao@example.com" }]
  }
};

export default user;

A compilação não é otimizada por padrão. O tsc faz transpilação simples (remove tipos e deixa JavaScript puro), mas não minifica, não faz tree-shaking avançado e não otimiza código dead. Isso significa que mesmo que você gaste 60 segundos compilando, ainda precisa passar por um bundler depois.

Limitações de Paralelização

O TypeScript não paraleliza bem o type checking. Mesmo com a flag --incremental, a primeira compilação é sequencial. Projetos monorepo sofrem especialmente porque precisam compilar múltiplos pacotes em série.

esbuild: A Revolução de Velocidade

Por que esbuild é 10-100x Mais Rápido?

O esbuild é escrito em Go, linguagem compilada que produz código máquina nativo. Ele ignora completamente a verificação de tipos (aquela que consome 80% do tempo do tsc) e apenas transpila e minifica. Para desenvolvimento, isso é ouro: você quer feedback rápido, não apenas compilação correta.

// Build com esbuild - setup básico
import * as esbuild from "esbuild";

await esbuild.build({
  entryPoints: ["src/index.ts"],
  bundle: true,
  minify: true,
  format: "esm",
  outfile: "dist/index.js",
  platform: "node",
  target: "es2020",
});

Este código compila um projeto TypeScript completo em menos de 100ms. Compare com tsc que levaria 10-30 segundos no mesmo projeto. A razão: esbuild não verifica tipos. Ele apenas remove as anotações de tipo e converte para JavaScript válido.

Configuração Prática para Desenvolvimento

Para desenvolvimento, você quer two-stage builds: use esbuild para feedback rápido e tsc apenas para type checking (sem emitir JavaScript).

// esbuild.config.ts - Build rápido para desenvolvimento
import * as esbuild from "esbuild";
import * as fs from "fs";

const isProduction = process.env.NODE_ENV === "production";

const buildConfig: esbuild.BuildOptions = {
  entryPoints: ["src/index.ts", "src/cli.ts"],
  outdir: "dist",
  platform: "node",
  format: "cjs",
  target: "es2020",
  bundle: false,
  sourcemap: !isProduction,
  minify: isProduction,
  logLevel: "info",
  external: ["./node_modules"],
};

if (!isProduction) {
  // Watch mode para desenvolvimento
  const context = await esbuild.context(buildConfig);
  await context.watch();
  console.log("⚡ esbuild watching...");
} else {
  // Build único para produção
  await esbuild.build(buildConfig);
  console.log("✅ Build completo");
}

Plugins esbuild para Casos Avançados

Plugins estendem esbuild para resolver imports especiais, carregar assets e customizar o pipeline.

// plugin-custom.ts - Exemplo de plugin esbuild
import * as esbuild from "esbuild";
import * as path from "path";
import * as fs from "fs";

const resolveEnvPlugin: esbuild.Plugin = {
  name: "resolve-env",
  setup(build) {
    // Intercepta imports de .env
    build.onResolve({ filter: /^@env$/ }, () => ({
      path: path.resolve("src/env.ts"),
      namespace: "env",
    }));

    // Define conteúdo do arquivo virtual
    build.onLoad({ filter: /.*/, namespace: "env" }, () => {
      const vars = Object.fromEntries(
        Object.entries(process.env)
          .filter(([k]) => k.startsWith("APP_"))
          .map(([k, v]) => [k, JSON.stringify(v)])
      );

      return {
        contents: `export const env = ${JSON.stringify(vars)};`,
        loader: "ts",
      };
    });
  },
};

await esbuild.build({
  entryPoints: ["src/index.ts"],
  bundle: true,
  outfile: "dist/index.js",
  plugins: [resolveEnvPlugin],
});

swc: Type Stripping com Rust Puro

Quando Usar swc em Vez de esbuild?

O swc (Speedy Web Compiler) é escrito em Rust e compila para WebAssembly. Diferente do esbuild que é um bundler completo, swc é principalmente um transpiler. Você o usa quando precisa: remover tipos rapidamente, aplicar transformações de AST customizadas, ou integrar com builderes existentes como webpack/Next.js.

// .swcrc - Configuração básica do swc
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "decorators": true,
      "dynamicImport": true
    },
    "transform": {
      "react": {
        "runtime": "automatic"
      }
    },
    "minify": {
      "compress": {
        "unused": true
      },
      "mangle": true
    }
  },
  "module": {
    "type": "commonjs"
  }
}

swc brilha em Next.js (que usa swc por padrão desde versão 12), Vite e builderes que precisam de transpilação incremental. Benchmarks mostram swc 20-30% mais rápido que esbuild em type stripping puro, mas menos flexível para bundling complexo.

Integração com Projetos Existentes

// build.ts - Substituir tsc por swc para transpilação rápida
import { exec } from "child_process";
import { promisify } from "util";
import * as path from "path";

const execAsync = promisify(exec);

async function buildWithSwc() {
  try {
    // Transpila TypeScript para JavaScript (sem type checking)
    await execAsync("swc src --out-dir dist --copy-files");

    // Rode type checking em paralelo, sem bloquear build
    execAsync("tsc --noEmit").catch(err => {
      console.error("❌ Type errors found:");
      console.error(err.stdout);
    });

    console.log("✅ Build com swc completo");
  } catch (error) {
    console.error("Build falhou:", error);
    process.exit(1);
  }
}

buildWithSwc();

Este padrão é poderoso: você obtém feedback de compilação em ~200ms (swc) e type checking roda em background. Desenvolvedor não bloqueia esperando.

Otimizações de tsc e Estratégias Híbridas

Configurações tsc para Melhor Performance

Mesmo usando tsc, várias opções reduzem o tempo de compilação. A chave é usar --incremental com --tsBuildInfoFile e limitar o escopo do type checking.

// tsconfig.json - Otimizado para build rápido
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020"],
    "moduleResolution": "node",

    // Incremental compilation - reutiliza builds anteriores
    "incremental": true,
    "tsBuildInfoFile": ".tsbuildinfo",

    // Pule verificação de tipos não utilizados em libs
    "skipLibCheck": true,

    // Não reemita se houver erros em dependências
    "noEmitOnError": false,

    // Type checking apenas se necessário
    "strict": true,
    "noImplicitAny": true,

    // Limite imports do lib
    "lib": ["ES2020"],

    "outDir": "./dist",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "removeComments": false
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Estratégia: tsc para Type Checking, esbuild para Build

Esta é a configuração mais prática para projetos grandes:

// build-hybrid.ts - Two-stage build: velocidade + segurança
import * as esbuild from "esbuild";
import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

async function hybridBuild(production = false) {
  console.log("🔨 Etapa 1: Build rápido com esbuild...");

  const start = Date.now();
  await esbuild.build({
    entryPoints: ["src/index.ts"],
    outdir: "dist",
    format: "cjs",
    platform: "node",
    bundle: false,
    minify: production,
    sourcemap: !production,
  });

  console.log(`✅ esbuild completado em ${Date.now() - start}ms`);

  if (production) {
    console.log("🔍 Etapa 2: Type checking com tsc...");
    try {
      await execAsync("tsc --noEmit");
      console.log("✅ Type checking passou");
    } catch (error) {
      console.error("❌ Type errors:", error);
      process.exit(1);
    }
  }
}

// No package.json
// "scripts": {
//   "build": "node build-hybrid.ts",
//   "build:prod": "NODE_ENV=production node build-hybrid.ts"
// }

hybridBuild(process.env.NODE_ENV === "production");

Resultado: builds de desenvolvimento em < 1 segundo, type checking em background. Produção ainda garante 100% type safety.

Monorepo: Compilação em Paralelo

// monorepo-build.ts - Compila packages em paralelo
import * as fs from "fs";
import * as path from "path";
import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

async function buildMonorepo() {
  const packagesDir = "packages";
  const packages = fs
    .readdirSync(packagesDir)
    .filter(f => fs.statSync(path.join(packagesDir, f)).isDirectory());

  console.log(`📦 Compilando ${packages.length} packages em paralelo...`);

  // Roda todos os builds simultaneamente
  const results = await Promise.allSettled(
    packages.map(pkg =>
      execAsync(`cd packages/${pkg} && npm run build`).then(
        () => `✅ ${pkg}`,
        err => `❌ ${pkg}: ${err.message}`
      )
    )
  );

  results.forEach(r => {
    if (r.status === "fulfilled") console.log(r.value);
    else console.error(r.reason);
  });

  const failures = results.filter(r => r.status === "rejected").length;
  if (failures > 0) process.exit(1);
}

buildMonorepo();

Conclusão

Aprendemos três lições críticas: Primeiro, performance de build é um multiplicador - economizar 30 segundos por build em uma equipe de 10 pessoas economiza 50+ horas por mês. Segundo, não existe ferramenta universal: use esbuild para builds rápidos (desenvolvimento), tsc para type safety (CI), e swc como intermediário eficiente em pipelines Next.js/Vite. Terceiro, a arquitetura hybrid (build rápido + type check assíncrono) oferece melhor relação custo-benefício que escolher apenas uma ferramenta.

Referências


Artigos relacionados