JSON Toolkit

บัญชีหน่วยความจำของ JSON: ต้นทุนจริงของสตริง ออบเจ็กต์ และอาร์เรย์ขนาดใหญ่

สรุปสำคัญ: ไฟล์ JSON ขนาด 10MB อาจกลายเป็น 30-50MB ใน V8 heap เรียนรู้ต้นทุนจริงของสตริง ออบเจ็กต์ และตัวเลข วิธีตั้ง baseline ด้วย process.memoryUsage และเมื่อใดที่การสตรีมช่วยได้

ขั้นสูงอ่านประมาณ 11 นาทีเผยแพร่ 2026-10-31อัปเดต 2026-10-31
บัญชีหน่วยความจำ JSON: ต้นทุนใน heap ของสตริง ออบเจ็กต์ และอาร์เรย์ใน V8

สรุปสั้น: ตั้ง baseline ด้วย process.memoryUsage ก่อน โดยบันทึกข้อความและกราฟออบเจ็กต์เป็นส่วนเพิ่มสองส่วนแยกกัน จากนั้นกำเนิดสำเนาเต็มหนึ่งชุดก่อนจะลองอย่างอื่น — ไม่ใช้ JSON.parse(JSON.stringify(x)) ไม่ห่อเอกสารดิบด้วยระบบ reactive ต่อไปตัดการอ้างอิงที่อยู่ยาวนาน และค่อยพิจารณาการสตรีมเป็นลำดับสุดท้าย: เส้นโค้งหน่วยความจำที่แบนราบคุ้มค่าที่จะแลกกับการสูญเสีย random access ก็ต่อเมื่อไฟล์ไม่พอดีกับหน่วยความจำหรือคุณต้องการเพียงไม่กี่ฟิลด์

เอกสาร JSON ขนาด 10MB อาจกลายเป็น 30MB ถึง 50MB ใน heap หลังจาก JSON.parse และจุดสูงสุดชั่วคราวยังสูงกว่านั้นอีก หากไม่คำนวณเลขนี้ "การปรับแต่ง" ก็เป็นเพียงการเดา

1. ข้อความและกราฟออบเจ็กต์เป็นบัญชีที่แตกต่างกันสองบัญชี

บนดิสก์และใน request body JSON คือลำดับของไบต์ เมื่อเข้าสู่หน่วยความจำแล้วมันกลายเป็นสองสิ่ง คือสตริงหนึ่งอัน และกราฟออบเจ็กต์ที่ประกอบด้วยออบเจ็กต์ อาร์เรย์ ตัวเลข และบูลีน การแยกบัญชีทั้งสองนี้เป็นวิธีเดียวที่จะอธิบายได้ว่าทำไมไฟล์ 10MB จึงทำให้โปรเซสใช้หน่วยความจำถึง 80MB

รูปแบบข้อมูล กราฟออบเจ็กต์เทียบกับขนาดข้อความ ต้นทุนหลัก
อาร์เรย์แบนของออบเจ็กต์ (ประมาณ 8 ฟิลด์ต่อชิ้น) ประมาณ 2.5-4 เท่า header ของออบเจ็กต์หนึ่งอันและช่อง property ต่อสมาชิกหนึ่งรายการ
คอนฟิกที่ซ้อนลึก ประมาณ 3-5 เท่า ออบเจ็กต์เล็กจำนวนมาก แต่ละตัวมี header ของตัวเอง
อาร์เรย์ของสตริงที่ยาวมาก ประมาณ 1.2-2 เท่า header ของสตริงบวกข้อมูลตัวอักษร
ตัวเลขขนาดใหญ่ที่มีทศนิยม ประมาณ 2-3 เท่า HeapNumber หนึ่งตัวต่อค่า
เอกสารที่มีคีย์ซ้ำกันมาก แปรผันตามจำนวนคีย์ การแฮชคีย์และการตั้งค่า property ระหว่างแยกวิเคราะห์

ตัวคูณเหล่านี้เป็นค่าอ้างอิงระดับลำดับขนาดที่ขึ้นกับเวอร์ชัน V8, pointer compression, จำนวนฟิลด์ และเนื้อหาของสตริง ตารางนี้ชี้ประเด็นเดียว: ขนาดของกราฟออบเจ็กต์กำหนดโดย "จำนวนโหนด x ต้นทุนต่อโหนด" ไม่ใช่จำนวนไบต์ในข้อความ

