# Rust WASM 파서를 TypeScript로 다시 작성했더니 3배 빨라졌다

> Clean Markdown view of GeekNews topic #27688. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=27688](https://news.hada.io/topic?id=27688)
- GeekNews Markdown: [https://news.hada.io/topic/27688.md](https://news.hada.io/topic/27688.md)
- Type: GN+
- Author: [xguru](https://news.hada.io/@xguru)
- Published: 2026-03-21T08:34:58+09:00
- Updated: 2026-03-21T08:34:58+09:00
- Original source: [openui.com](https://www.openui.com/blog/rust-wasm-parser)
- Points: 11
- Comments: 2

## Summary

Rust로 작성된 WASM 파서는 계산 자체는 빠르지만, **JS–WASM 경계에서의 직렬화·복사 오버헤드**가 전체 성능을 지배했습니다. 동일한 파이프라인을 TypeScript로 옮기자 호출당 2~4배 이상 빨라졌으며, 스트리밍 처리에서는 **문 단위 캐싱으로 O(N²) 구조를 O(N)**으로 단순화해 추가적인 3배 가까운 향상을 얻었습니다. 결과적으로 WASM은 계산 집약적 작업에는 유효하지만, JS 객체 파싱처럼 호출이 잦은 로직에는 부적합함이 명확해졌습니다.

## Topic Body

- **Rust로 작성된 WASM 파서**는 구조적으로 빠르지만, **JS-WASM 경계에서의 데이터 복사와 직렬화 오버헤드**가 성능 병목으로 드러남  
- **`serde-wasm-bindgen`을 통한 직접 객체 반환**은 JSON 직렬화보다 9~29% 느렸으며, 이는 런타임 간 세밀한 변환 비용 때문임  
- **TypeScript로 전체 파이프라인을 포팅**하자 동일한 아키텍처에서 **2.2~4.6배 빠른 단일 호출 성능**을 달성  
- 스트리밍 처리에서는 **문 단위 캐싱을 통한 O(N²)→O(N) 개선**으로 **2.6~3.3배 빠른 전체 처리 속도** 확보  
- 결과적으로, **WASM은 계산 집약적·저빈도 호출에 적합**하고, **JS 객체 파싱이나 잦은 호출 함수에는 부적합**함이 확인됨  

---

### Rust WASM 파서의 구조와 한계
- `openui-lang` 파서는 LLM이 생성한 DSL을 **React 컴포넌트 트리로 변환**하는 6단계 파이프라인으로 구성  
  - 단계: `autocloser → lexer → splitter → parser → resolver → mapper → ParseResult`  
  - 각 단계는 토큰화, 구문 분석, 변수 해석, AST 변환 등을 수행  
- Rust 코드 자체는 빠르지만, **JS↔WASM 간 문자열 복사·JSON 직렬화·역직렬화** 과정이 매 호출마다 발생  
  - 입력 문자열 복사(JS→WASM), Rust 내부 파싱, 결과 JSON 직렬화, JSON 복사(WASM→JS), JS에서 역직렬화  
- 이 경계 오버헤드가 전체 성능을 지배했으며, Rust 계산 속도는 병목이 아님  

### `serde-wasm-bindgen` 시도와 실패
- JSON 직렬화를 피하기 위해 **Rust 구조체를 직접 JS 객체로 반환**하는 `serde-wasm-bindgen`을 적용  
- 그러나 **30% 느려짐**이 관찰됨  
  - Rust 구조체 메모리를 JS가 직접 읽을 수 없고, 런타임 간 메모리 레이아웃이 달라 **필드 단위 변환**이 필요  
  - 반면 JSON 직렬화는 Rust 내부에서 한 번의 문자열 생성 후, JS에서 최적화된 `JSON.parse`로 처리  
- 벤치마크 결과  
  | Fixture | JSON round-trip | serde-wasm-bindgen | 변화 |
  | --- | --- | --- | --- |
  | simple-table | 20.5µs | 22.5µs | -9% |
  | contact-form | 61.4µs | 79.4µs | -29% |
  | dashboard | 57.9µs | 74.0µs | -28% |

### TypeScript로의 전환과 성능 향상
- 동일한 6단계 구조를 **TypeScript로 완전 포팅**, WASM 경계를 제거하고 **V8 힙 내에서 직접 실행**  
- 단일 호출 기준 벤치마크 결과  
  | Fixture | TypeScript | WASM | 속도 향상 |
  | --- | --- | --- | --- |
  | simple-table | 9.3µs | 20.5µs | 2.2배 |
  | contact-form | 13.4µs | 61.4µs | 4.6배 |
  | dashboard | 19.4µs | 57.9µs | 3.0배 |
- **WASM 제거만으로 호출당 비용이 대폭 감소**, 그러나 스트리밍 구조의 비효율은 여전히 존재  

### 스트리밍 파싱의 O(N²) 문제와 개선
- LLM 출력이 여러 청크로 전달될 때, 매번 전체 누적 문자열을 재파싱하는 **O(N²) 비효율** 발생  
  - 예: 1000자 문서를 20자씩 50회 파싱 → 총 25,000자 처리  
- 해결책으로 **문 단위 증분 캐싱(incremental caching)** 도입  
  - 완성된 문장은 캐시하고, 진행 중인 문장만 재파싱  
  - 캐시된 AST와 새 AST를 병합해 결과 반환  
- 전체 스트림 기준 벤치마크  
  | Fixture | 나이브 TS | 증분 TS | 속도 향상 |
  | --- | --- | --- | --- |
  | simple-table | 69µs | 77µs | 없음 |
  | contact-form | 316µs | 122µs | 2.6배 |
  | dashboard | 840µs | 255µs | 3.3배 |
- 문장이 많을수록 캐시 효과가 커지고, 전체 처리량이 선형적으로 개선  

### WASM 사용에 대한 교훈
- **적합한 경우**
  - **계산 집약적·상호작용 적은 작업**: 이미지·비디오 처리, 암호화, 물리 시뮬레이션, 오디오 코덱 등  
  - **기존 네이티브 라이브러리 이식**: SQLite, OpenCV, libpng 등  
- **부적합한 경우**
  - **JS 객체로 구조화된 텍스트 파싱**: 직렬화 비용이 지배적  
  - **짧은 입력을 자주 호출하는 함수**: 경계 비용이 계산보다 큼  
- 핵심 교훈  
  1. **병목 위치를 프로파일링 후 언어를 선택**해야 함  
  2. **`serde-wasm-bindgen`의 직접 객체 전달은 더 비쌈**  
  3. **알고리듬 복잡도 개선이 언어 전환보다 효과적**  
  4. **WASM과 JS는 힙을 공유하지 않으며**, 변환 비용은 항상 존재  

**최종 결과:** TypeScript 전환과 증분 캐싱으로 **호출당 2.2~4.6배, 전체 스트림 2.6~3.3배 성능 향상** 달성

## Comments



### Comment 53630

- Author: bbulbum
- Created: 2026-03-23T10:53:26+09:00
- Points: 1

고도의 Rust 성능개선 글을 돌려까는 취지가 아니었을까..

### Comment 53479

- Author: neo
- Created: 2026-03-21T08:34:58+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=47461094) 
- 진짜 핵심은 Rust보다 TypeScript가 아니라, **O(N²)에서 O(N)** 으로 줄인 스트리밍 알고리즘 수정임  
  문(statement) 단위 캐싱으로 이뤄진 이 변경만으로도 3.3배 향상이 있었음  
  언어 선택과는 별개로, 사용자 입장에서 느끼는 **지연(latency)** 개선의 주된 요인은 이 부분임  
  제목이 이런 흥미로운 엔지니어링 포인트를 과소평가한 느낌임
  - uv 프로젝트도 마찬가지임. 사람들은 “rust rulez!”만 외치지만, 실제 이득은 언어가 아니라 **알고리즘 개선**에서 나옴
  - 클릭베이트를 뚫고 본질을 짚어줘서 고마움  
    글 자체는 흥미롭지만, 요즘엔 과도한 **클릭 유도형 제목**에 지쳐 있음
  - n² 표현은 다소 과장된 듯함  
    각 호출 시간을 측정하고 **중앙값(median)** 을 쓰는 방식인데, 브라우저 환경에서 타이밍 공격 방어 로직이 있는 JS 엔진이라 정확도에 의문이 있음
  - 결국 **오해를 부르는 제목**에 가깝다고 생각함

- “언어 L에서 M으로 코드를 다시 썼더니 빨라졌다”는 말은 당연한 결과임  
  얽히고 잘못된 결정을 바로잡고, 새로 생긴 **더 나은 접근법**을 적용할 기회였기 때문임  
  사실 L=M이어도 마찬가지로, 속도 향상은 언어가 아니라 **리라이트와 재설계** 과정에서 나오는 것임
  - 이제 제3자가 원본을 모르는 상태에서 TypeScript 버전을 Rust로 다시 리라이트하면 또 성능이 오를지도 모름
  - 같은 언어로 다시 써도 **개선 효과**가 생기는 걸 자주 봄

- Rust와 JS 경계에서 객체 직렬화 성능을 개선하려고 더 깊이 파봤음  
  serde의 접근 방식이 성능상 좋지 않아 보였고, 이를 개선한 시도를 [내 블로그 글](https://neugierig.org/software/blog/2024/04/rust-wasm-to-js.html)에 정리했음

- Open UI가 WASM 관련 작업을 안 하는 이유가 궁금했음  
  그런데 이번 새 회사가 **Open UI**라는 이름을 써서 혼란스러웠음  
  원래 [Open UI W3C Community Group](https://open-ui.org/)은 5년 넘게 HTML의 popover, 커스터마이즈 가능한 select, invoker command, accordion 같은 표준을 만드는 그룹임  
  그들은 정말 훌륭한 일을 하고 있음

- “JSON 왕복을 건너뛰자”는 시도에서 serde-wasm-bindgen을 통합했다는데, 결국 **바이너리 형태의 JSON 재발명**처럼 보임  
  요즘 V8의 JSON은 이미 [매우 최적화](https://v8.dev/blog/json-stringify)되어 있고, [simdjson](https://github.com/simdjson/simdjson) 같은 구현은 초당 기가바이트 단위로 처리 가능함  
  JSON이 병목일 가능성은 낮다고 봄

- 그 블로그의 **디자인**이 정말 마음에 들었음  
  스크롤 위치에 따라 헤딩을 하이라이트하는 ‘scrollspy’ 사이드바가 특히 좋았음  
  Claude가 알려준 바로는 [fumadocs.dev](https://www.fumadocs.dev/)로 만든 것 같음
  - 흥미로움. 나도 곧 **좋은 문서 사이트**를 만들어야겠다고 생각함

- Rust WASM 파서의 목적이 잘 이해되지 않았음  
  글에서 그 부분이 명확하지 않아 더 설명이 필요함
  - LLM이 생성한 UI 컴포넌트를 정의하는 **전용 언어**를 사용한다고 함  
    이는 프롬프트 인젝션으로 인한 정보 유출을 막기 위한 것으로 보임  
    파서는 LLM에서 스트리밍되는 청크를 컴파일해 실시간 UI를 구성함  
    이전에는 청크마다 파서를 처음부터 다시 시작했는데, 이를 **점진적 처리 방식**으로 바꾸면서 (Rust→TypeScript 포팅 중) 성능이 크게 향상되었음

- TypeScript가 요즘 **Golang 기반**으로 돌아가는 게 아닌가 하는 의문이 있었음
  - TypeScript 컴파일러를 Go로 다시 쓰는 프로젝트가 진행 중인데, 그걸 말하는 것 같음

- 농담이지만, 다시 Rust로 리라이트하면 또 **3배 성능 향상**이 있을지도 모르겠음 /s
