JSON Toolkit

คำถามที่พบบ่อยเรื่องประสิทธิภาพและความปลอดภัย: ช้า กระตุก และข้อมูลที่ไม่น่าเชื่อถือ

สรุปสำคัญ: ทำไมไฟล์ JSON หลายสิบ MB เปิดไม่ขึ้น, การ deep clone แพงแค่ไหน, จะป้องกันการปนเปื้อน __proto__ อย่างไร และ XSS แทรกเข้ามาทางไหน — สิบข้อสรุปแบบปฐมพยาบาลที่จัดเรียงตามอาการ

ระดับกลางอ่านประมาณ 13 นาทีเผยแพร่ 2026-10-30อัปเดต 2026-10-30
ภาพปกคำถามที่พบบ่อยเรื่องประสิทธิภาพและความปลอดภัย

สรุปสั้น ๆ: วินิจฉัยตามอาการก่อน — parse, render หรือหน่วยความจำ — แล้วไล่ลำดับการแก้: ลดปริมาณข้อมูล, เลิก deep clone, ย้ายการ parse ไปที่ Worker พร้อม virtual scrolling และใช้ streaming กับข้อมูลที่เกิน 50 MB สำหรับอินพุตที่ไม่น่าเชื่อถือ ให้จำกัดขนาดและความลึกของการซ้อนก่อน แล้วจึงกรอง __proto__, constructor และ prototype

"JSON ช้า" พูดผิดเก้าครั้งจากสิบครั้ง; สิ่งที่ช้าจริงมักเป็นการ render, การ deep clone หรือลำดับการทำงานที่ผิด

1. ทำไมมันถึงช้า

parse ไฟล์ JSON ขนาดใหญ่ช้า จะปรับประสิทธิภาพอย่างไร?

หาคอขวดก่อน แล้วค่อยพูดถึงการปรับ แต่ละขั้นต่างกันหลายระดับขนาด:

ขั้นตอน เวลาคร่าว ๆ สำหรับข้อมูล 10 MB ทำไมจึงเป็นคอขวด
การส่งผ่านเครือข่าย ขึ้นกับแบนด์วิดท์ JSON ที่ไม่บีบอัดมักบีบเหลือประมาณ 1/5
JSON.parse 20–80 ms เป็น native implementation จึงเร็วอยู่แล้ว
การไล่ traversal แบบเรียกซ้ำ / deep clone 200–2000 ms ประมวลผลทีละ node ด้วย JS ล้วน
การ render DOM (มุมมอง tree) หลายวินาที ค้างได้ง่าย จำนวน node พุ่งสูง

ดังนั้นลำดับการปรับควรเป็น:

  1. ลดปริมาณข้อมูล: ให้ API ทำ pagination, คืนเฉพาะฟิลด์ที่ต้องใช้ และเปิด gzip/brotli;
  2. เลี่ยง deep clone: ดูหัวข้อถัดไป นี่คือต้นทุนซ่อนเร้นที่พบบ่อยที่สุด;
  3. render แบบ lazy: ในมุมมอง tree ให้ render เฉพาะระดับที่ขยาย แทนที่จะสร้าง DOM node หลายหมื่นตัวพร้อมกัน;
  4. streaming: อย่า parse ทั้งหมดพร้อมกันเมื่อข้อมูลเกิน 10 MB

ถ้าคอขวดอยู่ที่ JSON.parse จริง ๆ นั่นหมายความว่าข้อมูลมีหลายสิบ MB ขึ้นไป และสิ่งที่ควรทำคือ streaming parser:

import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';

// ประมวลผลสมาชิกของ array ทีละตัว หน่วยความจำไม่ขึ้นกับขนาดรวม
const pipeline = fs.createReadStream('big.json')
  .pipe(parser())
  .pipe(streamArray());

pipeline.on('data', ({ value }) => {
  processItem(value);
});

JSON หลายสิบ MB ทำให้เบราว์เซอร์พังทันทีที่เปิด ทำอย่างไร?

ก่อนอื่นยืนยันว่ามัน "พัง" หรือ "ค้าง" สองกรณีนี้ต้องจัดการต่างกันโดยสิ้นเชิง:

  • ค้าง: แท็บขึ้น "Page unresponsive" และอาจฟื้นเองในหลายสิบวินาที สาเหตุคือ long task ที่กิน main thread หมด;
  • พัง: แท็บกลายเป็น "Aw, Snap" มักเกิดจากหน่วยความจำหมด (OOM)

ไม่ว่ากรณีไหน อย่าประมวลผลบน main thread ให้ย้ายการ parse ไปที่ Web Worker:

// worker.js
self.onmessage = (event) => {
  const parsed = JSON.parse(event.data);
  self.postMessage(parsed);
};

Worker แก้ปัญหา "UI ไม่ค้าง" ไม่ได้แก้เพดานหน่วยความจำ แท็บของเบราว์เซอร์มักมีหน่วยความจำให้ใช้ 1–4 GB; JSON 200 MB ที่ parse เป็น object อาจกิน 1 GB หรือมากกว่า (ทุกคู่ key-value มี overhead ของ object) พอรวมการ render เข้าไปก็แตะเพดานได้ง่าย

ทางแก้จริงคือ อย่าโหลดทั้งหมด:

ขนาดข้อมูล แนวทาง
< 5 MB parse ด้วย JSON.parse ตรง ๆ และระวังการ render
5–50 MB parse ใน Worker + virtual scrolling
> 50 MB streaming parsing เก็บเฉพาะฟิลด์ที่ต้องใช้
> 500 MB เปลี่ยนรูปแบบข้อมูล (Parquet / SQLite / JSON Lines แบบแบ่งส่วน)

JSON.parse(JSON.stringify(obj)) เป็น deep clone ไหม?

เป็น deep clone แต่มัน "เปลี่ยนข้อมูลอย่างเงียบ ๆ" หลายจุด จึงไม่เหมาะเป็นเครื่องมือ clone ทั่วไป

const source = {
  date: new Date(),
  fn: () => {},
  undef: undefined,
  nan: NaN,
  map: new Map(),
  nested: { a: 1 },
};
JSON.parse(JSON.stringify(source));
// {
//   date: "2026-10-30T03:00:00.000Z",  // Date กลายเป็น string
//   nan: null,                          // NaN กลายเป็น null
//   nested: { a: 1 }                    // ระดับนี้เท่านั้นที่ถูก
// }
// fn กับ undef หายไป, map กลายเป็น {}

รายการปัญหา:

ชนิด ผลลัพธ์
Date กลายเป็น ISO string
NaN / Infinity กลายเป็น null
undefined / function / Symbol หายไปใน object, กลายเป็น null ใน array
Map / Set กลายเป็น {}
BigInt throw TypeError
reference ที่วนซ้ำ throw TypeError

เบราว์เซอร์สมัยใหม่มี structuredClone ซึ่งรองรับ Date, Map, Set และ reference ที่วนซ้ำ แต่ไม่รองรับ function:

const copy = structuredClone(source);

ถ้าเป็นแค่การ merge config แบบตื้น spread operator คือตัวเลือกที่ถูกที่สุด: { ...config, timeout: 3000 }

ทำไมขยายมุมมอง tree แล้วกระตุกมาก?

เพราะทุก node เป็น DOM element และเมื่อจำนวน node ถึงหลักหมื่น ต้นทุน layout ของเบราว์เซอร์ก็พุ่ง

JSON 10 MB อาจมี leaf node 50k–100k ตัว แม้จะ render เฉพาะพื้นที่ที่มองเห็น การสร้าง DOM ครั้งเดียวยังใช้เวลาหลายร้อยมิลลิวินาทีถึงหลายวินาที และการเลื่อนจอทุกครั้งก็ทำให้เกิด reflow

แนวทางที่ถูกต้อง:

  1. virtual scrolling: render เฉพาะไม่กี่สิบแถวใน viewport และนำ DOM node กลับมาใช้ซ้ำขณะเลื่อน;
  2. ยุบไว้โดยค่าเริ่มต้น: ขยายเฉพาะสองระดับแรก และ render ระดับลึกเมื่อต้องการ;
  3. จำกัดขนาดการ render: เกิน 10k node ให้แสดง "ข้อมูลใหญ่เกินไป ใช้การค้นหาเพื่อระบุตำแหน่ง" แทนการฝืน render

นี่คือเหตุผลที่ editor บนเว็บไซต์นี้ลดคุณภาพการแสดงผลอย่างนุ่มนวลเมื่อเจอไฟล์ใหญ่ — ไม่ใช่เพราะทางเทคนิคมัน render ไม่ได้ แต่เพราะ render ออกมาแล้วใช้งานไม่ได้

2. ทำไมถึงถูกโจมตี

__proto__ injection ทำงานอย่างไร?

คือ prototype pollution ผู้โจมตีสร้าง JSON ที่มี key __proto__; ถ้าโค้ดของคุณ merge มันเข้า object โดยไม่กรอง มันจะเขียนทับ Object.prototype และกระทบทุก object ใน runtime

const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');