2. สตริง: รายการที่ถูกประเมินต่ำที่สุด

สตริงใน V8 ไม่ใช่อาร์เรย์ตัวอักษรเปล่า ๆ มันมี header ของออบเจ็กต์ (pointer ไปยัง map, ฟิลด์ hash และความยาว) ซึ่งกินประมาณสิบกว่าไบต์ในการ build 64-bit ทั่วไปที่มี pointer compression ก่อนจะนับตัวอักษรใด ๆ ตัวอักษรถูกเก็บด้วยหนึ่งในสองวิธี:

  • สตริงหนึ่งไบต์ สำหรับตัวอักษรในช่วง Latin-1: 1 ไบต์ต่อตัวอักษร;
  • สตริงสองไบต์ เมื่อเนื้อหารวมตัวอักษร CJK, emoji หรือสิ่งใดที่อยู่นอก Latin-1: 2 ไบต์ต่อตัวอักษร

ตัวอักษรจีนหนึ่งตัวกิน 3 ไบต์ในไฟล์ UTF-8 แต่กิน 2 ไบต์ต่อตัวอักษรในสตริงสองไบต์ บวก header ขนาดข้อความและขนาดใน heap ไม่ใช่การวัดเดียวกัน และไม่ได้เคลื่อนไปทิศทางเดียวกันเสมอ

รายละเอียดสามข้อที่สำคัญโดยตรงกับงาน JSON:

  1. Slice ทำให้ parent ยังมีชีวิตอยู่ V8 แทนสตริงย่อยที่ยาวกว่าด้วย SlicedString ซึ่งเก็บการอ้างอิง parent บวก offset และความยาว เก็บ 200 ตัวอักษรที่ slice มาจาก log ขนาด 80MB แล้ว 80MB ทั้งก้อนจะยังเข้าถึงได้จนกว่าจะปลดปล่อย slice นั้น
  2. การต่อสตริงสร้าง ConsString การใช้ += ซ้ำ ๆ บนสตริงขนาดใหญ่จะสร้างต้นไม้ของชิ้นส่วนที่ถูกแบนเป็นหน่วยความจำต่อเนื่องก็ต่อเมื่อมีการอ่านตัวอักษรจริง การต่อหนึ่งหมื่นครั้งแล้วอ่านหนึ่งครั้งจะสร้างการจัดสรรหน่วยความจำก้อนใหญ่ในขณะนั้น
  3. ค่าที่ซ้ำกันไม่ถูกแชร์โดยอัตโนมัติ หนึ่งหมื่นเรกคอร์ดที่มี "status": "paid" โดยปกติจะได้ออบเจ็กต์สตริงอิสระหนึ่งหมื่นตัว parser ไม่ได้ชี้ค่าเหล่านั้นไปยังค่าคงที่ตัวเดียว หากต้องการประหยัดต้นทุนนั้นคุณต้องแมปสตริงที่พบบ่อยไปยัง enum หรือตัวเลขด้วยตัวเอง

เมื่อคุณต้องเก็บข้อความสั้น ๆ ไว้เป็นเวลานาน ให้เปลี่ยนมันเป็นสตริงอิสระแล้วปลดปล่อยต้นฉบับ:

// แย่: slice อ้างอิงเอกสารทั้งก้อน ดังนั้น logText จึงถูกเก็บกวาดไม่ได้
const summary = logText.slice(0, 200);

// ดี: สร้างสตริงอิสระ แล้วปลดปล่อย logText
const summaryCopy = Buffer.from(summary, 'utf8').toString('utf8');

3. ออบเจ็กต์: ทุกคีย์กินหนึ่งช่อง

ออบเจ็กต์ธรรมดามีอย่างน้อยสามสิ่ง: pointer ไปยัง hidden class (map), pointer ไปยัง property store และ pointer ไปยัง element store ด้วย pointer compression แต่ละ pointer คือ 4 ไบต์ และ header ของ heap object เพิ่มเข้าไปอีก ดังนั้นแม้ออบเจ็กต์ว่างก็กินประมาณสิบหกถึงยี่สิบกว่าไบต์ ในการ build 64-bit ที่ไม่มี pointer compression ให้เพิ่มเป็นสองเท่า

