V8의 JSON.stringify가 2배 빨라졌다 — 6가지 최적화 기법 해부
V8 13.8(Chrome 138)에서 적용된 JSON.stringify 성능 개선의 기술적 배경과 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
같은 카테고리 · JavaScript
비슷한 주제의 최신 글
태그가 겹치는 글
공통 태그가 많을수록 위에 보인다