Como usar o compilador incremental do TypeScript para builds mais rápidos

1. Fundamentos do Compilador Incremental do TypeScript

O compilador incremental do TypeScript é um mecanismo que reduz drasticamente o tempo de rebuild ao reutilizar resultados de compilações anteriores. Diferentemente do modo completo, que recompila todos os arquivos do projeto a cada execução do tsc, o modo incremental mantém um cache inteligente do grafo de dependências entre módulos.

Internamente, o TypeScript gera um arquivo especial chamado .tsbuildinfo. Esse arquivo armazena informações detalhadas sobre:
- O grafo completo de dependências entre arquivos
- Hashes dos conteúdos compilados
- Metadados de versões de cada módulo
- Estado das opções de compilação utilizadas

Quando você executa tsc novamente, o compilador compara os hashes atuais com os armazenados no .tsbuildinfo. Apenas arquivos modificados e seus dependentes diretos são recompilados, enquanto módulos inalterados são reaproveitados do cache.

// Exemplo de estrutura do .tsbuildinfo (simplificada)
{
  "program": {
    "fileInfos": {
      "src/utils.ts": {
        "version": "abc123",
        "signature": "def456",
        "affectsGlobalScope": false
      },
      "src/app.ts": {
        "version": "ghi789",
        "signature": "jkl012",
        "imports": ["src/utils.ts"]
      }
    },
    "options": {
      "incremental": true,
      "target": "ES2020",
      "module": "commonjs"
    }
  }
}

Os benefícios são mais evidentes em projetos com centenas ou milhares de arquivos, onde rebuilds completos podem levar minutos. Com o modo incremental, o tempo cai para segundos na maioria dos cenários.

2. Ativação e Configuração Inicial

Para ativar o compilador incremental, adicione a opção no tsconfig.json:

{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./.cache/tsbuildinfo.json",
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

A opção "tsBuildInfoFile" permite definir o caminho e nome do arquivo de cache. Por padrão, o TypeScript cria o arquivo na pasta de saída (outDir) com o nome .tsbuildinfo. É recomendável armazená-lo em um diretório separado para facilitar a limpeza seletiva.

Para projetos maiores, combine com "composite": true e "declarationMap": true:

{
  "compilerOptions": {
    "incremental": true,
    "composite": true,
    "declarationMap": true,
    "tsBuildInfoFile": "./.cache/tsbuildinfo.json"
  }
}

A opção "composite" habilita referências entre projetos, enquanto "declarationMap" gera mapas de declaração que aceleram a navegação em editores.

3. Uso com Projetos Monorepo e Referências de Projeto

Em monorepos, o modo incremental brilha quando combinado com referências de projeto. Configure cada subprojeto com seu próprio tsconfig.json:

// packages/core/tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [
    { "path": "../utils" }
  ]
}
// packages/utils/tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

No tsconfig.json raiz, referencie todos os subprojetos:

{
  "files": [],
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/utils" }
  ]
}

Execute o build com tsc --build (ou tsc -b). Esse comando processa os projetos na ordem correta de dependências, respeitando o cache incremental de cada um. Para forçar rebuild completo, use tsc --build --force.

# Build incremental (apenas o necessário)
tsc --build

# Forçar rebuild completo
tsc --build --force

# Build seletivo de um projeto específico
tsc --build packages/core

4. Integração com Ferramentas de Build e Watch Mode

O modo watch (tsc --watch) já utiliza o compilador incremental por padrão. Cada salvamento de arquivo dispara uma recompilação apenas dos módulos afetados.

# Executar em modo watch com diagnóstico
tsc --watch --diagnostics

Para integração com bundlers como Webpack, use o ts-loader com transpileOnly: true combinado com fork-ts-checker-webpack-plugin:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: [
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true,
              happyPackMode: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new ForkTsCheckerWebpackPlugin({
      typescript: {
        incremental: true,
        tsBuildInfoFile: './.cache/tschecker.json'
      }
    })
  ]
};

Em task runners como Gulp, utilize gulp-typescript com incremental: true:

const gulp = require('gulp');
const ts = require('gulp-typescript');

gulp.task('build', function() {
  return gulp.src('src/**/*.ts')
    .pipe(ts({
      incremental: true,
      tsBuildInfoFile: './.cache/gulp-ts.json'
    }))
    .pipe(gulp.dest('dist'));
});

Para CI/CD, configure scripts que preservem o cache entre execuções:

# .github/workflows/build.yml
- name: Restore TS build cache
  uses: actions/cache@v3
  with:
    path: |
      .cache/
      dist/
    key: ${{ runner.os }}-ts-${{ hashFiles('**/tsconfig.json') }}

- name: Build
  run: tsc --build

5. Limpeza e Gerenciamento do Cache Incremental

O cache incremental pode ser invalidado em situações como:
- Mudanças em tsconfig.json (especialmente opções que afetam a saída)
- Atualizações de dependências em node_modules
- Alterações manuais em arquivos compilados

Para limpeza seletiva, crie scripts personalizados:

// package.json
{
  "scripts": {
    "clean:cache": "rm -rf .cache/ dist/",
    "clean:tsbuild": "find . -name '.tsbuildinfo' -delete",
    "rebuild": "npm run clean:cache && tsc --build"
  }
}

O comando tsc --build --clean remove todos os artefatos de build dos projetos referenciados:

# Remove dist e .tsbuildinfo de todos os subprojetos
tsc --build --clean

6. Diagnóstico e Monitoramento de Performance

Use --diagnostics e --extendedDiagnostics para monitorar o desempenho:

# Diagnóstico básico
tsc --diagnostics

# Diagnóstico estendido (mais detalhes)
tsc --extendedDiagnostics

Exemplo de saída de diagnóstico:

Files:                         245
Lines:                     45,678
Nodes:                    123,456
Identifiers:               89,012
Symbols:                   34,567
Types:                     56,789
Instantiations:            12,345
Memory used:              456 MB
I/O read:                  0.12s
I/O write:                 0.08s
Parse time:                0.45s
Bind time:                 0.23s
Check time:                1.67s
Emit time:                 0.89s
Total time:                3.24s

Para comparar builds completos vs. incrementais:

# Build completo (primeira execução)
tsc --build --force
# Total time: 12.5s

# Build incremental (após modificar um arquivo)
tsc --build
# Total time: 1.8s (redução de ~85%)

Identifique gargalos observando:
- Check time elevado: módulos grandes ou dependências circulares
- Parse time alto: muitos arquivos importados desnecessariamente
- I/O read/write excessivo: cache mal configurado

7. Boas Práticas e Limitações Conhecidas

Boas práticas:
- Sempre versione o tsconfig.json, mas nunca o .tsbuildinfo (adicione ao .gitignore)
- Use "skipLibCheck": true para evitar verificação de tipos em declarações de bibliotecas
- Combine com "noEmit": true em projetos que só verificam tipos
- Para projetos com muitos arquivos dinâmicos, considere "isolatedModules": true

{
  "compilerOptions": {
    "incremental": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "noEmit": true
  }
}

Limitações:
- Mudanças no tsconfig.json invalidam completamente o cache
- O modo incremental não acelera a primeira compilação (sempre é completa)
- Projetos com muitos arquivos gerados dinamicamente podem ter ganhos reduzidos
- Dependências circulares podem forçar recompilações em cascata

Armadilhas comuns:
- Esquecer de limpar o cache após atualizar node_modules
- Alterar opções de target ou module sem limpar o cache
- Usar outDir diferente entre execuções

Referências