Named property ลงเอยในหนึ่งในสองที่:

  • In-object properties: ช่องที่วางเรียงต่อจาก header ของออบเจ็กต์โดยตรง ซึ่งให้การเข้าถึงเร็วที่สุด;
  • PropertyArray: เมื่อช่อง in-object หมด property จะย้ายไปยังอาร์เรย์แยก และออบเจ็กต์เก็บ pointer ไปยังอาร์เรย์นั้น

แต่ละช่องคือ 4 หรือ 8 ไบต์ และ สตริงของคีย์ถูกนับแยกต่างหาก นั่นคือเหตุผลที่ชื่อฟิลด์ยาว ๆ กลายเป็นต้นทุนจริงเมื่อมีเรกคอร์ดหลายแสนรายการ ไม่ใช่แค่เรื่องความสวยงาม

ข่าวดีคือ hidden class ถูกแชร์กัน: ออบเจ็กต์หนึ่งล้านตัวที่มีคีย์เดียวกันในลำดับเดียวกันแชร์ map หนึ่งอันแทนที่จะคัดลอกข้อมูลโครงสร้าง ข่าวร้ายคือรูปแบบเหล่านี้ทำลายการแชร์นั้นหรือดันออบเจ็กต์ไปสู่การแทนค่าที่แพงกว่า:

  • บางเรกคอร์ดมีฟิลด์ remark บางเรกคอร์ดไม่มี ทำให้เกิดสองรูปร่างและการตรวจรูปร่างเพิ่มขึ้นทุกครั้งที่เข้าถึง;
  • ลำดับการแทรกที่ต่างกันอาจสร้างรูปร่างที่ต่างกัน;
  • จำนวน property ที่มากเกินไป (เกินประมาณหนึ่งพัน) หรือการ delete บ่อย ๆ จะดันออบเจ็กต์เข้าสู่ dictionary mode ซึ่งทุก property ยังมีรายการใน hash table ด้วย;
  • Object.create(null) อยู่ใน dictionary mode ตั้งแต่เกิด มันเป็นตารางค้นหาที่ดีแต่เป็นตัวเลือกที่แย่สำหรับเรกคอร์ดที่มี schema คงที่ซึ่งเข้าถึงใน hot loop

มีความเข้าใจผิดที่พบบ่อยซึ่งควรแก้ไข: JSON อนุญาตคีย์ซ้ำ และ {"a":1,"a":2} เป็นเอกสารที่ถูกต้องซึ่งผลลัพธ์คือ a === 2 ออบเจ็กต์สุดท้ายเก็บค่าเดียว แต่ parser ยังสแกนและกำหนดทุกครั้งที่พบ คีย์ซ้ำไม่ได้ประหยัดหน่วยความจำ มันกินเวลาแยกวิเคราะห์เท่านั้น

4. อาร์เรย์และตัวเลข: element kind กำหนดความกว้าง

V8 ติดตาม "element kind" หลายชนิดสำหรับอาร์เรย์ และแต่ละชนิดมีความกว้างต่อช่องต่างกัน:

เนื้อหาของอาร์เรย์ Element kind ต้นทุนโดยประมาณต่อช่อง
จำนวนเต็มขนาดเล็ก (ในช่วง Smi) PACKED_SMI_ELEMENTS 4 ไบต์
ทศนิยมหรือจำนวนเต็มที่ใหญ่มาก PACKED_DOUBLE_ELEMENTS 8 ไบต์
สตริงหรือออบเจ็กต์ PACKED_ELEMENTS pointer 4 ไบต์บวกค่าตัวเอง
อาร์เรย์ที่มีช่องว่างจากการ delete HOLEY_* เหมือนเดิม บวกการตรวจช่องว่าง
อาร์เรย์ที่เบาบางมาก DICTIONARY_ELEMENTS รายการ hash ปกติ 20+ ไบต์

มีสองกฎที่ควรจดจำ:

