Você deve compilar TypeScript em binários únicos em 2026? O playbook de um CTO na era do Perry

Por Diogo Hudson Dias
Engineering leader in a São Paulo office evaluating a single-file executable on a Linux laptop with performance graphs on a second monitor.

Seu p95 não vai cair abaixo de 100 ms porque seu serviço Node precisa de 300 ms só para acordar. Sua equipe de operações está cansada de corrigir CVEs do glibc em quinze imagens base. E seu parceiro de devices quer um atualizador de 10–20 MB, não um tarball de 150 MB. Em 2019, isso era tolerável. Em 2026, é um imposto sobre cada cliente que você tem.

Uma onda de ferramentas está atacando o problema pela base. Perry compila TypeScript diretamente para executáveis usando SWC e LLVM. Javy compila JavaScript para módulos WASI que iniciam em poucos milissegundos. Deno pode compilar TS/JS em um binário único com V8 embutido. Até a AWS está experimentando um runtime em Rust compatível com Node e de baixa latência (LLRT).

Então aqui está a decisão que você precisa tomar como CTO: você deve mover algumas cargas de trabalho em TypeScript para deploys em binário único nos próximos dois trimestres? Não “reescrever tudo em Rust”. Não “continuar enviando imagens de 600 MB”. Uma decisão focada em números: quando compilar, onde isso paga, e como fazer sem quebrar suas equipes.

O menu de 2026 para entregar TypeScript sem um contêiner pesado

Vamos ser concretos sobre suas opções e o que você ganha pelo esforço.

1) Empacotar JS + runtime do Node em um arquivo (pkg, nexe)

  • O que é: Ferramentas como pkg e nexe empacotam seu app mais o runtime do Node em um único executável.
  • Por que ajuda: Um arquivo para distribuir. Funciona com a maioria das APIs do Node. Mudanças mínimas no código.
  • Números que vemos: Binários de 40–90 MB, cold starts tipicamente 120–300 ms em uma VM modesta (ainda inicializando V8/Node). Memória em idle ~40–80 MB.
  • Pontos de atenção: Continua sendo um runtime completo do Node; addons C++ e require() dinâmico geralmente funcionam, mas o startup não chega perto do nativo.

2) “deno compile” (V8 autocontido + APIs padrão)

  • O que é: deno compile produz um binário único com o runtime do Deno e seu programa. Suporte a TypeScript é de primeira classe.
  • Por que ajuda: APIs previsíveis e sandboxed. Melhor startup que Node em muitos casos. TS sem etapa de build.
  • Números que vemos: Binários de 25–80 MB dependendo dos recursos; cold starts ~60–150 ms; memória em idle ~30–60 MB.
  • Pontos de atenção: Você adota a standard library e o modelo de permissões do Deno. Migrar módulos específicos do Node pode dar trabalho.

3) JavaScript → WASI (Javy + Wasmtime)

  • O que é: Compile JS para um módulo WebAssembly que roda dentro de um runtime WASI como o Wasmtime. O Javy do Shopify é o caminho mais testado em produção.
  • Por que ajuda: Muito rápido no cold start (geralmente 5–20 ms), footprints de memória minúsculos (5–20 MB), modelo de capacidades granular. Ótimo para funções e plugins.
  • Números que vemos: Módulos .wasm de 1–5 MB; runtime hospedeiro de 3–10 MB. p50 de inicialização bem abaixo de 20 ms em x86 comum; a vazão em regime costuma ser menor que o JIT do V8, mas a latência vence para tráfego em rajadas.
  • Pontos de atenção: APIs do Node limitadas. Você está em um mundo de capacidades: sem filesystem, rede ou timers implícitos a menos que o host os forneça. Incrível para lógica de negócio “pura”; não é um drop-in para Express.

