V8의 JSON.stringify가 2배 빨라졌다 — 6가지 최적화 기법 해부
JSON.stringify는 JavaScript에서 가장 자주 호출되는 내장 함수 중 하나다. API 응답 직렬화, 로깅, 캐시 키 생성, 심지어 깊은 복사 트릭(JSON.parse(JSON.stringify(obj)))까지. 이렇게 많이 쓰이는 함수가 2배 빨라졌다면? V8 엔진 13.8(Chrome 138)에 포함된 이 최적화는 별도의 코드 변경 없이도 모든 JavaScript 애플리케이션에 적용된다.
얼마나 빨라졌나
JetStream2의 json-stringify-inspector 벤치마크 기준으로 2배 이상 빨라졌다. 비유하면 같은 도로, 같은 자동차인데 엔진 튜닝만으로 최고 속도가 두 배로 뛴 것이다. 운전자(개발자)가 할 일은 아무것도 없다.
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| 벤치마크 성능 | 기준 | 2배+ |
| V8 버전 | 13.7 이하 | 13.8 |
| Chrome 버전 | 137 이하 | 138 |
| Node.js 반영 | 미정 | V8 13.8 포함 버전부터 |
개발자가 코드를 한 줄도 바꾸지 않아도 Chrome 138로 업데이트하면 자동으로 적용된다.
[💡 잠깐! 이 용어는?] JetStream2: Apple이 만든 JavaScript/WebAssembly 벤치마크 스위트. 다양한 워크로드(정렬, 파싱, 직렬화 등)로 엔진의 종합 성능을 측정한다.
6가지 핵심 최적화
1. 부작용 없는 패스트 패스(Side-Effect-Free Fast Path)
기존 JSON.stringify는 직렬화 중간에 사용자 코드가 실행될 가능성(getter, toJSON, Proxy 등)을 항상 고려해야 했다. 비유하면 "혹시 폭탄이 있을지 모르니" 매 단계마다 방폭 검사를 하는 것과 같다.
새 구현은 객체에 부작용을 일으킬 요소가 없는지 먼저 확인하고, 없으면 검사 과정을 전부 건너뛰는 전용 경로로 진입한다. 게다가 기존의 재귀 호출 방식 대신 반복(iterative) 방식을 사용해 깊은 중첩에서도 스택 오버플로 위험이 줄었다.
2. 템플릿화된 문자열 처리(Templatized String Handling)
JavaScript 문자열은 1바이트(ASCII) 또는 2바이트(유니코드)로 저장된다. 기존에는 직렬화할 때 매 문자마다 "이게 1바이트인가 2바이트인가"를 체크했다.
새 구현은 컴파일 타임에 1바이트 전용과 2바이트 전용, 두 가지 버전의 직렬화 코드를 생성한다. 비유하면 국내 택배와 국제 택배 라인을 분리 운영하는 물류 센터와 같다. 섞어서 처리하는 것보다 분류해서 전용 라인에 태우는 게 빠르다.
3. SIMD 기반 문자 이스케이핑
JSON에서는 ", \, 제어 문자 등을 이스케이프해야 한다. 문자열이 길면 이 탐색 비용이 누적된다.
| 문자열 길이 | 기법 | 설명 |
|---|---|---|
| 긴 문자열 | 하드웨어 SIMD | ARM64 Neon 명령어로 한 번에 16바이트씩 처리 |
| 짧은 문자열 | SWAR | 일반 레지스터에서 비트 연산으로 여러 바이트를 동시에 처리 |
[💡 잠깐! 이 용어는?] SIMD(Single Instruction, Multiple Data): 하나의 명령어로 여러 데이터를 동시에 처리하는 CPU 기법. 이미지 처리, 벡터 연산, 문자열 탐색 등에서 성능을 크게 끌어올린다.
4. Hidden Class의 fast-json-iterable 플래그
V8은 객체의 구조를 Hidden Class로 관리한다. 새 최적화는 객체를 처음 직렬화할 때 아래 조건을 만족하면 Hidden Class에 플래그를 찍는다:
- Symbol 키가 없음
- 모든 프로퍼티가 enumerable
- 키에 이스케이프가 필요한 문자가 없음
같은 Hidden Class를 가진 다음 객체들은 이 검증 과정을 통째로 건너뛴다. 같은 구조의 객체를 반복 직렬화하는 API 응답 같은 시나리오에서 위력을 발휘한다.
5. Dragonbox 숫자 변환
숫자를 문자열로 바꾸는 알고리즘이 Grisu3에서 Dragonbox로 교체됐다. 이 변경은 JSON.stringify뿐 아니라 V8 전체의 Number.prototype.toString()에 영향을 준다.
| 알고리즘 | 특징 |
|---|---|
| Grisu3 (기존) | 빠르지만 일부 케이스에서 정확도를 위해 폴백 필요 |
| Dragonbox (신규) | 항상 정확하면서도 Grisu3보다 빠름 |
6. 세그먼트 버퍼 관리
기존에는 직렬화 결과를 하나의 연속 메모리 버퍼에 쌓았다. 객체가 크면 버퍼가 꽉 찰 때마다 더 큰 메모리를 할당하고 전체를 복사하는 비용이 발생했다.
새 구현은 세그먼트 단위로 메모리를 관리한다. 비유하면 이사할 때 짐이 많으면 더 큰 트럭을 빌리는 대신, 트럭 여러 대를 쓰는 방식이다. 재할당과 복사 비용이 사라진다.
패스트 패스가 작동하지 않는 경우
모든 경우에 2배가 되는 건 아니다. 아래 조건에 해당하면 기존 일반 직렬화 경로로 폴백한다:
replacer또는space인자를 사용한 경우- 객체에 커스텀
.toJSON()메서드가 있는 경우 - 배열 형태의 인덱스 프로퍼티를 가진 객체
ConsString같은 복합 문자열 타입
// 패스트 패스 O — 단순 객체 직렬화
JSON.stringify({ name: 'Kim', age: 30, active: true })
// 패스트 패스 X — replacer 사용
JSON.stringify(data, ['name', 'age'])
// 패스트 패스 X — space(들여쓰기) 사용
JSON.stringify(data, null, 2)
// 패스트 패스 X — toJSON 커스텀 메서드
const obj = { toJSON() { return 'custom' } }
JSON.stringify(obj)로깅이나 디버깅 목적의 JSON.stringify(data, null, 2)는 패스트 패스를 타지 않는다. 성능이 중요한 경로에서는 replacer와 space 없이 호출하는 것이 유리하다.
정리
- V8 13.8(Chrome 138)에서
JSON.stringify성능이 2배 이상 개선됐다 - 핵심은 부작용 없는 패스트 패스, SIMD 문자 탐색, Hidden Class 플래그, Dragonbox 숫자 변환, 세그먼트 버퍼의 조합이다
replacer,space,toJSON사용 시 패스트 패스가 작동하지 않는다- 코드 변경 없이 런타임 업데이트만으로 적용된다
- Node.js는 V8 13.8을 포함하는 버전부터 동일한 혜택을 받는다
API 서버에서 매 요청마다 JSON.stringify를 호출한다면, Chrome 138 이후의 V8을 사용하는 것만으로 직렬화 구간의 CPU 시간이 절반으로 줄어든다. 인프라 비용 절감이 코드 한 줄 없이 가능한 드문 케이스다.
참고:
- V8 Blog — Faster JSON.stringify: https://v8.dev/blog/json-stringify
- Dragonbox Algorithm: https://github.com/jk-jeon/dragonbox
관심 있을 만한 포스트
V8 Explicit Compile Hints — 주석 한 줄로 JavaScript 시작 속도를 630ms 줄이는 법
Chrome 136에 도입된 V8의 Explicit Compile Hints 기능으로 JavaScript 초기 로딩 성능을 개선하는 원리와 사용법을 분석한다.
V8 WasmGC 투기적 최적화 — 가상 메서드를 인라인으로 만드는 법
V8이 WasmGC의 가상 메서드 디스패치에 투기적 인라이닝을 도입해 Dart와 Java 앱에서 최대 8% 성능을 끌어낸 방법.
V8 Mutable Heap Numbers — 숫자 하나 바꿀 때마다 새 객체를 만들던 비효율을 잡다
V8 엔진이 스크립트 컨텍스트의 숫자 변수를 매번 새 HeapNumber로 할당하던 방식을 제자리 수정(mutable)으로 바꿔 최대 2.5배 성능 향상을 달성했다.
Native JSON Modules — 번들러 없이 JSON을 import하는 시대
Import Attributes와 함께 표준이 된 native JSON module. 어떻게 동작하고, 기존 번들러 방식과 뭐가 다른지 정리했다.
V8의 Sea of Nodes 탈출기 — 왜 우아한 이론이 실전에서 무너졌는가
V8 팀이 10년간 사용한 Sea of Nodes IR을 포기하고 Turboshaft로 전환한 7가지 이유와 그 교훈을 정리한다.
LCP 28초짜리 React 앱을 1초로 깎아낸 기록 — 4단계 성능 수술 프레임워크
번들 분석부터 에셋 최적화까지, React 앱의 LCP를 단계적으로 개선하는 실전 프레임워크를 다룬다.
CSS만으로 커스텀 셀렉트 박스 — JavaScript 150줄이 사라지는 순간
Chrome 135에 도입된 appearance: base-select와 sibling-index()로 JavaScript 없이 완전한 커스텀 드롭다운을 구현하는 방법을 분석한다.
Google WebMCP — 웹사이트가 AI 에이전트에게 '메뉴판'을 건네는 시대
Chrome 146에 탑재된 WebMCP의 Declarative·Imperative API 구조와 웹 개발자가 준비해야 할 변화를 분석한다.