หนึ่ง element kind ลดระดับได้เท่านั้น อาร์เรย์ที่เริ่มด้วยจำนวนเต็มขนาดเล็กจะกลายเป็น double array ทันทีที่คุณแทรก 0.5 และกลายเป็น generic element array เมื่อคุณแทรกสตริง มันไม่ฟื้นตัวโดยอัตโนมัติ คุณต้องสร้างอาร์เรย์ใหม่

สอง ตำแหน่งที่ตัวเลข JSON ลงเอยขึ้นกับค่าของมัน จำนวนเต็มในช่วง Smi (ประมาณบวกลบ 1.07 พันล้าน) ถูกเก็บเป็น Smi โดยตรงและกินช่อง pointer หนึ่งช่อง ตัวเลขที่มีทศนิยมหรือเลขชี้กำลัง หรือค่าที่อยู่นอกช่วงนั้น จะจัดสรร HeapNumber ประมาณ 16 ไบต์ โดยมี pointer ไปยังมันเก็บในอาร์เรย์ ดังนั้น [1, 2, 3] และ [0.1, 0.2, 0.3] ต่างกันไม่ใช่แค่ค่า แต่ต่างกันที่ "ช่อง 4 ไบต์หนึ่งช่อง" กับ "ช่อง 8 ไบต์บวก heap object"

นี่ก็เป็นรากของปัญหาความแม่นยำของ ID ที่เป็นจำนวนเต็มขนาดใหญ่เช่นกัน: เกินช่วง safe integer ตัวเลข JSON จะถูกมองเป็น double และหลักล่างจะหายไปเลย อาร์เรย์ JSON มาถึงในสภาพ packed และไม่มีช่องว่าง ดังนั้นอย่า delete สมาชิกหลังแยกวิเคราะห์ เพราะจะดันอาร์เรย์ทั้งก้อนไปสู่การแทนค่าที่แพงกว่า

5. จุดสูงสุดของการแยกวิเคราะห์และการทำให้เป็นอนุกรม: ช่วงที่โปรเซสตายจริง ๆ

หน่วยความจำในสภาวะคงที่คือบัญชีหนึ่ง จุดสูงสุดคืออีกบัญชี ขณะที่ JSON.parse(text) ทำงาน โปรเซสถือสามสิ่งพร้อมกัน:

  1. สตริงอินพุต text;
  2. กราฟออบเจ็กต์ที่กำลังสร้างและยังไม่สมบูรณ์;
  3. โครงสร้างชั่วคราวของ parser เอง

จุดสูงสุดของการแยกวิเคราะห์ประมาณ "ข้อความ + กราฟออบเจ็กต์ + overhead ชั่วคราว" หลังแยกวิเคราะห์ หาก text ยังถูกอ้างอิงโดยตัวแปร closure cache ระดับโมดูล หรือ reactive store หน่วยความจำของข้อความนั้นจะไม่ถูกคืน

การทำให้เป็นอนุกรมก็มีจุดสูงสุดเช่นกัน JSON.stringify(data) ถือกราฟออบเจ็กต์เต็มไว้ขณะสร้างสตริงเอาต์พุต โดยมีบัฟเฟอร์การต่อสตริงอยู่ระหว่างกลาง เมื่อเอาต์พุตเข้าใกล้ขีดจำกัดความยาวสตริงของ V8 (ประมาณ 500 ล้านตัวอักษร) มันจะโยน RangeError ความล้มเหลวนี้มักปรากฏในสคริปต์ที่ส่งออกฐานข้อมูลทั้งหมดเป็นเอกสาร JSON เดียว

รูปแบบที่พบบ่อยและแพงที่สุดคือการใช้การทำให้เป็นอนุกรมเป็นการ clone แบบลึก:

// จุดสูงสุดประมาณ: กราฟออบเจ็กต์เดิม + ข้อความชั่วคราว + กราฟออบเจ็กต์ใหม่
const safeCopy = JSON.parse(JSON.stringify(bigData));

บรรทัดเดียวนี้เก็บข้อมูลชุดเดียวกันสามครั้ง สำหรับงานอ่านอย่างเดียว ให้ส่งการอ้างอิงไป เมื่อต้องการแยกข้อมูลจริง ๆ ให้ใช้ structuredClone และยอมรับว่ามันสร้างกราฟออบเจ็กต์ทั้งหมดใหม่เช่นกัน