// โค้ดอันตราย: merge แบบเรียกซ้ำโดยไม่ตรวจชื่อ key
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      target[key] = target[key] || {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

merge({}, payload);
({}).isAdmin;  // true — ทุก object ถูกปนเปื้อนแล้ว

ข้อเท็จจริงสำคัญ: JSON.parse เอง ไม่ ปนเปื้อน prototype; {"__proto__": ...} parse ออกมาเป็น own property ธรรมดา สิ่งที่อันตรายคือการ merge, การกำหนดค่า, Object.assign, lodash.merge และปฏิบัติการคล้ายกันหลัง parse

การป้องกันสามข้อ:

  1. กรองชื่อ key: ข้าม __proto__, constructor และ prototype เมื่อ merge;
  2. ใช้ Object.create(null) เป็น container เพื่อไม่มี prototype chain ให้ปนเปื้อน;
  3. ใช้ Map เพื่อเก็บคู่ key-value ที่ไม่น่าเชื่อถือ
function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue;
    }
    const value = source[key];
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      target[key] = safeMerge(target[key] ?? {}, value);
    } else {
      target[key] = value;
    }
  }
  return target;
}

โปรดทราบว่า for...in ไล่ property ที่สืบทอดมาด้วย; Object.keys() ปลอดภัยกว่า

ต้องระวังอะไรเมื่อ parse JSON ที่ไม่น่าเชื่อถือ?

ทำตามลำดับนี้ — ต้นทุนต่ำ ผลตอบแทนสูง:

  1. จำกัดขนาด: ปฏิเสธทันทีถ้าเกินขีดที่ตั้งไว้ โดยไม่ต้องอ่านเข้า memory ก่อน;
  2. จำกัดความลึก: JSON ที่ซ้อนกันหลายร้อยระดับทำให้ stack ของ parser แบบเรียกซ้ำล้นได้;
  3. จำกัดจำนวน key: ป้องกันการโจมตีแบบ hash collision และการขยายหน่วยความจำ;
  4. กรอง key อันตราย: __proto__, constructor, prototype;
  5. ตรวจโครงสร้าง: ผ่าน Schema เพื่อยืนยันชนิดและฟิลด์ที่จำเป็น;
  6. อย่าเชื่อ Content-Type: ชนิดที่ประกาศกับเนื้อหาจริงอาจไม่ตรงกัน

การตรวจความลึกมีต้นทุนต่ำ; ใช้ regex กรองหยาบ ๆ ก่อน parse หรือไล่ตรวจหลัง parse ก็ได้:

function measureDepth(value, depth = 0) {
  if (value === null || typeof value !== 'object') {
    return depth;
  }
  let maxDepth = depth;
  for (const key of Object.keys(value)) {
    maxDepth = Math.max(maxDepth, measureDepth(value[key], depth + 1));
  }
  return maxDepth;
}

แต่โค้ดนี้เองก็เรียกซ้ำ และอาจยังล้น stack กับข้อมูลที่จงใจทำลึก วิธีที่ปลอดภัยคือบังคับขีดจำกัดระหว่างการ parse ด้วย parser ที่รองรับตัวเลือกความลึก (ไลบรารีบางตัวมีพารามิเตอร์ maxDepth)

XSS ผ่าน JSON เกิดขึ้นได้อย่างไร?

JSON เองไม่ใช่ HTML และไม่ถูก execute XSS เกิดตรงจุดที่ "ค่า JSON ถูกต่อ string เข้า HTML"

เส้นทางที่พบบ่อยสามทาง:

เส้นทาง ตัวอย่าง การป้องกัน
ต่อ string ใน template ฝั่งเซิร์ฟเวอร์ res.send('<div>' + data.name + '</div>') escape HTML ตอนส่งออก
innerHTML ฝั่ง frontend el.innerHTML = data.html ใช้ textContent หรือ sanitize
ฝังใน script แบบ inline <script>var d = ${JSON.stringify(data)}</script> escape <, >, &

ข้อที่สามคือข้อที่ละเอียดอ่อนที่สุด JSON.stringify ไม่ escape < ดังนั้นข้อมูลที่มี </script><script>alert(1)</script> สามารถปิดแท็ก script ก่อนเวลาอันควรได้:

const data = { name: '</script><script>alert(1)</script>' };
// ผลลัพธ์: <script>var d = {"name":"</script><script>alert(1)</script>"}</script>

วิธีแก้คือแปลงอักขระอันตรายเป็น Unicode escape:

function safeJsonForHtml(data) {
  return JSON.stringify(data)
    .replace(/</g, '\\u003c')
    .replace(/>/g, '\\u003e')
    .replace(/&/g, '\\u0026')
    .replace(/\u2028/g, '\\u2028')
    .replace(/\u2029/g, '\\u2029');
}

