pile·
백엔드·vercel-blogVercel Blog·

WebStreams를 14.6배 빠르게 만든 방법: fast-webstreams 구현기

Vercel 엔지니어링팀이 네이티브 WebStreams의 구조적 병목(청크마다 Promise 생성 + 마이크로태스크 큐 홉)을 파고들어 일부 시나리오에서 14.6배 빠른 `fast-webstreams` 라이브러리를 개발했다. 파이핑 최적화(중간 체인을 단일 pipeline으로 묶기)와 동기식 읽기(버퍼 데이터 즉시 반환)를 조합해 Promise 오버헤드를 제거했다. 이 작업은 Node.js 코어에도 17~20% 성능 개선을 제안하는 업스트림 PR로 이어졌다.

핵심 포인트
  • 네이티브 WebStreams는 청크마다 Promise 생성 및 마이크로태스크 큐 홉이 발생해 고처리량 스트리밍에서 CPU 낭비가 심하다
  • 파이핑 지연 최적화: pipe() 호출 시 즉시 연결 대신 링크 기록만 하다가 최종 pipeTo() 시점에 단일 Node.js pipeline()으로 묶어 청크당 Promise 제거
  • 동기식 읽기: 버퍼에 데이터가 이미 있으면 마이크로태스크 큐 없이 즉시 반환
  • 벤치마크: 읽기 루프 3.7배, 3개 변환 체인 9.7배, 응답 본문 포워딩 2.0배, 최고 14.6배 향상
  • React Server Components 렌더링 처리량: 110MB/s → 1,600MB/s (14.6배 개선)
  • Matteo Collina가 이 아이디어를 Node.js 코어에 PR로 올려 머지되면 모든 Node.js 사용자에게 17~20% WebStreams 개선 예정
상세 정리
  • 문제 배경: WebStreams는 Fetch API, Node.js, 브라우저 표준으로 널리 쓰이지만, 청크마다 Promise를 생성하는 구조가 고처리량 스트리밍에서 병목이 된다. Vercel은 Edge Runtime과 서버 컴포넌트 렌더링에서 이 문제를 직접 겪었다.
  • Promise 오버헤드 분석: 네이티브 ReadableStream.read()는 데이터 유무와 무관하게 항상 Promise를 생성한다. 스트림 체인(pipe)에서 청크당 Promise 생성이 중첩되어 CPU 사용이 급증한다.
  • 파이핑 지연 최적화: fast-webstreams는 pipe() 호출 시 즉시 연결하지 않고 업스트림 링크 목록에만 추가한다. 최종 pipeTo()가 호출될 때 전체 체인을 단일 Node.js stream.pipeline() 호출로 묶는다. 중간 체인의 청크당 Promise가 사라진다.
  • 동기식 읽기 구현: 버퍼에 데이터가 이미 있을 때 Promise.resolve(value)로 즉시 반환해 마이크로태스크 큐 진입을 건너뛴다. 이로써 이미 버퍼링된 데이터를 소비할 때 await 비용이 제거된다.
  • 벤치마크 결과: 읽기 루프(단순) 3.7배, 3개 변환 스트림 체인 9.7배, 응답 본문 포워딩 2.0배 개선. 특정 React Flight 렌더링 시나리오에서 14.6배 달성.
  • React Server Components 효과: 서버 컴포넌트 렌더링 처리량이 110MB/s에서 1,600MB/s로 늘었다. Vercel Edge Runtime이 이 라이브러리를 적용한 첫 사례다.
  • Node.js 업스트림 기여: Node.js 핵심 기여자 Matteo Collina가 이 프로젝트 아이디어를 Node.js 코어에 PR로 제안했다. 머지되면 별도 라이브러리 없이 모든 Node.js 사용자가 WebStreams에서 17~20% 성능 개선을 얻는다.
  • 배포 현황: npm 패키지 experimental-fast-webstreams로 제공. 장기 목표는 WebStreams 표준 구현이 충분히 빨라져 이 라이브러리가 필요 없어지는 것.
왜 읽나WebStreams 기반 스트리밍 서버, React Server Components, Edge Runtime 등 고처리량 스트리밍을 다루는 Node.js/서버 엔지니어에게 Promise 오버헤드를 제거하는 실전 최적화 원리와 라이브러리 수준의 구현 기법을 구체적으로 보여준다.
vercel-blog
Vercel Blog 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·github-engGitHub Engineering·

    조기 종료를 없애야 벡터화된다 — 메모리 속도 소스 코드 케이스 폴딩

    GitHub의 코드 검색 엔진 Blackbird는 480TB 이상의 소스 코드를 인덱싱하기 전 모든 바이트에 case folding을 적용한다. 이 글은 Rust로 구현한 case folding을 메모리 대역폭 한계(45+ GiB/s)까지 끌어올린 두 가지 반직관적 최적화를 상세히 다룬다. 핵심은 루프 조기 종료(break) 제거로 LLVM 벡터화를 유도하고, UTF-8을 디코딩하지 않고 바이트 공간 산술만으로 fold를 수행하는 것이다.

    #rust#unicode#simd+2
  2. 백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 — 전시 아키텍처 개선기 (2/3)

    여기어때 전시개발팀이 숙소 상세(PDP) API를 해부한 결과, 코드상으로는 DB 호출 3번처럼 보이던 요청이 실제로는 MongoDB $lookup 체인으로 컬렉션을 19회 접근하는 구조였다. 이 트랜잭션 스크립트 방식의 핵심 문제는 "aggregation이 I/O를 가린다"는 점으로, 독립적인 쿼리 10개가 단일 파이프라인에 직렬화되어 병렬화 기회를 잃고, 가격 때문에 거의 안 바뀌는 이미지까지 매 요청마다 읽어야 하는 읽기 증폭이 발생했다. V3에서는 "조회 시점 조립"을 "쓰기 시점 사전 조립"으로 전환하고, 화면별로 복제되던 가격 계산 로직을 goodsprice 단일 모듈로 수렴했다. 4개 API(PLP/PDP/RDP/ILP)의 반복 마이그레이션은 Claude Code skill로 절차를 고정하고 쉐도잉 + 동일성 검증으로 안전망을 마련하는 방식으로 진행됐다.

    #architecture#migration#caching+2