6. การวัดด้วย process.memoryUsage

ใน Node process.memoryUsage() คืนค่าตัวเลขห้าตัวพร้อมกัน: rss (หน่วยความจำกายภาพที่โปรเซสใช้), heapTotal, heapUsed (JS heap ที่ใช้), external (หน่วยความจำที่ออบเจ็กต์ C++ ถืออยู่) และ arrayBuffers (หน่วยความจำของ ArrayBuffer และ Buffer) เมื่อศึกษาการจัดการ JSON heapUsed เป็นเลนส์ที่ถูกต้องสำหรับกราฟออบเจ็กต์ และ rss บอกคุณว่าโปรเซสทั้งหมดกำลังเสียการควบคุมหรือไม่

สคริปต์วัดต้องมีวินัยสามข้อ: เรียก global.gc() ก่อน แซมเป็นระยะ และเปลี่ยนตัวแปรทีละตัว สคริปต์นี้บันทึกส่วนเพิ่มสำหรับ "อ่านข้อความ" และ "แยกวิเคราะห์เป็นออบเจ็กต์" แยกกัน:

// รัน: node --expose-gc measure-json-memory.mjs
import { readFileSync } from 'node:fs';

global.gc();
const baseline = process.memoryUsage().heapUsed;

const text = readFileSync('big.json', 'utf8');
const afterRead = process.memoryUsage().heapUsed;

const data = JSON.parse(text);
const afterParse = process.memoryUsage().heapUsed;

console.log({
  textOnHeap: afterRead - baseline,
  graphOnHeap: afterParse - afterRead,
  parsePeak: afterParse - baseline,
});

กับดักไม่กี่ข้อ:

  • --expose-gc ต้องเปิดอย่างชัดเจน มิฉะนั้น global.gc จะไม่มีอยู่;
  • heapUsed ครอบคลุมเฉพาะ JS heap Buffer ที่อ่านไฟล์อยู่ใน external และ arrayBuffers ดังนั้นอย่าใช้ตัวเลขเดียวอธิบายทุกอย่าง;
  • อย่า console.log ออบเจ็กต์ขนาดใหญ่ระหว่างแซม มันทำให้การอ้างอิงชั่วคราวยังมีชีวิตอยู่;
  • การรันครั้งแรกมีการ warm-up ของ JIT และการคอมไพล์แบบ lazy ให้รันอย่างน้อยสามรอบแล้วหาค่ามัธยฐาน;
  • หากต้องการเปรียบเทียบในเบราว์เซอร์ ให้เก็บ heap snapshot ในแผง Memory ของ DevTools และอ่าน Retained Size แทนที่จะจ้องฟันเลื่อยใน Allocation Timeline

หากไม่มี baseline นี้ คำกล่าวเช่น "เปลี่ยนไปสตรีมแล้วจะหาย" หรือ "มี cache แล้วจะเร็วขึ้น" ก็เป็นเพียงความรู้สึก

7. การสตรีมได้จุดสูงสุดของหน่วยความจำ ไม่ได้ลดเวลาทั้งหมด

การแลกเปลี่ยนในการแยกวิเคราะห์แบบสตรีมนั้นชัดเจน: random access แลกกับหน่วยความจำที่ไม่โตตามขนาดไฟล์อีกต่อไป

แนวทาง หน่วยความจำจุดสูงสุด Random access เหมาะกับ
JSON.parse เต็มรูปแบบ ข้อความบวกกราฟออบเจ็กต์ทั้งหมด ได้ ต่ำกว่าประมาณ 100MB และต้องการฟิลด์ส่วนใหญ่
SAX parser เช่น stream-json หนึ่ง chunk บวกสมาชิกปัจจุบัน ไม่ได้ ไฟล์ยักษ์ หรือต้องการเพียงไม่กี่ฟิลด์
JSON Lines ทีละบรรทัด ออบเจ็กต์ของหนึ่งบรรทัด ไม่ได้ ข้อมูลที่เป็นชุดเรกคอร์ดตามธรรมชาติ
ดัชนีภายนอกหรือฐานข้อมูล ไม่ต้องโหลดทั้งหมด ได้ การสอบถามซ้ำบนข้อมูลชุดเดิม
import { createReadStream } from 'node:fs';
import { parser } from 'stream-json';
import { pick } from 'stream-json/filters/Pick';
import { streamArray } from 'stream-json/streamers/StreamArray';

