Tăng tốc phân tích JSON lớn
Kết luận: Tối ưu theo thứ tự sai khiến mọi lần đổi thư viện đều vô ích. Hãy đo riêng mạng, phân tích và kết xuất, rồi giảm số lần phân tích, thu nhỏ payload và dùng dạng luồng.

Tóm lại: hãy đo riêng mạng, phân tích và kết xuất trước — phần lớn "phân tích chậm" thực ra là kết xuất chậm — rồi leo thang theo thứ tự: ít lần phân tích hơn, payload nhỏ hơn, dạng luồng, worker, và định dạng nhị phân cuối cùng.
Đừng bắt đầu bằng việc đổi thư viện. Thêm một bộ đếm thời gian là bạn thường sẽ thấy vấn đề nằm ở khâu kết xuất.
Bước một: đo từng giai đoạn
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();
| Giai đoạn chiếm ưu thế | Vấn đề thực sự | Cách sửa |
|---|---|---|
| Mạng | Payload quá lớn | Chọn trường, phân trang, nén |
JSON.parse |
Đầu vào thực sự lớn | Ít lần phân tích hơn, dạng luồng, worker |
| Kết xuất | Hàng chục nghìn nút cùng lúc | Danh sách ảo, phân trang, thu gọn |
Trình duyệt hiện đại phân tích 1MB trong khoảng 5–15ms và 10MB trong 80–150ms. Nếu 10MB mất hai giây, thì JSON.parse không phải nút cổ chai của bạn.
Bước hai: phân tích ít đi
Đây là chiến thắng rẻ nhất và cũng bị bỏ qua nhiều nhất.
- Đừng phân tích cùng một payload lặp đi lặp lại. Mười component mỗi cái gọi
JSON.parse(localStorage.getItem(...))là mười lần phân tích. Hãy lưu đệm một lần ở cấp module. - Đừng dùng
JSON.parse(JSON.stringify(x))để sao chép sâu. Nó tốn hơn gấp đôi lượng dữ liệu;structuredClonethì không. - Đừng phân tích trên luồng chính rồi gửi object sang worker.
postMessagesao chép nó thêm lần nữa. Hãy gửi văn bản thô và để worker phân tích. - Yêu cầu ít trường hơn. Nếu giao diện chỉ cần ba thuộc tính, API không nên gửi ba trăm.
Bước ba: thu nhỏ payload
Phân trang theo page/size, dùng con trỏ thay vì offset sâu, trường tóm tắt trong endpoint danh sách. Điều này thường vượt mọi tối ưu frontend cộng lại, vì nó giảm mạng, phân tích và bộ nhớ cùng lúc.
Bước bốn: dùng dạng luồng
Khi dữ liệu phải được xử lý toàn bộ và thực sự lớn:
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);
}
Nhiều mã hơn, và không truy cập ngẫu nhiên được — chỉ phù hợp để xử lý từng bản ghi.
Bước năm: đưa ra khỏi luồng chính
Phân tích nhanh nhưng chặn. Hãy đặt nó trong worker và truyền văn bản, không phải object đã phân tích, nếu không việc sao chép có cấu trúc sẽ nhân đôi chi phí.
Bước sáu: đổi định dạng, cuối cùng
Với lưu lượng nội bộ ổn định ở mức vài chục megabyte, NDJSON chảy tự nhiên, còn Protobuf hoặc MessagePack nhỏ hơn và nhanh hơn. Cả hai đều là thay đổi giao thức cần phối hợp.
Đừng làm
evalJSON: không an toàn và không nhanh hơn- Stringify JSON để nhúng vào JSON: nhân đôi kích thước
- Phân tích hàng trăm nghìn chuỗi nhỏ trong vòng lặp: hãy gộp lô
Hỏi đáp
Làm sao biết nút cổ chai là phân tích hay kết xuất?
Hãy đo riêng thời gian gửi yêu cầu, đọc văn bản, JSON.parse và kết xuất bằng performance.now(), rồi coi giai đoạn lớn nhất là nút cổ chai thực sự. Trình duyệt hiện đại phân tích 1MB trong khoảng 5–15ms và 10MB trong 80–150ms, nên một payload 10MB mất hai giây hiếm khi là vấn đề của JSON.parse.
Tại sao đổi thư viện thường không giúp gì?
JSON.parse vốn đã native và nhanh; phần lớn "phân tích chậm" đến từ việc phân tích lặp lại hoặc kết xuất hàng chục nghìn nút cùng lúc. Đổi thư viện chỉ mua được 10–30%, trong khi bỏ đi một lần phân tích đầy đủ thường tiết kiệm một nửa.
Có nên đưa phân tích vào Web Worker không?
Hãy đưa vào worker khi phân tích chặn luồng chính và trang bị đóng băng hoặc hoạt ảnh rớt khung hình. Hãy truyền văn bản thô thay vì object đã phân tích, nếu không việc sao chép có cấu trúc của postMessage sẽ nhân đôi chi phí.