4) TypeScript → AOT nativo (Perry: SWC + LLVM)

  • O que é: Projetos como o Perry transpilem TS para um IR e emitem código nativo via LLVM.
  • Por que ajuda: Binários únicos pequenos e rápidos; sem runtime para inicializar. Potencialmente 10–50 ms de cold start, RSS compacto, fácil de assinar e distribuir.
  • Números que vemos: Ainda no começo, mas binários de 8–20 MB são viáveis; cold starts de 20–50 ms em AMIs Linux comuns; memória em idle frequentemente abaixo de 20 MB.
  • Pontos de atenção: Semântica de JS incompleta, suporte limitado ou inexistente a padrões dinâmicos de eval/reflexão e sem APIs core do Node. As restrições do ecossistema de bibliotecas são reais hoje.

5) Reescrever trechos críticos em Go/Rust (a opção nuclear)

  • O que é: Mover serviços sensíveis à latência para Go/Rust e manter orquestração/lógica de negócio em TS.
  • Por que ajuda: Desempenho, ferramentas e perfis operacionais comprovados. Cold starts em dezenas de milissegundos, imagens minúsculas com distroless.
  • Pontos de atenção: Custo de duas linguagens, impacto em contratação e atrito de migração.

Onde binários únicos realmente fazem você economizar

Nem todo serviço merece ser compilado. Foque em três perfis onde os números justificam a mudança.

1) Autoescalar para zero e serverless

  • Se sua função escala a zero e recebe tráfego espinhoso, cortar 150–300 ms do cold start vira economia real e melhora de conversão. Vimos o p95 de uma API em Lambda cair de ~420 ms para ~160 ms ao mover o handler para um módulo WASI baseado em Javy hospedado em um shim em Rust. Efeito de negócio: menos checkouts abandonados no mobile.
  • Lembre: a vazão geralmente favorece um V8 JIT aquecido. Para trabalho em rajadas e de curta duração, o startup domina; para carga sustentada, o JIT vence. Meça ambos.

2) Dispositivos de borda e instaladores offline

  • Frotas Windows com políticas EDR agressivas adoram binários únicos assinados. Menos DLLs, menos falsos positivos, allowlisting mais simples. Entregar um executável assinado de 12–20 MB vence negociar um instalador de 150 MB com o InfoSec a cada sprint.
  • Em gateways Linux, um binário estático ligado com MUSL dentro de um contêiner minúsculo distroless (< 10 MB) simplifica patches de CVE e reduz banda para updates over-the-air.

3) Ambientes regulados e disciplina de SBOM

  • É mais fácil manter um SBOM de alta qualidade e uma cadeia de proveniência para um artefato. Um único ELF que você pode cosign, atestar com SLSA v1.0 e escanear com Syft/Grype vence um grafo espalhado de camadas e dependências transitivas do npm de que você nem precisa em runtime.

Os trade-offs que você não pode ignorar

Binários únicos compram simplicidade e velocidade de startup, mas os custos são reais. Não faça isso às cegas.

  • Compatibilidade: Qualquer coisa que dependa de require() dinâmico, eval() ou addons nativos do Node será dolorosa ou impossível fora de um runtime completo do Node. Deno ameniza parte disso; WASI e compiladores AOT não.
  • Observabilidade: Stack traces, source maps e profiling ficam mais difíceis quando você abandona seu runtime usual. Verifique sua toolchain para simbolização e dumps de crash antes do rollout. Para WASI, você instrumentará o runtime hospedeiro para traces e métricas.
  • Vazão vs latência: O JIT do V8 pode vencer AOT para workloads pesados e estáveis. Se seu serviço fica quente atrás de um balanceador a 70% de CPU, mantenha um runtime JIT ou reescreva em Go/Rust.
  • Segurança não é automática: Lincar estaticamente com MUSL remove o loader dinâmico mas incorpora o que você compilou. Você precisa reconstruir agressivamente quando CVEs aparecerem. Menos peças móveis ≠ menos responsabilidades.
  • Atrito de equipe: Migrar para Deno ou WASI muda APIs e modelos mentais. Espere 2–6 semanas para engenheiros sêniores internalizarem as novas restrições.

Um plano pragmático de benchmark (4 semanas, um engenheiro)