const pipeline = createReadStream('huge.json')
  .pipe(parser())
  .pipe(pick({ filter: 'items' }))
  .pipe(streamArray());

for await (const { value } of pipeline) {
  handleItem(value);
}

โค้ดนี้ไม่เคยสร้างทรีเต็ม ดังนั้นเส้นโค้งหน่วยความจำจึงแบนราบ ต้นทุนก็มีจริงเช่นกัน: คุณไม่สามารถย้อนกลับไปเอาฟิลด์ที่ข้ามไปได้ การรวมแบบ global ทำได้ยาก การกู้คืนจากข้อผิดพลาดเป็นความรับผิดชอบของคุณ และการสแกนตัวอักษรทีละตัวใน pure JS ปกติช้ากว่า JSON.parse เนทีฟประมาณหนึ่งลำดับขนาด

การตัดสินใจไม่ใช่ "ไฟล์เกิน 10MB หรือไม่" ให้ถามสองคำถาม: ต้องการฟิลด์ส่วนใหญ่หลังแยกวิเคราะห์หรือไม่? อยู่กับการเข้าถึงแบบลำดับได้หรือไม่? หากตอบใช่ทั้งสองข้อ การแยกวิเคราะห์เต็มรูปแบบถูกต้อง และความพยายามของคุณควรอยู่ที่การลดสำเนาและการเรนเดอร์ หากตอบไม่ข้อใดข้อหนึ่ง ให้พิจารณาการสตรีม สำหรับกลยุทธ์แบบแบ่งชั้นฉบับเต็ม ดู การแยกวิเคราะห์ การเรนเดอร์ และการสตรีม JSON ขนาดใหญ่

8. ลดสำเนา ปลดปล่อยเร็วขึ้น

ในงานหน่วยความจำ การกำจัดสำเนาเต็มหนึ่งชุดมักมีค่ามากกว่าการเปลี่ยน parser:

  • ส่งการอ้างอิงสำหรับงานอ่านอย่างเดียว อย่า deep-clone "เพื่อความปลอดภัย" โดยเฉพาะอย่าใช้ JSON.parse(JSON.stringify(x));
  • อย่าห่อเอกสารดิบในระบบ reactive reactive ของ Vue proxy กราฟออบเจ็กต์ทั้งหมดแบบเรียกซ้ำ และชั้น proxy มีต้นทุนหน่วยความจำของตัวเอง;
  • คำนวณและแคชข้อมูลที่ได้มาแยกกัน หากรายการต้องการเพียง id, title และ status อย่าเอาทั้งเรกคอร์ดใส่ store;
  • ปรับสตริงที่ใช้บ่อยให้เป็นมาตรฐาน แมปค่าเช่น "paid" และ "pending" ไปยัง enum ตัวเลขเพื่อประหยัดทั้งออบเจ็กต์สตริงและต้นทุนการเปรียบเทียบ;
  • ตัดการอ้างอิงที่อยู่ยาวนาน ตั้งออบเจ็กต์ขนาดใหญ่เป็น null เมื่อใช้เสร็จ ล้าง cache และยกเลิกการลงทะเบียน callback เพื่อให้ closure ไม่พามันไปยังหน้าถัดไป;
  • คำนึงถึงต้นทุนการคัดลอกของ Worker postMessage ใช้ structured clone โดยค่าเริ่มต้น การส่งกราฟออบเจ็กต์ 50MB กลับไปยัง main thread จะสร้างสำเนาที่สอง ให้คืนสรุปทุกครั้งที่ทำได้

