ทำไมต้นไม้ JSON ขนาดใหญ่จึงกระตุก: กลยุทธ์ virtual scrolling และการยุบ
สรุปสำคัญ: การกระตุกเริ่มเมื่อมีการสร้าง DOM node ไม่ใช่เมื่อคุณเลื่อนหน้า ครอบคลุมการเรนเดอร์แบบยุบก่อน รายการเสมือน การไฮไลต์ผลค้นหา และขอบเขต Worker พร้อมตัวชี้วัด

สรุปสั้น: แยก "ช้า" ออกเป็น การแยกวิเคราะห์ การสร้าง node การจัดวางและวาด และการตอบสนองต่อการโต้ตอบ แล้ววัดแต่ละขั้น จากนั้นทำตามลำดับนี้ — ยุบไว้เป็นค่าเริ่มต้น เพิ่มรายการเสมือน ย้ายการคำนวณไปยัง Worker และจำกัดการไฮไลต์เฉพาะแถวที่มองเห็น การกระโดดไปใช้รายการเสมือนทันทีโดยไม่ยุบเพียงลดชุดที่มองเห็น แต่การคำนวณทรีทั้งต้นใหม่ยังมีต้นทุนอยู่
การกระตุกมักมาจาก DOM node หลายแสนโหนดที่คุณสร้างขึ้นสำหรับ 5MB นั้น ไม่ใช่จาก 5MB นั้นเอง
1. แยก "ช้า" ออกเป็นสี่ขั้น
เมื่อผู้ใช้บอกว่า "เปิดแล้วมันค้าง" ปัญหาอาจอยู่ได้ในสี่ขั้นที่ต่างกันโดยสิ้นเชิง แต่ละขั้นมีวิธีแก้ต่างกัน:
| ขั้น | ตัวกระตุ้นทั่วไป | เกณฑ์ที่รับรู้ได้ | ต้นทุนหลัก |
|---|---|---|---|
| แยกวิเคราะห์และสร้างออบเจ็กต์ | เปิดไฟล์ วางข้อความ | งานใดเกิน 50ms | การสแกนสตริง การสร้างกราฟออบเจ็กต์ |
| สร้าง DOM node | เรนเดอร์ทั้งทรีพร้อมกัน | หลายหมื่นโหนด | การจัดสรร element, text node, วัตถุ style |
| จัดวางและวาด | แสดงครั้งแรก เลื่อน | เฟรมใดเกิน 16.7ms | เรขาคณิต reflow, repaint |
| ตอบสนองการโต้ตอบ | ขยาย ยุบ ค้นหา | เกิน 100ms รู้สึกหน่วง | การแบนและเรนเดอร์ใหม่ |
Main thread ของเบราว์เซอร์ทำทีละอย่าง หากขั้นใดขั้นหนึ่งยึดมันไว้เกิน 50ms การเลื่อนและการพิมพ์จะตกเฟรม; งานแบบนั้นหลายงานติดกันแล้วหน้าเว็บดูเหมือนตาย การปรับแต่งข้อแรกจึงไม่ใช่ requestAnimationFrame แต่คือการวัดว่าขั้นไหนใช้เวลาจริง
2. จำนวน node คือศัตรูตัวจริง
Tree viewer เพิ่มจำนวน node ตามการออกแบบ โดยปกติบรรทัด JSON หนึ่งบรรทัดกลายเป็น "ย่อหน้า + คีย์ + ทวิภาค + ค่า + จุลภาค + chevron" ดังนั้นการประมาณแบบอนุรักษ์คือ 3 ถึง 6 DOM node ต่อบรรทัด:
| ขนาดไฟล์ | จำนวนบรรทัดโดยประมาณ | DOM node ของ tree view (ประมาณ) | การเรนเดอร์ครั้งเดียวหน้าตาเป็นอย่างไร |
|---|---|---|---|
| 100 KB | 2,500 | 10,000 | ปกติพอทนได้ |
| 1 MB | 25,000 | 100,000 | กระตุกชัดเจน อาจมีหน้าจอว่างหลายวินาที |
| 5 MB | 125,000 | 500,000 | มีแนวโน้มสูงที่จะไม่ตอบสนอง |
| 20 MB | 500,000 | เกิน 2 ล้าน | ทั้งหน่วยความจำและการจัดวางพัง |
แต่ละ element ไม่ใช่แค่ออบเจ็กต์ไม่กี่สิบไบต์: มันพากฎ style, กล่องการจัดวาง และคำสั่งวาด เมื่อสร้างแล้ว node ยังมีต้นทุนการจัดวางและ style แม้ภายหลังจะถูกซ่อนโดย parent ที่ยุบไว้ ตราบใดที่มันยังไม่ได้ถูกลบจริง "เรนเดอร์ทุกอย่างแล้วซ่อน" ไม่ใช่การปรับแต่ง มันเพียงเลื่อนต้นทุนไปยัง reflow ครั้งถัดไป
3. ยุบก่อน ค่อยทำเสมือน
Virtual scrolling แก้ปัญหา "แถวที่มองเห็นมากเกินไป" การยุบแก้ปัญหา "เราไม่ควรต้องเห็นแถวมากขนาดนี้เลย" ลำดับสำคัญ: สำหรับเอกสารหนึ่งล้านบรรทัดที่ขยายไว้เป็นค่าเริ่มต้น แม้รายการเสมือนที่สมบูรณ์แบบก็ต้องคำนวณทรีทั้งหมดใหม่เมื่อผู้ใช้คลิก "ยุบทั้งหมด"
กลยุทธ์การยุบที่ใช้งานได้:
- ขยายเฉพาะสองระดับแรกเป็นค่าเริ่มต้น root และฟิลด์โดยตรงของมันมองเห็นได้ ระดับที่ลึกกว่านั้นแสดงสรุปเช่น
items: Array(12000); - ใส่ชนิดและจำนวนในสรุป ผู้ใช้ที่ตรวจว่า "ชื่อฟิลด์ถูกต้องหรือไม่" แทบไม่ต้องดูหนึ่งหมื่นเรกคอร์ด;
- แคชแถวต่อ subtree แต่ละโหนดที่ขยายเป็นเจ้าของอาร์เรย์แถวที่แบนแล้ว การยุบทิ้งมัน และการขยายคำนวณเฉพาะ subtree นั้นใหม่;
- อย่าสแกนทรีทั้งหมดใหม่ทุกครั้งที่สลับ หากขยายที่แถว 3,000 ให้แทนเฉพาะช่วงนั้นและคงแถวรอบข้างไว้
/**
* แทรกแถวของ subtree ต่อจาก parent แทนที่จะแบนทรีทั้งหมดใหม่
* @param rows อาร์เรย์แถวที่มองเห็นปัจจุบัน
* @param parentIndex ดัชนีของแถว parent
* @param childRows แถวที่แบนแล้วของ subtree
* @returns อาร์เรย์แถวที่มองเห็นชุดใหม่
*/
function replaceSubtreeRows(
rows: TreeRow[],
parentIndex: number,
childRows: TreeRow[],
): TreeRow[] {
const nextRows = rows.slice();
nextRows.splice(parentIndex + 1, 0, ...childRows);
return nextRows;
}
รูปแบบที่ต้องหลีกเลี่ยงคือ "ขยายหนึ่งครั้ง จัดวางใหม่ทั้งหน้า" การไล่กราฟออบเจ็กต์ทั้งหมดใหม่ทุกคลิกทำให้ต้นทุนโตเป็นเส้นตรงกับขนาดเอกสาร ด้วยการแคชต่อ subtree การขยายครั้งเดียวมีต้นทุนเท่ากับ subtree นั้นเท่านั้น
4. รายการเสมือน: เรนเดอร์เฉพาะ viewport
เมื่อการยุบลดจำนวนแถวลงแล้ว ส่วนที่เหลือยังอาจมีหลายหมื่นแถว รายการเสมือนกำหนดความสูงของคอนเทนเนอร์เป็น "จำนวนแถวทั้งหมด x ความสูงแถวคงที่" และสร้าง element เฉพาะ viewport บวกบัฟเฟอร์เล็กน้อยด้านบนและล่าง
const ROW_HEIGHT = 24;
const OVERSCAN = 8;
/**
* คำนวณช่วงแถวที่ viewport ควรเรนเดอร์
* @param rows แถวที่มองเห็นทั้งหมด
* @param scrollTop offset การเลื่อนปัจจุบันของคอนเทนเนอร์
* @param viewportHeight ความสูงที่มองเห็นของคอนเทนเนอร์
* @returns แถวที่จะเรนเดอร์ ดัชนีเริ่มต้น และ offset แนวตั้ง
*/
function getViewportRows(
rows: TreeRow[],
scrollTop: number,
viewportHeight: number,
): { visibleRows: TreeRow[]; startIndex: number; offsetTop: number } {
const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
const end = Math.min(
rows.length,
Math.ceil((scrollTop + viewportHeight) / ROW_HEIGHT) + OVERSCAN,
);
const visibleNodes = rows.slice(start, end);
return {
visibleRows: visibleNodes,
startIndex: start,
offsetTop: start * ROW_HEIGHT,
};
}
ข้อจำกัดที่ไม่สามารถต่อรองได้:
- ความสูงแถวต้องคงที่ เท่านั้นที่
scrollTop / ROW_HEIGHTจะให้ดัชนีได้โดยตรง การอนุญาตให้แถวตัดบรรทัดบังคับให้คุณวัดและแคชทุกความสูง ซึ่งเพิ่มความซับซ้อนและความสั่นไหวขึ้นหนึ่งลำดับขนาด; - offset ด้วย
transformหรือ absolute positioning อย่าดันเนื้อหาลงด้วย spacer element หรือทุกการเลื่อนจะเปลี่ยนแปลง layout; - ใช้ key ที่มั่นคง กำหนด key ของแถวตาม path ไม่ใช่ดัชนีอาร์เรย์ มิฉะนั้นการขยายแถวหนึ่งจะ remount ทุกแถวหลังจากนั้น;
- คำนวณเลขเฉพาะใน scroll handler เลื่อนการเรนเดอร์ไปที่
requestAnimationFrameและป้องกันการจัดตารางซ้ำ; - เก็บบัฟเฟอร์เล็ก ห้าถึงสิบแถวแต่ละด้านก็พอ การเพิ่มบัฟเฟอร์เป็นสองเท่าจะเพิ่ม DOM node และงาน style เป็นสองเท่า
ด้วย viewport 800 พิกเซลและแถว 24 พิกเซล จะมองเห็นประมาณ 34 แถว และแม้มีบัฟเฟอร์คุณเรนเดอร์ประมาณห้าสิบ เอกสารเบื้องหลังอาจมีหนึ่งแสนแถวขณะที่ DOM ถือ element ไม่กี่ร้อยตัวอย่างสม่ำเสมอ ทำให้ต้นทุนการจัดวางแบนราบ
5. ยุบ ขยาย และการยึดจุดยึดการเลื่อน
เมื่อดัชนีแถวกลายเป็นพิกัดเดียว รายละเอียดการโต้ตอบบางอย่างเริ่มพัง:
- รักษาจุดยึดการมองเห็นเมื่อยุบเหนือ viewport แถวด้านล่างเลื่อนขึ้น หาก
scrollTopไม่ถูกปรับใน commit เดียวกัน เนื้อหาจะดูเหมือนกระโดด บันทึก path ของแถวจุดยึดและคืนตำแหน่งการเลื่อนโดยใช้ดัชนีใหม่ของมัน - ขยาย ancestor ก่อนกระโดด เมื่อการค้นหาพบฟิลด์ลึก ให้ขยาย parent ทุกตัวก่อน จากนั้นคำนวณดัชนีแถวเป้าหมาย แล้วจึงเลื่อนและไฮไลต์ ข้ามขั้นแรกแล้วแถวเป้าหมายจะไม่อยู่ในอาร์เรย์ที่มองเห็นด้วยซ้ำ
- ขับเคลื่อนการนำทางด้วยคีย์บอร์ดจากแถวที่มองเห็น ขึ้นและลงเลื่อนดัชนีแถว ซ้ายและขวาขยายและยุบ อย่าไล่ทรีทั้งหมดใน key handler
- รวมการอัปเดตเป็นหนึ่งเฟรม เมื่อขยายหลายระดับติดกัน ให้รวมการเปลี่ยนแปลงอาร์เรย์และเรนเดอร์ครั้งเดียว มิฉะนั้นแต่ละระดับจะกระตุ้น reflow เต็มรูปแบบ
6. การค้นหาและการไฮไลต์: คำนวณครั้งเดียว แสดงเล็กน้อย
การค้นหาเป็นกับดักประสิทธิภาพที่ถูกละเลยบ่อยที่สุดใน tree viewer หากทุกการกดปุ่มไล่กราฟออบเจ็กต์ทั้งหมดและสร้างรายการผลลัพธ์ใหม่ เอกสารหนึ่งแสนโหนดจะค้างทันที
แนวทางแบบแบ่งชั้น:
- Debounce อินพุต 150-250ms และติดหมายเลขลำดับให้แต่ละการค้นหาเพื่อทิ้งผลลัพธ์ที่ล้าสมัย;
- สแกนใน Worker การไล่กราฟออบเจ็กต์เป็นการคำนวณล้วนที่ไม่ขึ้นกับ DOM ดังนั้นไม่ควรบล็อกการพิมพ์;
- เก็บเฉพาะ path และดัชนีแถว อย่าคัดลอกโหนดที่ตรงทุกตัวหรือสร้าง HTML ไฮไลต์ล่วงหน้า;
- ไฮไลต์เฉพาะแถวที่มองเห็น viewport มีไม่กี่สิบแถว ให้ใช้ style ตาม path ที่ตรงตอนเรนเดอร์;
- ใช้ "ขยาย ancestor แล้วหาดัชนี" ซ้ำสำหรับการกระโดด เส้นทางเดียวกับที่ใช้เมื่อคลิก chevron
รายละเอียดด้านความปลอดภัยข้อหนึ่ง: คีย์และค่าใน tree viewer คือข้อมูลผู้ใช้ การสร้าง HTML ไฮไลต์จากค่าเหล่านั้นต้อง escape มิฉะนั้นสตริงที่มี markup จะกลายเป็น XSS JSON viewer ไม่ต้องการ rich text ดังนั้น text node บวกชื่อคลาสจึงปลอดภัยและเร็วกว่าทั้งคู่
7. ไฮไลต์ไวยากรณ์: CodeMirror กับ Monaco เป็นเครื่องมือที่ต่างกัน
Tree view กับ text editor เป็นโมเดลการเรนเดอร์ที่ต่างกัน ดังนั้น "อันไหนเร็วกว่า" ไม่ใช่การเปรียบเทียบตรง ๆ:
| มิติ | CodeMirror 6 | Monaco Editor |
|---|---|---|
| การวางตำแหน่ง | แกนเอดิเตอร์แบบโมดูลาร์ | เอดิเตอร์ IDE เต็มรูปแบบ |
| การเรนเดอร์ | อิง viewport | อิง viewport แต่ model และ decoration หนักกว่า |
| การแยกวิเคราะห์ | Lezer แบบเพิ่มพูนบนช่วงที่เปลี่ยน | Monarch tokenizer ทีละบรรทัด |
| ไฟล์ขนาดใหญ่ | ขึ้นกับส่วนขยาย เบาเป็นค่าเริ่มต้น | ครบฟีเจอร์ หน่วยความจำและต้นทุนเริ่มต้นสูงกว่า |
| การพับและการค้นหา | ส่วนขยายแยก โหลดตามต้องการ | มีในตัวพร้อม overhead ถาวร |
| กับดักทั่วไป | ส่วนขยายมากเกินไปทำให้ต้นทุนกลับมา | Minimap, sticky scroll และ decoration ซ้อนกัน |
ข้อสรุปเชิงปฏิบัติสองข้อ:
- Tree view ไม่ต้องการ tokenizer เต็มรูปแบบ ให้คีย์ สตริง ตัวเลข และบูลีนสีละหนึ่งสีด้วย text node ธรรมดาและคลาส CSS; ต้นทุนต่ำกว่าการรันไฮไลต์ไวยากรณ์หนึ่งลำดับขนาด;
- อย่า mount tree view และเอดิเตอร์พร้อมกัน การดูแลโมเดลสองชุดสำหรับเอกสารเดียวเพิ่มหน่วยความจำเป็นสองเท่า ทำลาย view เก่าแทนที่จะซ่อนมัน
หากเอดิเตอร์อยู่ใน viewer จริง ๆ CodeMirror 6 เหมาะกับ "อ่านอย่างเดียว ไฟล์ขนาดใหญ่ ไฮไลต์ภายในเครื่อง" ขณะที่ Monaco เหมาะกับ completion, diagnostics และ multi-cursor editing วัดเวลา first paint และหน่วยความจำที่อยู่อาศัยก่อนอ่านรายการฟีเจอร์ สำหรับการเปรียบเทียบเอดิเตอร์และเครื่องมือ CLI เชิงลึก ดู เอดิเตอร์ เครื่องมือบรรทัดคำสั่ง และระบบอัตโนมัติ
8. ขอบเขต Worker: ย้ายการแยกวิเคราะห์และการสร้างแถวออกจาก main thread
อีกครึ่งของการกระตุกในการเรนเดอร์คือการแยกวิเคราะห์บน main thread สำหรับ JSON ที่เกิน 1MB การแยกวิเคราะห์ การจัดรูปแบบ และการสร้างแถวแบบแบนควรอยู่ใน Web Worker; main thread เพียงรับแถวและวาด viewport
Worker ไม่ได้ฟรี: postMessage ใช้ structured clone ดังนั้นการส่งกราฟออบเจ็กต์ 50MB กลับไปยัง main thread จะสร้างสำเนาที่สองและเพิ่มจุดสูงสุดเป็นสองเท่า ขอบเขตที่ถูกต้องคือ แยกวิเคราะห์และแบนใน Worker คืนเฉพาะแถวที่ renderer ต้องการ:
worker.onmessage = (event: MessageEvent<{ rows: TreeRow[] }>) => {
rows = event.data.rows;
requestAnimationFrame(() => renderViewport());
};
worker.postMessage({ text });
หากข้อมูลแถวยังใหญ่ ให้บีบอัดแถวเป็นอาร์เรย์คู่ขนาน (path, type, value offset) ใส่ใน ArrayBuffer และโอนความเป็นเจ้าของเพื่อให้ main thread ไม่ต้องจ่ายค่าคัดลอก ต้นทุนคือการเขียน serializer ของคุณเอง ซึ่งคุ้มค่าที่หลักล้านแถวเท่านั้น
กฎสำหรับขอบเขตนั้นง่าย: main thread ควรทำงานเฉพาะที่ต้องแตะ DOM การแยกวิเคราะห์ การค้นหา การเรียงลำดับ การจัดรูปแบบ และการ diff ย้ายออกได้ทั้งหมด การเรนเดอร์ การเลื่อน อินพุต และ focus อยู่บน main thread และทุกเฟรมต้องพอดีกับงบประมาณงาน
9. ตัวชี้วัดและลำดับการนำไปใช้
การปรับแต่งหยุดที่ "รู้สึกเร็วขึ้น" ไม่ได้ ชุดต่อไปนี้ใส่ในรายการตรวจสอบการยอมรับได้เลย:
| ตัวชี้วัด | เป้าหมาย | วิธีวัด |
|---|---|---|
| เฟรมที่มองเห็นครั้งแรก | ต่ำกว่า 300ms | performance.now() จากเปิดถึงวาดครั้งแรก |
| Long task | ไม่มีตัวใดเกิน 50ms | PerformanceObserver เฝ้า longtask |
| เวลาเฟรมการเลื่อน | คงที่ต่ำกว่า 16.7ms | บันทึก Performance ใน DevTools |
| DOM element ใน viewport | ต่ำกว่า 2,000 | document.querySelectorAll('*').length |
| การตอบสนองขยาย/ยุบ | ต่ำกว่า 50ms | ประทับเวลารอบการโต้ตอบ |
| ค้นหาใน 100,000 แถว | ต่ำกว่า 300ms ภายใน Worker | จับเวลาใน Worker และส่งระยะเวลากลับ |
| Heap ที่อยู่อาศัย | ต่ำกว่า 3 เท่าของขนาดข้อความ | heap snapshot ใน DevTools Memory |
วัดด้วยไฟล์จริงแทนที่จะสร้างออบเจ็กต์เล็ก ๆ สม่ำเสมอ; รูปร่างข้อมูลสำคัญ และสตริงยาว การซ้อนลึก และอาร์เรย์ที่กว้างมากให้เส้นโค้งต่างกัน สำหรับบัญชีกราฟออบเจ็กต์เอง ดู บัญชีหน่วยความจำของ JSON
ทำตามลำดับนี้:
- วัด baseline: เวลาเรนเดอร์ครั้งแรก จำนวน node เวลาเฟรมการเลื่อน;
- ยุบเป็นค่าเริ่มต้น: ขยายสองระดับ แสดงชนิดและจำนวนในสรุป;
- รายการเสมือน: ความสูงแถวคงที่ key ที่มั่นคง viewport บวกบัฟเฟอร์เล็ก;
- ย้ายการคำนวณ: การแยกวิเคราะห์ การค้นหา และการแบนไปยัง Worker;
- จำกัดการไฮไลต์: จัด style เฉพาะแถวที่มองเห็น หลีกเลี่ยงการวาดทรีทั้งหมดใหม่;
- เพิ่มการป้องกัน: เมื่อเกินเกณฑ์ เสนอ "แสดง N แถวแรก" หรือบังคับให้ใช้ตัวกรองค้นหา
ทุกขั้นมีเป้าหมายเดียว: ลด node และออบเจ็กต์ที่ต้องมีอยู่ในขณะเดียวกัน แทนที่จะขอให้เบราว์เซอร์ทำงานหนักขึ้นกับมัน พื้นหลังเต็มของกลยุทธ์แบบแบ่งชั้นอยู่ใน การแยกวิเคราะห์ การเรนเดอร์ และการสตรีม JSON ขนาดใหญ่
Virtual scrolling แก้การกระตุกตอนแยกวิเคราะห์ได้หรือไม่?
ไม่ได้ Virtual scrolling ลดเฉพาะ DOM node หากการแยกวิเคราะห์ยังเกิดบน main thread หน้าก็ยังกระตุก ให้ย้ายการแยกวิเคราะห์ไปยัง Worker สำหรับเอกสารเกิน 1MB และจัดการสองปัญหาแยกกัน
ขยายทุกอย่างแล้วยังกระตุก มีอะไรผิด?
อาจเป็นเพราะรายการเสมือนไม่ได้ทำงานจริง หรือแต่ละการขยายสแกนทรีทั้งหมดใหม่ ตรวจว่าแถวนอกจอยังสร้าง DOM หรือไม่ และการขยายคำนวณเฉพาะ subtree ที่ได้รับผลกระทบหรือไม่
CodeMirror หรือ Monaco สำหรับ JSON viewer?
แนะนำ CodeMirror 6 สำหรับอ่านอย่างเดียว ไฟล์ขนาดใหญ่ และไฮไลต์ภายในเครื่อง; การโหลดแบบโมดูลาร์ตามต้องการเบากว่า เลือก Monaco เมื่อต้องการ completion, diagnostics และ multi-cursor editing ไม่ว่าจะเลือกอันไหน ให้หลีกเลี่ยงการ mount tree view และเอดิเตอร์สำหรับเอกสารเดียวกันพร้อมกัน
ทำไมตำแหน่งการเลื่อนผิดหลังกระโดดจากการค้นหา?
ปกติเพราะ view เลื่อนก่อนขยาย ให้ขยาย ancestor ทั้งหมดก่อน แล้วกำหนดตำแหน่งการเลื่อนจากดัชนีแถวใหม่ และไฮไลต์เป็นลำดับสุดท้าย กำหนด key ของแถวตาม path ไม่ใช่ดัชนี เพื่อให้การขยายไม่เลื่อนทุกดัชนี
FAQ
จะรู้ได้อย่างไรว่าการกระตุกมาจากขั้นไหน?
แยก "ช้า" ออกเป็น การแยกวิเคราะห์และสร้างออบเจ็กต์ การสร้าง DOM node การจัดวางและวาด และการตอบสนองการโต้ตอบ แล้วจับเวลาแต่ละขั้นด้วย performance.now() จากเปิดถึงวาดครั้งแรก จากนั้นเฝ้า longtask ด้วย PerformanceObserver — งานใดเกิน 50ms ทำให้ตกเฟรม และบันทึก Performance ใน DevTools แสดงว่าเวลาเฟรมการเลื่อนยังต่ำกว่า 16.7ms หรือไม่
ทำไมการเรนเดอร์ทั้งทรีพร้อมกันจึงทำให้หน้าจอว่าง?
เอกสาร 5MB ประมาณ 125,000 บรรทัด และ tree view ปกติเปลี่ยนแต่ละบรรทัดเป็น 3 ถึง 6 DOM node — ประมาณ 500,000 element พร้อมกัน แต่ละ element ยังพากฎ style, กล่องการจัดวาง และคำสั่งวาด ดังนั้นงาน main thread หลายงานเกิน 50ms ติดกันทำให้หน้าเว็บดูเหมือนตาย Node ที่ถูกซ่อนโดย parent ที่ยุบไว้ยังมีต้นทุนการจัดวาง ดังนั้น "เรนเดอร์ทุกอย่างแล้วซ่อน" ไม่ใช่การปรับแต่ง
ควรส่งกราฟออบเจ็กต์ 50MB กลับจาก Worker หรือไม่?
ไม่ควร postMessage ใช้ structured clone ดังนั้นฝั่งรับสร้างสำเนาที่สองและจุดสูงสุดเพิ่มเป็นสองเท่า ขอบเขตที่ถูกต้องคือแยกวิเคราะห์และแบนภายใน Worker แล้วคืนเฉพาะแถวที่ renderer ต้องการ; ที่ระดับล้านแถวคุณสามารถแพ็กอาร์เรย์คู่ขนาน (path, type, value offset) ลงใน ArrayBuffer และโอนความเป็นเจ้าของได้