Performance do compilador: project references e incremental
1. Introdução aos desafios de performance no compilador TypeScript
Projetos TypeScript de grande porte enfrentam um problema clássico: o tempo de compilação cresce proporcionalmente ao número de arquivos. O compilador tsc, por padrão, realiza uma recompilação total sempre que é invocado, reavaliando dependências, verificando tipos e emitindo código para todos os arquivos do projeto. Para codebases com centenas ou milhares de módulos, esse processo pode levar minutos, impactando negativamente o ciclo de desenvolvimento e a integração contínua.
O TypeScript oferece duas soluções complementares para mitigar esse gargalo: o modo incremental (--incremental) e as project references. Enquanto o primeiro otimiza compilações subsequentes através de cache, o segundo permite dividir um monorepo em projetos independentes, compilando apenas o que mudou.
2. Modo incremental (--incremental)
O modo incremental introduz um arquivo de metadados chamado .tsbuildinfo que armazena informações sobre a compilação anterior: quais arquivos foram processados, suas dependências e os resultados da verificação de tipos. Quando o tsc é executado novamente, ele compara as informações em cache com o estado atual dos arquivos, recompilando apenas aqueles que sofreram alterações.
Para ativar o modo incremental, adicione a seguinte configuração ao tsconfig.json:
{
"compilerOptions": {
"incremental": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
O arquivo .tsbuildinfo será gerado automaticamente no diretório de saída. Você pode personalizar seu nome e localização com "tsBuildInfoFile": "./cache/.tsbuildinfo".
Limitações: O modo incremental não oferece ganhos significativos quando mudanças estruturais afetam muitas dependências (ex.: alteração em uma interface base). Além disso, ele não resolve o problema de projetos monolíticos onde qualquer mudança exige revalidação de tipos global.
3. Project References: conceitos fundamentais
Project references permitem estruturar um codebase como uma coleção de projetos TypeScript menores, cada um com seu próprio tsconfig.json. O compilador entende as dependências entre esses projetos e pode compilar apenas aqueles que foram modificados, além de reutilizar declarações de tipos pré-compiladas.
Para que um projeto possa ser referenciado, ele deve definir:
composite: true: Habilita o modo de projeto composto, que exigedeclarationedeclarationMap.declaration: true: Gera arquivos.d.tsque expõem a API pública.declarationMap: true: Cria source maps para as declarações, facilitando a navegação no editor.
// core/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
O projeto consumidor referencia o projeto core usando o campo references:
// app/tsconfig.json
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../core" }
],
"include": ["src"]
}
4. Configurando project references na prática
Vamos criar um monorepo com dois projetos: core (biblioteca de utilitários) e app (aplicação principal). A estrutura de pastas será:
monorepo/
├── core/
│ ├── src/
│ │ └── utils.ts
│ ├── tsconfig.json
│ └── package.json
├── app/
│ ├── src/
│ │ └── main.ts
│ ├── tsconfig.json
│ └── package.json
└── tsconfig.base.json
Passo 1: Configure o projeto core com composite: true:
// core/tsconfig.json
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
Passo 2: Configure o projeto app com a referência:
// app/tsconfig.json
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../core" }
],
"include": ["src"]
}
Passo 3: Compile usando tsc --build. Este comando processa projetos na ordem correta de dependências, começando pelo core:
tsc --build app/tsconfig.json
O compilador primeiro compila core, depois app, reaproveitando as declarações geradas.
5. Otimizações avançadas com --build
O comando tsc --build oferece várias flags úteis:
--force: Recompila todos os projetos, ignorando o cache.--clean: Remove os artefatos de build de todos os projetos.--dry: Mostra o que seria compilado sem executar.
Para um pipeline CI/CD eficiente, você pode combinar project references com o modo incremental:
# No CI, compila apenas projetos alterados
tsc --build --incremental app/tsconfig.json
# Para rebuild completo (ex.: após alteração no tsconfig)
tsc --build --force app/tsconfig.json
A ordem de compilação é determinada automaticamente pelo grafo de dependências. Se core depende de lib, e app depende de core, o compilador processa lib → core → app.
6. Boas práticas e armadilhas comuns
Estrutura de pastas recomendada: Mantenha cada projeto em seu próprio diretório, com tsconfig.json na raiz. Evite aninhar projetos dentro de outros projetos.
Dependências circulares: Project references não suportam dependências circulares. Se o projeto A referencia B e B referencia A, o compilador emitirá um erro. Reavalie a arquitetura para remover ciclos.
Gerenciamento de outDir e rootDir: Cada projeto deve ter seu próprio outDir. O rootDir deve apontar para a pasta src do projeto. Isso evita conflitos de saída e garante que os source maps funcionem corretamente.
Problemas com --watch: O modo --watch com project references pode ser instável em algumas versões. Prefira usar tsc --build --watch para monitorar múltiplos projetos.
Quando não usar: Projetos pequenos (menos de 50 arquivos) ou monolíticos não se beneficiam de project references. O overhead de configuração supera os ganhos de performance.
7. Medindo e monitorando a performance
Use as flags de diagnóstico para entender onde o tempo de compilação é gasto:
tsc --build --diagnostics app/tsconfig.json
A saída mostra métricas como:
- Tempo de resolução: Quanto tempo leva para encontrar e carregar módulos.
- Tempo de verificação de tipos: Análise semântica e inferência.
- Tempo de emissão: Geração de arquivos .js e .d.ts.
Para métricas mais detalhadas, use --extendedDiagnostics:
tsc --build --extendedDiagnostics app/tsconfig.json
Comparação prática:
| Cenário | Tempo (1000 arquivos) |
|---|---|
| Sem otimizações | 45s |
--incremental |
12s (primeira), 3s (subsequente) |
| Project references | 8s (primeira), 2s (subsequente) |
| Ambos combinados | 6s (primeira), 1.5s (subsequente) |
8. Integração com ferramentas modernas (Bun, VS Code)
O modo incremental interage bem com o tsserver do VS Code, que usa o mesmo cache .tsbuildinfo para fornecer feedback rápido durante a edição. O editor consegue detectar alterações incrementais e revalidar tipos apenas nos arquivos modificados.
Para runtimes como Bun, que possuem seu próprio transpilador TypeScript, o tsc ainda é necessário para verificação de tipos. Configure scripts no package.json:
{
"scripts": {
"build": "tsc --build",
"build:fast": "tsc --build --incremental",
"typecheck": "tsc --noEmit",
"clean": "tsc --build --clean"
}
}
Em pipelines CI/CD, considere armazenar o cache dos arquivos .tsbuildinfo entre execuções:
# Exemplo de CI com GitHub Actions
- name: Cache TypeScript build info
uses: actions/cache@v3
with:
path: |
**/.tsbuildinfo
**/dist
key: ${{ runner.os }}-ts-${{ hashFiles('**/tsconfig.json') }}
Isso reduz drasticamente o tempo de build em ambientes de integração contínua, especialmente em monorepos com múltiplos projetos referenciados.
Referências
- Documentação oficial: Project References — Guia completo sobre project references, incluindo configuração e exemplos práticos.
- Documentação oficial: Compiler Options - incremental — Referência detalhada da opção
incrementale do arquivo.tsbuildinfo. - TypeScript Wiki: Performance — Dicas oficiais de performance, incluindo uso de project references e modo incremental.
- Artigo: How to Speed Up TypeScript Compilation — Tutorial prático sobre otimizações de compilação em projetos TypeScript de grande porte.
- TypeScript Deep Dive: Project References — Explicação detalhada com exemplos de monorepos e estratégias de build.
- Documentação: tsc --build — Referência sobre o modo
--builde suas flags (--force,--clean,--dry). - Artigo: Measuring TypeScript Compilation Performance — Post oficial do time TypeScript sobre métricas e diagnósticos de performance.