ควรจำกัดความลึกของการซ้อนไหม?

ควร นี่คือหนึ่งในการป้องกันที่คุ้มค่าที่สุดเมื่อเทียบกับต้นทุน

ผลของการไม่จำกัด: ฟังก์ชันเรียกซ้ำทำให้ stack ล้น, JSON.parse พังในบาง implementation เมื่อความลึกมากเกิน และการ serialize ก็ทำให้ stack ล้นแบบเดียวกัน [[[[...]]]] ขนาด 1 KB ก็เพียงพอที่จะล้ม parser ที่ไม่มีเกราะป้องกัน

ขีดที่สมเหตุสมผลขึ้นกับธุรกิจ:

ชนิดข้อมูล ขีดจำกัดที่แนะนำ
object ธุรกิจทั่วไป 10–20 ระดับ
โครงสร้าง tree (org chart, ไดเรกทอรี) 50–100 ระดับ
API gateway ทั่วไป 20 ระดับ

JSON hijacking คืออะไร?

ช่องโหว่เชิงประวัติศาสตร์: เบราว์เซอร์รุ่นเก่าอนุญาตให้ <script src> ข้ามโดเมนโหลดและ execute JSON และผู้โจมตีขโมยข้อมูลโดยนิยาม constructor Array หรือ Object.prototype.__defineSetter__ ใหม่

การป้องกันสมัยใหม่ค่อนข้างครบ:

  • เบราว์เซอร์ไม่อนุญาตให้ hijack array literal ด้วย __defineSetter__ แล้ว;
  • Content-Type: application/json ที่ถูกต้องป้องกันการ execute script;
  • X-Content-Type-Options: nosniff ป้องกัน MIME sniffing

ระบบเก่ายังมีความเสี่ยงตกค้าง คำแนะนำในทางปฏิบัติ: ใช้ object ที่ระดับบนสุด ไม่ใช่ array เปล่า wrapper อย่าง {"data": [...]} ทำให้ response ไม่เป็น JavaScript statement ที่ถูกต้อง ({ ถูกมองเป็น code block) และบล็อก hijacking ที่ระดับไวยากรณ์

การ log JSON มีความเสี่ยงอะไร?

สามข้อ:

  1. ข้อมูลละเอียดอ่อนถูกเขียนลงดิสก์: เบอร์โทร, เลขบัตรประชาชน, token และรหัสผ่านแบบ plaintext ตกลงในระบบ log ซึ่งมักมีสิทธิ์หลวมกว่าและเก็บนานกว่า;
  2. log injection: ค่า JSON ที่มีขึ้นบรรทัดใหม่และคำนำหน้า log ปลอมจะแยกระเบียนหนึ่งเป็นหลายรายการและปนเปื้อน audit trail;
  3. ขนาดพุ่ง: การเขียน response 10 MB ทั้งก้อนลง log ทำให้ระบบ log เต็มอย่างรวดเร็ว

แนวทาง:

// ปิดข้อมูลและตัดทอนก่อน log
function safeLog(data) {
  const cloned = structuredClone(data);
  for (const key of ['password', 'token', 'idCard', 'phone']) {
    if (key in cloned) cloned[key] = '***';
  }
  const text = JSON.stringify(cloned);
  return text.length > 2000 ? text.slice(0, 2000) + '...(truncated)' : text;
}

ระบบ log แบบ structured (เช่น pino) escape ขึ้นบรรทัดใหม่เอง แต่การปิดข้อมูลและการตัดทอนยังเป็นหน้าที่ของคุณ

3. สรุปเร็ว

อาการ ตรวจอะไรก่อน วิธีแก้
parse ช้า ปริมาณข้อมูล, มี deep clone หรือไม่ แบ่งส่วน/streaming, เลิกใช้ JSON.parse(JSON.stringify())
หน้าเว็บค้าง long task บน main thread, จำนวน node ที่ render Worker + virtual scrolling
แท็บพัง การใช้หน่วยความจำ อย่าโหลดทั้งหมด เปลี่ยนไปใช้ streaming
deep clone ข้อมูลหาย Date, NaN, Map ใช้ structuredClone
object จู่ ๆ มี property เกินมา prototype pollution กรอง __proto__, ใช้ Map
มี script เด้งขึ้นมา string ถูกต่อเข้า HTML escape <, ใช้ textContent
stack overflow ความลึกของการซ้อน จำกัดที่ 10–20 ระดับ
log เต็ม log ทั้ง response ตัดทอน + ปิดข้อมูล