Antes de se comprometer, faça um bake-off com seu próprio código e dados. Um IC sênior consegue te dar uma decisão em um mês.

  1. Escolha três microbenchmarks
    • HTTP JSON echo com 3 middlewares pequenos
    • Job de curta duração: validar e transformar um payload JSON de 200 KB
    • CLI que varre um diretório local (5k arquivos) e emite um sumário
  2. Implemente em quatro variantes
    • Node 20 + esbuild, empacotado com pkg ou nexe
    • Deno compile
    • Javy + host Wasmtime (shim em Rust fornecendo fs/clock mínimos)
    • Perry AOT (última estável que comporte seu código)
  3. Meça os mesmos quatro números
    • Cold start (do start do processo até pronto) em t4g.small e t3.small (ARM + x86)
    • Latência p95 sob rajadas (picos de 1–100 RPS)
    • Memória RSS em idle
    • Tamanho do artefato (binário + qualquer runtime que você precise enviar)
  4. Rode por três dias
    • Automatize com GitHub Actions, publique uma tabela no seu wiki. Repita no Windows 11, Ubuntu 22.04 e Amazon Linux 2023.

Espere ver algo assim (ilustrativo, não é dogma): cold start de Node/pkg 180–350 ms, 60–80 MB; Deno 80–150 ms, 30–60 MB; Javy/WASI 8–20 ms, 8–15 MB; Perry AOT 25–50 ms, 12–20 MB. Seu resultado pode variar por IO e uso de bibliotecas. Esse é o ponto — meça a sua realidade, não a minha.

Framework de decisão: quando compilar vs quando manter como está

Use isto como guia na sua próxima revisão de arquitetura.

  • Se a maioria das chamadas é fria ou fica quente por milissegundos, então mire WASI ou AOT para a camada de handler e mantenha a lógica pesada em um serviço aquecido. Divida o trabalho.
  • Se você precisa de compatibilidade drop-in com Node e só quer um artefato mais simples, então use pkg/nexe hoje e planeje uma trilha Deno para serviços que podem migrar. É o ganho de menor risco.
  • Se você opera em frotas Windows com políticas rígidas de AppLocker, então priorize binários únicos assinados (Deno compile ou AOT) e um canal de atualização de primeira classe.
  • Se seu serviço é CPU-bound e fica quente, então mantenha Node com V8 JIT ou mova o trecho crítico para Go/Rust. Perseguir um startup de 20 ms não vai importar com p95=700 ms de compute.
  • Se você precisa de isolamento e segurança por capacidades, então prefira WASI. Rodando extensões não confiáveis ou semi‑confiáveis? WASI + capacidades do host é mais seguro do que “diretórios de plugins” no Node.

Esboço de implementação: como entregar binários únicos de forma responsável

Builds e targets

  • Linux: Construa x86_64 e aarch64. Prefira MUSL para links estáticos onde as toolchains suportarem. Valide em Amazon Linux 2023 e Ubuntu 22.04.
  • Windows: Produza arquivos PE assinados com Authenticode. Teste no Windows 10/11 com EDRs comuns habilitados. Evite spawn de shells; isso dispara detecções.
  • macOS: Assine e faça notarization. Espere quirks do Gatekeeper. Use binários universais se realmente precisar de x86_64 e arm64.

Escolhas de empacotamento

  • Contêiner ou binário puro? Para K8s, entregue o binário dentro de uma imagem scratch/distroless para que o seu time de ops não invente um modelo de processo diferente. Para serverless ou desktops, binário puro está ok.
  • Configuração: Incorpore defaults razoáveis e depois sobrescreva via variáveis de ambiente ou um único arquivo TOML/YAML ao lado do binário. Não reintroduza o espalhamento de configuração estilo npm.
  • Assets estáticos: Embuta templates e pequenos datasets no build. Mantenha o total abaixo de 20 MB para updates rápidos; qualquer coisa maior deve ser buscada com verificações de integridade na primeira execução.

Segurança e proveniência

  • Assine tudo: Use cosign para contêineres e binários brutos. Anexe atestações SLSA v1.0 apontando para sua execução de CI.
  • SBOM: Gere com Syft. Mesmo que seja um arquivo único, documente a toolchain (versões de SWC, LLVM, Deno, runtime WASI).
  • Cadência de patches: Rebuilds mensais no mínimo; rebuilds de emergência para CVEs críticos em runtimes ou libc mesmo se você estiver estaticamente linkado.

