A CI da sua aplicação vai ficando mais lenta com o tempo. Um pouco a cada mês, até o dia em que ela deixa de ser um incômodo e vira um impedimento.

Qual é o seu primeiro passo?

Vi isso acontecer semana passada.

A CI de um serviço Node + TypeScript levava 15 minutos. Testes de unidade com coverage, uns 1.200 testes.

E o raciocínio que aparece na hora é sempre o mesmo: se está lento, tem algo errado, deve dar pra otimizar. Atualiza o TypeScript pra versão em Go, que faz typecheck muito mais rápido. Troca o eslint e o prettier pelo ox, que são muito mais rápidos.

Nada disso é falso. O TypeScript em Go é absurdamente mais rápido. O oxlint é mais rápido que o eslint.

E mesmo assim não ia consertar nada.

O TypeScript 7 é tudo isso mesmo

O tsc era um compilador de TypeScript escrito em TypeScript, rodando em Node. A 7 é uma porta fiel dele para Go, em binário nativo, com type-checking paralelo em 4 threads.

Os números da Microsoft não são modestos: VS Code caiu de 125,7s pra 10,6s, Sentry de 139,8s pra 15,7s. No editor, abrir um arquivo com erros saiu de ~17,5s pra menos de 1,3s.

O JavaScript emitido é o mesmo, o sistema de tipos é o mesmo. Isso é uma história de compile-time, ponto final.

Um detalhe pra quem for migrar: a 7.0 não tem API programática, ela só chega na 7.1. Qualquer ferramenta que embute o compilador - typescript-eslint, Volar, e por tabela Vue, Svelte, Astro, Angular - continua no TypeScript 6.

A faca japonesa e o feijão

A lei de Amdahl explica por que trocar o compilador não ia mudar nada:

A lei de Amdahl: S igual a 1 dividido por ((1 - p) + p / s). Duas barras de mesmo comprimento comparam antes e depois: no ANTES, uma fatia estreita p e uma fatia larga 1 - p; no DEPOIS, a fatia p virou um risco fininho e a fatia 1 - p continua idêntica. Quando s cresce, S tende a 1 / (1 - p).
A fatia que você otimizou encolhe até virar um risco. A outra não se move - e é ela que define o total.

p é a fração do trabalho que você melhorou, s é o quanto melhorou. A parte que dói é o limite: se s tender ao infinito, o speedup máximo do sistema inteiro vira 1 / (1 - p).

Ou seja: o pedaço que você não otimizou é o seu teto. E ele não se move.

Pensa no almoço de domingo. Você leva 40 minutos. Compra uma faca japonesa cara que corta cebola 10x mais rápido, e a propaganda não mentiu. Só que cortar cebola levava 2 minutos, e agora leva 12 segundos.

O almoço continua levando 38 minutos e pouco.

Porque o que demora é o feijão na panela. E feijão não é problema de corte, é problema de espera. Faca nenhuma resolve espera.

Fizeram a atualização

Subiram tudo junto: eslint 8.57 -> oxlint 1.76, prettier 3.0 -> oxfmt 0.61, TypeScript 5.9 -> TypeScript 7.

passo antes depois
lint 5,56s 3,73s
format:check 6,51s 3,42s
typecheck 4,46s 4,04s
typecheck dos testes 4,50s 3,93s
total 21,03s 15,12s

As engines novas entregaram os 6x a 10x prometidos. O tsc 7 compila o projeto inteiro em 1,22s. O que come a diferença é o overhead fixo de cada invocação: subir o processo e carregar a ferramenta custa mais que o trabalho em si.

Resultado líquido: 5,91 segundos. De 900.

E olha que esses quatro passos somados eram 21s numa pipeline de 900s, ou seja p = 2,3%. Se as ferramentas fossem infinitamente rápidas, custo zero:

S igual a 1 dividido por (1 - 0,023) igual a 1,024x. Uma barra longa de 900s de pipeline com um risco vermelho fininho na ponta esquerda, marcado como 21s de lint, format e typecheck (p = 2,3%). Abaixo, uma barra quase do mesmo tamanho: 879s no melhor caso possível. Ganho líquido: 21 segundos.
Com ferramenta infinitamente rápida e custo zero, a pipeline de 900s cai pra 879s. Esse é o teto.

No cenário impossível, a CI cairia de 15min00 pra 14min39. Esse era o teto, e ele estava disponível antes de qualquer linha de código, com uma divisão de trinta segundos.

Isso não faz do benchmark da Microsoft mentira. O Slack cortou type-checking na CI de 7,5 minutos pra 1,25 minuto, só que lá é um passo dedicado de tsc, onde o compilador é 100% do trabalho. Mesmo compilador, mesmo ganho de engine, resultado oposto: a diferença não está na ferramenta, está no denominador.

Medir custa cinco minutos

O GitHub Actions já mostra a duração de cada step na UI, de graça. Abra a última run e leia antes de qualquer teoria. Depois divida o tempo do passo pelo tempo total: aquilo é o máximo que você pode ganhar atacando ele. Se der 2%, você acabou de economizar uma semana.

Se precisar cavar mais fundo, as ferramentas já existem e são subutilizadas de um jeito criminoso:

  • jest --verbose te dá a duração por arquivo. Quase sempre 3 arquivos respondem por metade do tempo.
  • jest --detectOpenHandles acha promise que não resolve e timer que não limpa, que deixam o runner sentado esperando o event loop drenar depois que os testes já acabaram. Existe faz anos.

O Knuth escreveu isso em 1974, na mesma página do Structured Programming with go to Statements de onde saiu o “premature optimization is the root of all evil” que todo mundo cita pela metade:

“É frequentemente um erro fazer julgamentos a priori sobre quais partes de um programa são realmente críticas, já que a experiência universal dos programadores que usam ferramentas de medição é que os palpites intuitivos deles falham.”

O problema é o pulo

Nada disso quer dizer “não atualize”. Atualize - o TypeScript 7 é um feito de engenharia impressionante e eu vou migrar tudo que der. Aqueles 6 segundos não mudam a CI, mas mudam o loop do agente, que roda typecheck e lint dezenas de vezes por hora. É ganho real, só que em outro número.

O que está errado é outra coisa: ir de “está lento” direto pra “troca por algo mais rápido”, sem a divisão de trinta segundos no meio. Velocidade de ferramenta não é velocidade de sistema.

E trocar ferramenta é confortável porque parece produtivo: tem PR, tem changelog, tem benchmark bonito pra mostrar. Medir primeiro parece burocracia.

Mas medir é o que separa engenharia de torcida.

Por hoje é só.