Shopping cart

Subtotal $0.00

View cartCheckout

Building better devs

TnewsTnews
Programação

Scriptc da Vercel Compila TypeScript para Binário Nativo Sem V8

Email : 15

A promessa que ninguém ousou fazer

Compila TypeScript. Gera um binário nativo. Sem Node. Sem V8. Sem engine JavaScript dentro do executável. Parece piada, mas é exatamente o que o Scriptc, projeto da Vercel Labs, entrega.

Quando o repositório apareceu no GitHub nesta semana, a reação inicial da comunidade foi previsível: “mais um compilador hypado que vai morrer em 3 meses”. Justo. A própria Vercel já enterrou o ZeroLang, que acumulou 1.200 commits e parou do nada em junho. Mas o Scriptc tem algo que esses projetos abandonados não tinham: 800 testes de paridade byte a byte contra o Node.js, binários de 320KB que iniciam em 4 milissegundos, e um pipeline de compilação que faz qualquer dev de sistemas levantar a sobrancelha.

Eu não costumo me empolgar com ferramentas novas. Depois de ver dezenas de “Node.js killers” irem pro cemitério, meu ceticismo default é alto. Mas o Scriptc mexeu comigo por um motivo específico: ele não tenta substituir o Node. Ele elimina o runtime inteiro.

Como o Scriptc funciona (sem enrolação)

O pipeline é direto:

  1. Seu código TypeScript passa pelo compilador oficial do TypeScript para checagem de tipos
  2. O código é rebaixado para uma representação intermediária (IR)
  3. A IR é convertida para C
  4. O Clang compila o C em binário nativo

É isso. Sem mágica, sem VM escondida, sem interpretador rodando por baixo dos panos.

O compilador opera em três camadas, que a Vercel chama de “tiers”:

Tier Comportamento Resultado
Tier 1 (Static) Compila para código nativo puro Sem engine no binário
Tier 2 (Dynamic) Embarca o QuickJS-NG (~620KB) Para código que não pode ser estático
Tier 3 (Rejected) Falha na compilação Com erro específico e sugestão de rewrite

O Tier 1 é o padrão. Se seu código usa classes, closures, async/await, a stdlib, ou APIs do Node como fs, path, process e http, ele compila direto para nativo. Nada de engine JavaScript no executável final.

O Tier 2 existe para a realidade: quando você precisa de dependências npm que distribuem JavaScript puro, ou quando o código é muito dinâmico para compilar estaticamente. Nesse caso, você passa a flag --dynamic e o compilador embarca o QuickJS-NG, um engine leve de ~620KB. Compare com o V8 do Node, que sozinho já ocupa dezenas de megabytes.

O Tier 3 é onde o Scriptc mostra maturidade: em vez de compilar errado ou gerar comportamento inesperado, ele rejeita o código, mostra um código de erro específico, e sugere como reescrever.

Os números que importam

Vamos aos benchmarks, rodados em hardware Apple M-series:

Métrica Scriptc Node.js Go Nota
Startup ~2.4ms ~47ms ~2ms Scriptc empata com Go
Binário (hello world) ~320KB ~120MB (SEA) ~2MB 375x menor que Node SEA
Memória 1 a 4MB 67 a 116MB ~8MB Até 29x menos que Node

O hello world do Node como Single Executable Application (SEA) pesa entre 60 e 120MB. O do Scriptc? 320KB. Não é typo. Trezentos e vinte kilobytes.

Startup de 2.4ms significa que você pode usar TypeScript para CLIs que precisam responder rápido. Scripts que hoje levam 47ms só para o Node inicializar passam a rodar em tempo equivalente ao de um binário Go ou Rust.

A memória é outro ponto brutal. Se você roda containers, sabe o peso que o Node carrega. Um processo Node típico consome 67 a 116MB de RAM antes de executar qualquer linha do seu código. O Scriptc comprime isso para 1 a 4MB.

O que compila e o que não compila

A lista de features suportadas é surpreendentemente longa:

Linguagem:

  • Classes com herança simples e dispatch dinâmico
  • Closures com semântica de captura do JavaScript
  • Generics (monomorphizados, como templates em C++)
  • Discriminated unions
  • async/await sobre fibers stackful
  • Exceptions com finally
  • Destructuring, spread, rest parameters
  • Getters/setters, iterators, template literals
  • Expressões regulares

Standard Library:

  • Strings com semântica UTF-16 exata (sim, incluindo surrogate pairs)
  • Arrays, Maps, Sets com ordenação JavaScript-exact
  • JSON com casts validados em runtime
  • Math, typed arrays, Buffer, Error hierarchies

APIs do Node.js:

  • Filesystem (sync e promises)
  • Path, process, child_process com streams
  • Crypto, URL, compression
  • Timers, signal handlers
  • O stack completo de rede: net, http, https, tls (usando mbedTLS), UDP, DNS
  • File watching e readline

Web Standards:

  • Fetch API
  • WHATWG subset (streams, Headers, AbortSignal)

Eu li essa lista três vezes porque parecia boa demais. Mas o mecanismo de testes diferenciais é real: 800+ programas rodam tanto no Node quanto como binário nativo, e a saída precisa ser idêntica byte a byte. stdout, stderr e exit code.

O elefante na sala: é tudo IA?

A discussão no Hacker News foi inevitável. Um commit único adicionou centenas de milhares de linhas de código. Vários devs apontaram que o codebase parece ter sido gerado por IA (provavelmente Claude), e as críticas foram pesadas. Um comentário resumiu o sentimento: “isso fede a psicose de IA com armas nucleares”.

É uma crítica válida? Parcialmente.

O código gerado por IA tem um problema real de manutenção a longo prazo. Se a equipe não entende profundamente cada decisão arquitetural do compilador, qualquer bug em edge cases vai ser um pesadelo. E compiladores são 90% edge cases.

Por outro lado, o mecanismo de testes é sólido. Testes diferenciais contra o Node, com verificação de memory safety via AddressSanitizer e auditoria de reference counting. Se vaza memória ou tem use-after-free, o build falha. Isso não é vibe coding: é engenharia com safety net.

A pergunta certa não é “IA escreveu o código?” e sim “os testes cobrem os cenários que importam?”. E a resposta, pelo menos olhando o que está público, é que sim.

Comptime: execução em tempo de compilação

Uma feature que passou despercebida pela maioria é o comptime:


const config = comptime(() => {
  // Esse código roda no build, não no runtime
  return JSON.parse(readFileSync('config.json', 'utf-8'));
});

O comptime executa TypeScript durante a compilação, dentro de uma VM isolada no compilador, e injeta o resultado como literal no binário. Se você conhece comptime do Zig, é exatamente a mesma ideia.

Na prática, isso significa que configurações, feature flags, e qualquer dado estático pode ser resolvido em build time, eliminando I/O e parsing em runtime.

FFI nativo: chamando C direto do TypeScript

Com a flag --ffi, você pode fazer chamadas diretas ao C ABI a partir de declarações TypeScript:


// Declaração type-safe
declare function sqlite3_open(filename: string, db: Pointer): number;

// Uso direto, sem binding manual
const db = new Pointer();
sqlite3_open("data.db", db);

Isso abre possibilidades que antes exigiam N-API ou node-ffi: bindings nativos para SQLite, OpenGL, CUDA, qualquer biblioteca C. Sem overhead de marshalling entre JavaScript e nativo, porque não tem JavaScript rodando.

Pensa no que isso significa na prática. Hoje, se você quer chamar uma lib C do Node, precisa escrever um addon com N-API em C/C++, compilar com node-gyp (boa sorte com isso no Windows), e lidar com o overhead de crossing boundary entre o V8 e o código nativo. Com Scriptc, a chamada é direta. O TypeScript vira C, e a chamada FFI é uma chamada de função normal no binário final.

Runtime validation: casts que funcionam de verdade

Uma decisão de design interessante: o Scriptc trata as casts do TypeScript como contratos verificáveis. No TypeScript convencional, as é uma mentira que o compilador aceita sem questionar:


// No TypeScript normal, isso compila sem erro
// mas explode em runtime se o JSON não tiver "name"
const user = JSON.parse(data) as User;

No Scriptc, esse as vira uma validação real. Se o JSON não tem a estrutura esperada, o programa lança um TypeError com o caminho exato do campo que falhou. É como ter Zod embutido no compilador, sem dependência extra, sem schema duplicado.

Isso sozinho já justifica experimentar a ferramenta. Quantos bugs em produção nascem de um as any ou um as Config que nunca foi validado?

Coverage: saiba exatamente o que compila

O comando scriptc coverage analisa seu código e mostra, statement por statement, o que compila estaticamente e o que cairia pro Tier 2:


$ scriptc coverage src/server.ts

Statements: 94.2% static (1,247 / 1,324)
Dynamic sites:
  src/plugins/legacy.ts:42  - eval() call
  src/utils/reflect.ts:18   - Reflect.get with computed key
  src/vendor/old-lib.ts:*   - untyped npm dependency

É uma ferramenta de diagnóstico que te mostra exatamente onde seu código perde performance. Refatore os pontos dinâmicos, rode coverage de novo, e veja o percentual subir. Gamificação involuntária de código limpo.