อีกครึ่งของบัญชีขนาดอยู่ใน หยุดทำการบีบอัด JSON ผิดลำดับ: ไบต์บนดิสก์และไบต์บนสายเป็นบัญชีที่ต่างจากกราฟออบเจ็กต์ใน heap และไม่มีเปอร์เซ็นต์เดียวอธิบายทั้งสองได้ สำหรับพฤติกรรมของเอดิเตอร์และเครื่องมือ CLI ต่อหน่วยความจำ ดู เอดิเตอร์ เครื่องมือบรรทัดคำสั่ง และระบบอัตโนมัติ

9. รายการตรวจสอบ

อาการ สาเหตุที่เป็นไปได้มากกว่า การดำเนินการแรก
ไฟล์ 10MB ดันโปรเซสไปถึง 50MB กราฟออบเจ็กต์และข้อความยังมีชีวิตทั้งคู่ วัดเป็นระยะ ตรวจว่าข้อความยังถูกถืออยู่หรือไม่
OOM ตอนแยกวิเคราะห์ ปกติในสภาวะคงที่ จุดสูงสุดของการแยกวิเคราะห์ทับซ้อนกับข้อมูลเก่า ปลดปล่อยออบเจ็กต์เก่าก่อนแยกวิเคราะห์ไฟล์ใหม่
หน่วยความจำค่อย ๆ เพิ่มและไม่ลด slice, cache หรือ closure ถือการอ้างอิง เก็บ heap snapshot แล้วเรียงตาม retained size
อาร์เรย์ใหญ่กว่าที่คาด ทศนิยมลดระดับ element kind รวมชนิดตัวเลขให้เป็นหนึ่งเดียว หลีกเลี่ยง delete
เพิ่ม cache แล้วกลับช้าลง คีย์และค่ากินหน่วยความจำ เพิ่มแรงกดดัน GC จำกัดขนาด cache และเก็บเฉพาะสรุป

ทำไมหน่วยความจำจึงโตสามหรือสี่เท่าหลัง JSON.parse?

เพราะข้อความเป็นเพียงอินพุต ผลลัพธ์ของการแยกวิเคราะห์คือกราฟออบเจ็กต์ ทุกออบเจ็กต์มี header ทุก property มีช่อง และทุกสตริงมี header ของตัวเอง เมื่อมีโหนดมากพอ overhead ทั้งหมดจะเกินจำนวนไบต์ของข้อความ ตัวคูณขึ้นกับรูปร่างข้อมูล และอาร์เรย์ออบเจ็กต์แบบแบนต่างจากเอกสารที่ซ้อนลึกอย่างมาก

ไฟล์ JSON 10MB ควรแยกวิเคราะห์เต็มหรือสตรีม?

ดูวัตถุประสงค์ก่อน หากต้องการ random access ต้องการฟิลด์ส่วนใหญ่ และไฟล์พอดีกับหน่วยความจำ ให้ใช้ JSON.parse เต็มรูปแบบ หากต้องการเพียงไม่กี่ฟิลด์ หรือไฟล์ไม่พอดี ให้สตรีม 10MB ไม่ใช่เส้นแบ่ง การสตรีมมักกลายเป็นสิ่งจำเป็นที่ระดับเกิน 100MB

ทำไม process.memoryUsage().heapUsed ไม่ตรงกับการตรวจสอบของระบบ?

heapUsed ครอบคลุมเฉพาะส่วนที่ใช้ของ JS heap ไม่รวม external, arrayBuffers และ code space การตรวจสอบของระบบรายงาน rss ซึ่งเป็นการวัดที่ต่างกัน ให้ติดตาม heapUsed และ rss ไปพร้อมกัน ช่องว่างที่ใหญ่บ่งชี้ถึง Buffer และออบเจ็กต์เนทีฟ

JSON.parse(JSON.stringify(x)) เป็น deep clone ที่ปลอดภัยหรือไม่?

ไม่ใช่ มันถือกราฟเดิม ข้อความชั่วคราว และกราฟใหม่พร้อมกัน ดังนั้นจุดสูงสุดประมาณสามเท่าของสภาวะคงที่ และมันตัด undefined, ฟังก์ชัน และความหมายของ Date ทิ้ง ให้ส่งการอ้างอิงสำหรับงานอ่านอย่างเดียว เมื่อต้องการแยกข้อมูลจริง ๆ ให้ใช้ structuredClone และยอมรับต้นทุนการสร้างใหม่เช่นเดียวกัน