TypeScript를 Rust로 옮긴 tsrs — 진짜 노림수는 속도보다 Wasm, 하지만 Workers에선 Rust도 한 스레드다
Cloudflare의 TypeScript Rust 포트 tsrs를 요약하고, AI가 내게 빙의해 Go Wasm의 GC 비용과 Workers의 단일 스레드 제약을 따져 물었다.
Cloudflare가 TypeScript 컴파일러를 Rust로 옮긴 tsrs를 공개했다. 이미 Go로 10배 빨라진 TypeScript를 왜 또 옮기는지가 핵심이라, 질문은 AI가 내게 빙의해서 Wasm 쪽을 파고들게 했다.
Cloudflare가 TypeScript 컴파일러를 Rust로 옮긴 tsrs를 공개했습니다. VoidZero, Owner.com의 Max Schwenk와 함께 Oxc 툴체인 프로젝트로 키우고 있고, TypeScript 팀과도 협업한다고 해요.
- 성능: 같은 워커 수 기준으로 TypeScript 7.0(Go로 다시 쓴 버전)보다 최대 8배 빠르고 메모리는 약 70% 덜 씁니다. TypeScript 팀과 같은 설정으로 맞춰 쟀다고 강조하지만 Cloudflare 측 측정입니다. tsrs에서 찾은 최적화 일부는 이미 TypeScript 7.1 베타에 반영돼 7.0보다 최대 2.5배 빨라졌다고 합니다.
- 왜 Rust인가: 속도보다 WebAssembly가 핵심입니다. Go는 Wasm으로 만들 때 가비지 컬렉터까지 같이 들어가지만, Rust는 메모리를 가볍게 다룰 수 있어서 Workers 샌드박스나 브라우저 안에서 개발 도구 전체를 돌리기 좋다는 거예요. AI 에이전트용 격리 개발 환경을 염두에 둔 이야기입니다.
- 포크가 아님: 언어 설계는 TypeScript 팀이 정하고, tsrs는 원본을 충실히 따라갑니다. 매일 에이전트가 원본 변경을 검토하고 매일 밤 TypeScript 테스트를 돌려 차이를 찾는 방식으로 유지할 계획입니다. Next.js를 따라가는 Vinext와 같은 방식이에요.
npm install tsrs 로 실험판을 써 볼 수 있고, 진단 메시지 출력 순서처럼 아직 원본과 다른 부분이 남아 있다고 밝혔습니다.
Go도 Wasm으로 빌드되잖아, GC 같이 들어가는 게 실제로 얼마나 손해야?
파일 크기보다는 메모리와 병렬 처리 쪽 손해가 큽니다.
- 파일 크기: TinyGo 문서에 따르면 hello world 하나가 표준 Go로는 약 837KB, TinyGo로는 약 10KB입니다(예전 버전 기준 측정). 그 차이 대부분이 GC를 포함한 런타임이에요. 하지만 컴파일러처럼 큰 프로그램에서는 런타임 몇 MB가 전체에서 차지하는 비중이 작습니다.
- 메모리: Go는 Wasm 메모리 안에 자기 GC가 관리하는 힙을 따로 둡니다. 그런데 Wasm 메모리는 늘릴 수만 있고 줄일 수는 없어서, 한 번 커진 최대치가 그대로 남아요. 요청마다 짧게 도는 Workers에서는 이 최대치가 곧 비용이고 한도입니다.
- 병렬 처리: Go의 Wasm 빌드에서는 고루틴이 한 스레드에서만 돕니다. TypeScript 7.0은 타입 검사 워커 여러 개를 동시에 돌려서 빨라졌는데, Wasm에서는 이 이점을 거의 못 씁니다.
다만 기사의 벤치마크(8배 빠름, 메모리 70% 절약)는 네이티브끼리 비교한 것이고, Wasm에서 비교한 수치는 없습니다. 그러니 "Wasm에서는 Rust가 낫다"는 건 아직 정성적인 주장이에요. TypeScript 7의 비공식 Wasm 빌드(tsgo-wasm)는 있지만, 크기나 메모리를 잰 자료는 찾지 못했습니다.
출처: TinyGo 개요 · GopherCon 2019, Get Going with WebAssembly · tsgo-wasm
그럼 Rust로 만든 Wasm은 Workers에서 멀티스레드로 돌아가?
아니요, Workers 안에서는 Rust도 스레드 하나로 돕니다. Cloudflare가 Rust Workers 글에서 직접 "Workers는 단일 스레드이고 JS 이벤트 루프 안에서 돈다"고 썼고, Durable Object 안에도 "스레드는 정확히 하나"라고 밝혔어요.
그래서 Workers에서는 Go 빌드나 Rust 빌드나 똑같이 한 스레드입니다. 여기서 Rust가 내세우는 이점은 병렬 처리가 아니라 이쪽입니다.
- 메모리: GC 힙을 따로 두지 않아 메모리를 적게 씁니다. Workers의 메모리 한도 안에 넣기 쉬워요.
- 성능 예측: GC가 멈추는 순간이 없어서 실행 시간이 고르게 나옵니다.
병렬 처리가 꼭 필요하면 여러 Worker나 Durable Object로 일을 나눠 맡기는 구조를 짜야 할 것 같은데, 이건 제 추정입니다.
브라우저는 사정이 다릅니다. SharedArrayBuffer와 교차 출처 격리 헤더(COOP/COEP)를 갖추면 Wasm에서도 스레드를 쓸 수 있어요. 다만 tsrs의 Wasm 빌드가 실제로 스레드를 쓰는지는 확인하지 못했습니다.
기사의 "같은 워커 수 기준 8배"도 네이티브 비교입니다. 한 스레드뿐인 Workers 환경에서 얼마나 빠른지는 따로 재 봐야 알 수 있어요.
출처: Cloudflare, Rust Workers Emscripten 타깃 · web.dev, WebAssembly 스레드
정리
- tsrs는 TypeScript 7.0(Go) 대비 최대 8배 빠르고 메모리를 약 70% 덜 쓴다고 하지만, Cloudflare 측정이고 네이티브끼리 비교한 수치다.
- Rust를 고른 이유는 속도보다 Wasm이다. Go Wasm은 자기 GC 힙을 따로 두는데 Wasm 메모리는 줄일 수 없고, 고루틴도 한 스레드에서만 돈다.
- 그렇다고 Rust가 Workers에서 병렬로 도는 건 아니다. Workers는 단일 스레드라, Rust의 이점은 작은 메모리와 예측 가능한 성능 쪽이다.
- Wasm이나 한 스레드 환경에서의 성능 수치는 기사에 없어 따로 재 봐야 한다.
이 대화, 이어서 해 보기
글 주소와 함께 질문이 채워진 채로 열립니다.