Por que a comunidade está cética (e por que faz sentido)

O ceticismo do HN não é gratuito. Existem problemas reais:

1. O QuickJS como fallback é lento

Quando o código cai pro Tier 2 (dynamic), o engine embarcado é o QuickJS-NG. Ele é leve (620KB), mas a performance é ordens de magnitude inferior ao V8. Se metade do seu código precisa de --dynamic, o ganho de performance evapora.

2. A Vercel tem histórico de abandonar projetos

O ZeroLang ganhou 1.200 commits e morreu silenciosamente. O Turbopack levou anos para atingir paridade com o Webpack. A Vercel é ótima em gerar hype, nem tão boa em manter projetos de longo prazo que não alimentam diretamente o Next.js.

3. Competidores já existem

O Porffor, um projeto solo, já passa ~68% do Test262 (a suíte oficial de testes do JavaScript). O AssemblyScript faz TypeScript para WebAssembly há anos. O Static TypeScript da Microsoft Research explorou ideias similares. Nenhum ganhou tração massiva.

4. Compilar JavaScript é intrinsecamente difícil

JavaScript é uma linguagem projetada para ser interpretada. eval(), with, protótipos dinâmicos, coerção de tipos: tudo isso existe para ser resolvido em runtime. Compilar estaticamente significa ou ignorar esses features (limitando a compatibilidade) ou emular eles nativamente (eliminando o ganho de performance).

O Scriptc faz a primeira opção: rejeita o que não consegue compilar. É uma decisão pragmática, mas significa que seu código precisa ser “Scriptc-friendly” para aproveitar o Tier 1.

Onde o Scriptc faz sentido hoje

Se eu fosse apostar em cenários de uso imediato:

CLIs e ferramentas de dev

Binários de 320KB que iniciam em 2.4ms. Sem dependência de Node instalado. Distribuição é copiar um arquivo. Para ferramentas internas, scripts de CI/CD, e CLIs de linha de comando, isso é transformador.

Microserviços em containers

Imagens Docker minúsculas. Um binário Scriptc + Alpine Linux resulta em containers de poucos megabytes. O cold start em serverless seria praticamente instantâneo.

Edge computing

Cloudflare Workers, Deno Deploy, Vercel Edge Functions: todos executam JavaScript em runtimes leves. Com Scriptc, você poderia compilar para binários nativos que rodam direto no edge, sem engine.

IoT e embarcados

1 a 4MB de RAM. Sem runtime pesado. TypeScript compilado para ARM. Para dispositivos com recursos limitados, isso abre portas que o Node nunca conseguiu.

Scripts de automação e DevOps

Se você mantém scripts em Bash porque “Node é pesado demais para um script simples”, o Scriptc elimina essa desculpa. Escreva em TypeScript com type safety, compile para um binário estático, distribua sem dependências. O devops da sua equipe não precisa ter Node instalado para rodar a ferramenta.

Scriptc vs. o ecossistema atual

Ferramenta Abordagem Engine Binário típico
Node.js Interpreta JS via V8 V8 (~30MB) N/A (precisa do Node)
Bun Runtime rápido com JavaScriptCore JSC (~20MB) ~60MB (SEA)
Deno Runtime seguro com V8 V8 (~30MB) ~80MB (compile)
Scriptc Compila TS para nativo Nenhum (Tier 1) ~320KB
Porffor Compila JS para Wasm/nativo Nenhum Variável

A diferença filosófica é clara: Node, Bun e Deno são runtimes que carregam um engine JavaScript completo. Scriptc é um compilador que elimina o engine.

O que vem por aí

O repositório é Apache 2.0, o que significa que forks e contribuições são bem-vindos. O suporte primário é macOS ARM, com Linux e Windows via cross-compilation. A documentação no scriptc.dev ainda é básica, mas funcional.

O que eu gostaria de ver nos próximos meses:

  • Benchmarks em workloads reais (servidores HTTP com carga, processamento de dados)
  • Suporte melhorado para Linux como plataforma primária
  • Integração com o ecossistema Vercel (deploy direto de binários Scriptc no Edge)
  • Mais transparência sobre o papel da IA na geração do código

Se a Vercel mantiver o projeto vivo (grande “se”), o Scriptc tem potencial para mudar como pensamos sobre TypeScript em produção. Não como substituição do Node para apps complexas, mas como alternativa para os milhares de scripts, CLIs e microserviços que não precisam de 120MB de runtime para rodar um console.log.

A pergunta que fica: quantos dos seus projetos TypeScript realmente precisam de um engine JavaScript completo rodando por baixo?


Fonte de inspiração: Scriptc: TypeScript-to-Native Compiler (GitHub)

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Posts