Observabilidade

  • Logs: Sempre logue JSON para stdout/stderr. Seu runtime pode ser novo; seu pipeline de logs não deveria ser.
  • Métricas: Exponha um endpoint Prometheus para serviços de longa duração ou envie métricas no encerramento para funções/CLIs. Para WASI, instrmente o runtime hospedeiro e propague spans.
  • Crashes: Torne core dumps opt-in e documentados. Para AOT, entregue arquivos de símbolos separadamente. Para Deno, mantenha source maps e faça o mapeamento no Sentry ou no seu APM.

Atualizações

  • Desktop/edge: Use atualizações diferenciais e assinadas (zstd + bsdiff). Hospede em um CDN com metadata ao estilo TUF. Faça rollout em anéis graduais (1%, 10%, 50%, 100%).
  • Servidor: Mantenha blue/green com health checks. Binários únicos tornam rollbacks triviais — troque symlinks ou tags de imagem.

Um pequeno estudo de caso quase real

Uma de nossas equipes moveu um processador de webhooks em rajadas do Node 20 (Express + ajv) para um design dividido: um validador e roteador compilado com Javy rodando sob Wasmtime dentro de um host minúsculo em Rust, com enriquecimento pesado encaminhado para um serviço Node aquecido. Em instâncias AWS Graviton, vimos:

  • Tamanhos dos binários: Módulo wasm de 4,2 MB; runtime hospedeiro de 6,8 MB
  • Cold start: ~12 ms até o primeiro byte para a camada WASI (antes era ~280 ms)
  • Latência p95: 160–190 ms fim a fim sob rajadas (antes era ~410–480 ms)
  • Custo: ~27% de redução em compute para o mesmo perfil de tráfego devido a menos cold starts e footprints de instância menores

Trade-offs: bibliotecas exclusivas do Node ficaram no caminho quente; a equipe teve que aprender design baseado em capacidades. Levou 3 semanas até produção, incluindo CI, métricas e playbooks de on-call. Se pagou no ciclo de faturamento seguinte.

Como um pod nearshore pode reduzir seu risco

Se seu time core está ocupado entregando features, um pod nearshore de duas a quatro pessoas pode rodar o bake-off, ligar o CI e endurecer o primeiro serviço sem seu staff tirar o olho do roadmap. No Brasil você encontra engenheiros TypeScript-first que já entregaram em Deno, lutaram com code signing no Windows e podem trabalhar nos seus horários dos EUA com 6–8 horas de sobreposição. Tipicamente vemos 20–30% menor TCO do que staffing nos EUA para esse tipo de trabalho de plataforma, com o benefício adicional de que outra equipe absorve a curva de aprendizado de Perry/WASI para que seus times de produto não precisem.

O que fazer na segunda-feira

  • Escolha um serviço em rajadas ou CLI que te irrite hoje.
  • Levante o benchmark em quatro variantes e rode por uma semana.
  • Decida qual trilha pilotar: pkg/nexe para simplificação imediata, Deno para uma mudança de runtime conservadora, WASI para matar cold starts, ou Perry AOT onde ele compila hoje.
  • Reserve duas sprints para levar o vencedor à produção com hardening adequado, assinatura, SBOM e observabilidade.
  • >

Principais pontos

  • Binários únicos não são moda; são simplificadores operacionais e alavancas de latência quando você escolhe os workloads certos.
  • Deno compile, Javy/WASI e Perry AOT cobrem pontos diferentes na fronteira compatibilidade/latência. Escolha intencionalmente.
  • Espere cold starts de 5–50 ms com WASI/AOT, 60–150 ms com Deno e 120–300 ms com empacotadores de Node — em linhas gerais. Meça os seus.
  • Segurança e observabilidade não vêm de graça. Planeje assinatura, SBOMs, tratamento de crashes e métricas desde o dia um.
  • Um bake-off de 4 semanas com seu código basta para tomar uma decisão confiante, baseada em números — sem reescrita.

Author: Diogo Hudson Dias

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation