대용량 JSON 파싱 속도 올리기
핵심 요약: 잘못된 순서로 최적화하면 라이브러리 교체가 모두 무용합니다. 네트워크, 파싱, 렌더를 따로 측정한 뒤 파싱을 줄이고, 페이로드를 줄이며, 스트리밍하세요.

핵심부터 말하면, 먼저 네트워크, 파싱, 렌더를 따로 측정하세요. 대부분의 "느린 파싱"은 느린 렌더입니다. 그다음 사다리를 순서대로 오르세요. 파싱 횟수 줄이기, 페이로드 줄이기, 스트리밍, Worker, 이진 형식은 마지막.
라이브러리 교체부터 시작하지 마세요. 타이머 하나를 더하면 대개 렌더가 문제임을 발견합니다.
1단계: 단계 측정
const t0 = performance.now();
const response = await fetch(url);
const text = await response.text();
const t1 = performance.now();
const data = JSON.parse(text);
const t2 = performance.now();
render(data);
const t3 = performance.now();
| 지배 단계 | 진짜 문제 | 수정 |
|---|---|---|
| 네트워크 | 페이로드가 너무 큼 | 필드 선택, 페이지네이션, 압축 |
JSON.parse |
진짜로 큰 입력 | 파싱 줄이기, 스트리밍, Worker |
| 렌더 | 수만 노드를 한 번에 | 가상 목록, 페이지징, 접기 |
현대 브라우저는 1MB를 대략 5~15ms, 10MB를 80~150ms에 파싱합니다. 10MB가 2초 걸린다면 JSON.parse가 병목이 아닙니다.
2단계: 파싱 줄이기
가장 저렴한 이득이고 가장 간과됩니다.
- 같은 페이로드를 반복 파싱하지 마세요. 열 개 컴포넌트가 각각
JSON.parse(localStorage.getItem(...))를 호출하면 파싱 열 번입니다. 모듈 수준에서 한 번 캐시하세요. - 딥 복사에
JSON.parse(JSON.stringify(x))를 쓰지 마세요. 데이터량 두 배 이상의 비용이 듭니다.structuredClone은 그렇지 않습니다. - 메인 스레드에서 파싱한 객체를 Worker로 보내지 마세요.
postMessage가 다시 복제합니다. 원본 텍스트를 보내고 Worker가 파싱하게 하세요. - 필드를 적게 요청하세요. UI가 세 속성만 필요하면 API가 삼백 개를 보내면 안 됩니다.
3단계: 페이로드 줄이기
page/size 페이지네이션, 깊은 offset 대신 커서, 목록 엔드포인트의 요약 필드. 대개 모든 프런트엔드 최적화를 합친 것보다 낫습니다. 네트워크, 파싱, 메모리를 한 번에 줄이기 때문입니다.
4단계: 스트리밍
데이터를 전체로 처리해야 하고 진짜 큰 경우:
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
const pipeline = fs.createReadStream('big.json').pipe(parser()).pipe(streamArray());
for await (const { value } of pipeline) {
await handleOne(value);
}
코드가 더 많고 임의 접근이 없어 레코드별 처리에만 적합합니다.
5단계: 메인 스레드 밖으로 이동
파싱은 빠르지만 블로킹입니다. Worker에 넣고 텍스트를 넘기세요. 파싱된 객체가 아니라, 아니면 구조적 복제가 비용을 두 배로 만듭니다.
6단계: 형식 변경, 마지막
내부 트래픽이 일관되게 수십 MB라면 NDJSON이 자연히 스트리밍되고 Protobuf나 MessagePack이 더 작고 빠릅니다. 둘 다 조정이 필요한 프로토콜 변경입니다.
하지 말 것
- JSON을
eval: 안전하지 않고 더 빠르지도 않음 - JSON을 JSON에 내장하려 문자열화: 크기가 두 배
- 수십만 개의 작은 문자열을 루프에서 파싱: 배치하세요
FAQ
파싱이 병목인지 렌더가 병목인지 어떻게 아나?
요청, 텍스트 읽기, JSON.parse, 렌더를 performance.now()로 따로 시간을 재고 가장 큰 단계를 진짜 병목으로 취급하세요. 현대 브라우저는 1MB를 대략 5~15ms, 10MB를 80~150ms에 파싱하므로 10MB 페이로드가 2초 걸리는 것은 드물게 JSON.parse 문제입니다.
라이브러리 교체가 왜 대개 도움이 안 되나?
JSON.parse는 이미 네이티브이고 빠릅니다. 대부분의 "느린 파싱"은 반복 파싱이나 수만 노드를 한 번에 렌더하는 데서 옵니다. 라이브러리 교체는 10~30퍼센트를 벌지만, 전체 파싱 하나를 제거하면 종종 절반을 아낍니다.
파싱을 Web Worker로 옮겨야 하나?
파싱이 메인 스레드를 블록해 페이지가 멈추거나 애니메이션이 프레임을 놓치면 Worker로 옮기세요. 파싱된 객체가 아니라 원본 텍스트를 넘기세요. 아니면 postMessage 구조적 복제가 비용을 두 배로 만듭니다.