2 นาที

รายการแดชบอร์ด 100k แถว ให้เร็ว: เริ่มจากอะไร

เรียนรู้วิธีสร้างรายการแดชบอร์ดที่เร็วแม้มี 100k แถว โดยใช้การแบ่งหน้า การจำลองการแสดงผล การกรองอย่างชาญฉลาด และการปรับคำสั่งฐานข้อมูล เพื่อให้เครื่องมือภายในยังคงลื่นไหล

รายการแดชบอร์ด 100k แถว ให้เร็ว: เริ่มจากอะไร

ทำไมหน้ารายการถึงช้าลงเมื่อข้อมูลเพิ่มขึ้น

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

จุดเปลี่ยนที่พบบ่อยคือประมาณ 100k แถว เพราะมันกดทุกส่วนของระบบพร้อมกัน ชุดข้อมูลยังไม่ใหญ่เกินไปสำหรับฐานข้อมูล แต่ก็เพียงพอที่จะทำให้ความไม่ประสิทธิภาพเล็ก ๆ สะดุดตาบนเบราว์เซอร์และเครือข่าย หากพยายามโชว์ทุกอย่างพร้อมกัน หน้าจอเรียบง่ายจะกลายเป็นสายการประมวลผลหนัก

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

จะเป็นประโยชน์ถ้าจะแบ่งงานออกเป็นสี่ส่วน:

  • เครือข่าย: ส่งกี่ไบต์ และบ่อยแค่ไหน
  • ฐานข้อมูล: สแกน เรียง และรวมข้อมูลเท่าไหร่ต่อคำขอ
  • การเรนเดอร์ในเบราว์เซอร์: สร้าง วัด และวาดโหนด DOM กี่ตัว
  • งาน JavaScript: เรนเดอร์ซ้ำบ่อยแค่ไหน คำนวณคอลัมน์ หรือคำนวณผลลัพธ์บ่อยเท่าไร

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

เมื่อทีมสร้างเครื่องมือภายในอย่างรวดเร็ว (รวมถึงด้วยแพลตฟอร์มแบบโค้ดจากแชทเช่น Koder.ai) หน้ารายการมักเป็นที่แรกที่การเติบโตของข้อมูลจริงเปิดเผยช่องว่างระหว่าง “ใช้งานกับชุดข้อมูลเดโมได้” กับ “รู้สึกทันทีทุกวัน”

เลือกเป้าหมายประสิทธิภาพให้ถูกต้อง

ก่อนจะปรับจูน ให้ตัดสินใจว่า "เร็ว" สำหรับหน้านี้หมายถึงอะไร หลายทีมไล่ตาม throughput (โหลดทุกอย่าง) ในขณะที่ผู้ใช้ส่วนใหญ่ต้องการ low latency (เห็นการอัปเดตเร็ว) รายการสามารถรู้สึกทันทีแม้ว่าจะไม่โหลดแถวทั้ง 100k ก็ต่อเมื่อมันตอบสนองเร็วต่อการเลื่อน การเรียง และตัวกรอง

เป้าหมายที่ใช้งานได้จริงคือเวลาเพื่อแถวแรก ไม่ใช่เวลาในการโหลดทั้งหมด ผู้ใช้เชื่อถือเพจเมื่อเห็นแถวแรก 20–50 แถวอย่างรวดเร็วและการโต้ตอบยังคงลื่น

ควรวัดอะไร (และทำไม)

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

  • เวลาเพื่อแถวแรกหลังเปิดหน้า หรือเปลี่ยนตัวกรอง
  • เวลาเพื่อให้ตัวกรองหรือการเรียงแสดงผลอัปเดต
  • ขนาดการตอบกลับ (ขนาด payload JSON โดยประมาณ)
  • คำสั่งช้าในฐานข้อมูล (โดยเฉพาะ COUNT(*) และ SELECT กว้าง ๆ)
  • การกระโดดของ main-thread ในเบราว์เซอร์ (การเลื่อนกระตุก การพิมพ์หน่วง)

ตัวชี้เหล่านี้ตรงกับอาการที่พบบ่อย ถ้า CPU ของเบราว์เซอร์พุ่งเมื่อเลื่อน แสดงว่าฝั่ง frontend ทำงานหนักเกินต่อแถว หากสปินเนอร์รอก่อนแต่การเลื่อนหลังจากนั้นเป็นปกติ ปัญหามักอยู่ที่ backend หรือเครือข่าย ถ้าคำขอเร็วแต่หน้าเด้งค้าง มักจะเป็นการเรนเดอร์หรือการประมวลผลบนไคลเอนต์หนักเกินไป

ทดสอบ frontend vs backend แบบเร็ว ๆ

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

ตัวอย่าง: หน้าคำสั่ง (Orders) ภายในช้าขณะพิมพ์ค้นหา ถ้า API คืน 5,000 แถวแล้วเบราว์เซอร์กรองบนทุก keypress การพิมพ์จะหน่วง หาก API ใช้ 2 วินาทีเพราะ COUNT บนตัวกรองที่ไม่มีดัชนี คุณจะรอก่อนที่แถวใด ๆ จะเปลี่ยน ทั้งสองกรณีผู้ใช้ร้องเรียนเหมือนกัน แต่การแก้ต่างกัน

พื้นฐาน frontend: ทำให้การเรนเดอร์ถูกและเบา

เบราว์เซอร์มักเป็นคอขวดแรก หน้ารายการสามารถรู้สึกช้าได้แม้ API จะเร็ว เพราะหน้าพยายามวาดมากเกินไป กฎข้อแรกง่าย ๆ: อย่าเรนเดอร์หลายพันแถวใน DOM พร้อมกัน

แม้ก่อนจะเพิ่ม virtualization เต็มรูปแบบ ให้ทำให้แต่ละแถวเบาไว้ แถวที่มี wrapper ซ้อนกัน ไอคอน tooltip และสไตล์เชิงเงื่อนไขซับซ้อนในแต่ละเซลล์ จะมีค่าใช้จ่ายทุกครั้งที่เลื่อนและอัปเดต ให้ใช้ข้อความธรรมดา ป้ายเล็ก ๆ สองสามชิ้น และองค์ประกอบเชิงโต้ตอบไม่เกินหนึ่งหรือสองชิ้นต่อแถว

ความสูงแถวคงที่ช่วยได้มากกว่าที่คิด เมื่อทุกแถวมีความสูงเท่ากัน เบราว์เซอร์คาดเดาเลย์เอาต์ได้และการเลื่อนจะลื่น ความสูงแถวไม่คงที่จะทำให้ต้องวัดซ้ำและ reflow หากต้องการรายละเอียดเพิ่มเติม ให้พิจารณาแผงด้านข้างหรือพื้นที่ขยายเดียว ไม่ใช่แถวหลายบรรทัดทั้งหมด

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

กฎง่าย ๆ ให้จำ

ถ้าค่าหนึ่งมองไม่เห็น อย่าคำนวณมันตอนนี้ แคชผลการจัดรูปแบบที่แพงและคำนวณเมื่อจำเป็น เช่น เมื่อแถวปรากฏหรือเมื่อผู้ใช้เปิดแถว

รายการปรับปรุงด่วนที่มักได้ผลชัดเจน:

  • จำกัดการเรนเดอร์เริ่มต้นให้เป็นเพจขนาดเล็ก (หรือหน้าต่างเสมือน)
  • ให้ความสูงของแถวคงที่และหลีกเลี่ยงเซลล์หลายบรรทัด
  • เลื่อน UI หนัก (tooltip เมนู) ไว้ที่ hover หรือ click
  • ใช้ memoization หรือแคชสำหรับการจัดรูปแบบวันที่และสกุลเงินต่อแถว
  • หลีกเลี่ยงการเรนเดอร์ซ้ำทั้งชุดเมื่อมีการเปลี่ยนแค่ตัวกรองเดียว

ตัวอย่าง: ตาราง Invoices ภายในที่จัดรูปแบบคอลัมน์สกุลเงินและวันที่ 12 คอลัมน์จะกระตุกขณะเลื่อน การแคชค่าที่จัดรูปแบบต่อ invoice และหน่วงงานสำหรับแถวที่อยู่นอกหน้าจอสามารถทำให้รู้สึกทันที แม้ก่อนจะทำงานเชิงลึกฝั่ง backend

Virtualization: ทำให้การเลื่อน 100k แถวลื่น

Virtualization หมายถึงตารางจะวาดเฉพาะแถวที่มองเห็นได้จริง (บวก buffer เล็กน้อยด้านบนและล่าง) เมื่อเลื่อนมันจะนำ DOM element เดิมมาใช้ซ้ำแล้วสลับข้อมูลภายใน นั่นช่วยให้เบราว์เซอร์ไม่พยายามวาดคอมโพเนนต์แถวจำนวนหมื่น ๆ พร้อมกัน

Virtualization เหมาะกับรายการยาว ตารางกว้าง หรือแถวหนัก (avatar ชิปสถานะ เมนูการกระทำ tooltip) และมีประโยชน์เมื่อผู้ใช้เลื่อนเยอะและคาดหวังว่าจะเห็นวิวต่อเนื่องลื่น ๆ แทนการกระโดดเป็นหน้าทีละหน้า

จุดที่ virtualization ทำให้ยุ่งยาก

มันไม่ใช่เวทย์มนตร์ มีบางอย่างที่มักสร้างความประหลาดใจ:

  • ความสูงแถวไม่คงที่ (ข้อความตัดบรรทัด แถวขยายได้) ทำให้การเลื่อนลื่นถูกทำลายเว้นแต่จะวัดความสูงหรือรักษาขนาดแถวให้คงที่
  • หัวตารางยึดติด (sticky headers) และคอลัมน์ยึดติดอาจขัดกับ container แบบ virtual หากเลย์เอาต์ใช้ CSS ซับซ้อน
  • การนำทางด้วยคีย์บอร์ด (ขึ้น/ลง, page up/down, focus ในเซลล์) ต้องระวังเป็นพิเศษเพื่อไม่ให้ focus กระโดดไปยังแถวที่ยัง unmounted
  • การทำ bulk actions ต้องมีกฎชัดเจน: เลือกเฉพาะแถวที่มองเห็น เลือกตามตัวกรอง หรือเลือกทั้งชุดข้อมูล

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

Virtualization + Pagination (โดยไม่ทำให้ผู้ใช้สับสน)

คุณสามารถผสมทั้งสอง: ใช้การแบ่งหน้า (หรือโหลดแบบ cursor) เพื่อจำกัดสิ่งที่ดึงจากเซิร์ฟเวอร์ และใช้ virtualization เพื่อรักษาการเรนเดอร์ให้ถูกภายในสไลซ์ที่ดึงแล้ว

รูปแบบปฏิบัติคือดึงขนาดเพจปกติ (มัก 100–500 แถว), ทำ virtualization ภายในเพจนั้น, และให้ตัวควบคุมชัดเจนสำหรับย้ายระหว่างหน้า หากใช้ infinite scroll ให้แสดงตัวบ่งชี้ "Loaded X of Y" เพื่อให้ผู้ใช้เข้าใจว่าพวกเขายังไม่เห็นทั้งหมด

ตัวเลือกการแบ่งหน้าที่ยังเร็ว

เพิ่มประสิทธิภาพเวลาเพื่อแถวแรก
สร้างหน้ารายการแอดมินที่โหลดแถวแรก 50 แถวได้เร็ว และตอบสนองกับตัวกรองได้ลื่นไหล

ถ้าต้องการหน้ารายการที่ยังใช้งานได้เมื่อข้อมูลเพิ่ม การแบ่งหน้ามักเป็นค่าเริ่มต้นที่ปลอดภัย มันคาดเดาได้ ทำงานได้ดีกับเวิร์กโฟลว์แอดมิน (รีวิว แก้ไข อนุมัติ) และรองรับความต้องการทั่วไปเช่นการส่งออก "หน้า 3 พร้อมตัวกรองเหล่านี้" โดยไม่มีความประหลาดใจ หลายทีมกลับมาใช้การแบ่งหน้าหลังจากลองวิธีการเลื่อนอื่น ๆ

Infinite scroll ให้ความรู้สึกดีสำหรับการเรียกดูแบบไม่ตั้งใจ แต่มีต้นทุนแฝง ผู้ใช้จะเสียความรู้สึกว่าตัวเองอยู่ตรงไหน ปุ่มย้อนกลับมักไม่กลับไปตำแหน่งเดิม และเซสชันยาวอาจสะสมหน่วยความจำเมื่อโหลดแถวมากขึ้น ทางสายกลางคือปุ่ม Load more ที่ยังใช้เพจ ทำให้ผู้ใช้คงทิศทาง

Offset vs keyset pagination

Offset pagination เป็นแบบคลาสสิก page=10&size=50 มันง่าย แต่เมื่อโตขึ้นอาจช้าบนตารางใหญ่เพราะฐานข้อมูลอาจต้องข้ามแถวจำนวนมากไปยังหน้าหลัง ๆ และมันทำให้แถวเปลี่ยนหน้าถ้ามีแถวใหม่เข้ามา

Keyset pagination (เรียกอีกอย่างว่า cursor pagination) ขอ "50 แถวถัดไปหลังจากรายการที่เห็นล่าสุด" โดยใช้ id หรือ created_at มันมักจะเร็วเพราะไม่ต้อง count และ skip มาก

กฎปฏิบัติ:

  • ใช้ offset กับรายการเล็ก ชุดข้อมูลคงที่ และเมื่อจำเป็นต้องกระโดดไปหน้าที่ระบุ
  • ใช้ keyset กับรายการใหญ่ การแทรกบ่อย และการนำทาง Next/Previous

แสดงจำนวนรวมโดยไม่ต้องจ่ายแพงทุกครั้ง

ผู้ใช้มักชอบเห็นจำนวนรวม แต่การทำ "count ทุกแถวที่ตรงเงื่อนไข" อาจแพงกับตัวกรองหนัก ๆ ตัวเลือกได้แก่การแคชจำนวนสำหรับตัวกรองยอดนิยม, อัปเดตจำนวนในพื้นหลังหลังหน้าโหลด, หรือแสดงจำนวนโดยประมาณ (เช่น "10,000+")

ตัวอย่าง: หน้าสั่งซื้อภายในสามารถแสดงผลทันทีด้วย keyset pagination แล้วเติมจำนวนรวมที่แม่นยำเมื่อผู้ใช้หยุดเปลี่ยนตัวกรองสักครู่

ถ้าคุณสร้างด้วย Koder.ai ให้จัดการพฤติกรรมการแบ่งหน้าและการนับเป็นส่วนของสเปคหน้าตั้งแต่เริ่ม เพื่อที่ API และสถานะ UI ที่สร้างจะไม่ขัดกันทีหลัง

การกรองและการค้นหาที่ให้ความรู้สึกทันที

หน้ารายการส่วนใหญ่รู้สึกช้าเพราะเริ่มแบบเปิดกว้าง: โหลดทุกอย่าง แล้วค่อยให้ผู้ใช้แคบลง พลิกมุมมองนั้น เริ่มด้วยค่าพื้นฐานที่สมเหตุสมผลซึ่งคืนผลชุดเล็กที่มีประโยชน์ (เช่น 7 วันที่ผ่านมา, ไอเท็มของฉัน, สถานะ: Open) และให้ตัวเลือก All time เป็นการเลือกชัดเจน

การค้นหาด้วยข้อความเป็นกับดักทั่วไปอีกอย่าง ถ้ารัน query ทุกครั้งที่พิมพ์ คุณจะสร้างคิวคำขอและ UI กระพริบ ให้ debounce ช่องค้นหาเพื่อเรียกเมื่อผู้ใช้หยุดพิมพ์เล็กน้อย และยกเลิกคำขอเก่าพอมีคำขอใหม่ กฎง่าย ๆ: ถ้าผู้ใช้ยังพิมพ์ อย่าทุบเซิร์ฟเวอร์

การกรองจะรู้สึกเร็วเมื่อชัดเจนด้วย แสดงชิปตัวกรองด้านบนตารางเพื่อให้ผู้ใช้เห็นว่าสิ่งใดกำลังถูกใช้และสามารถลบได้ด้วยคลิกเดียว ให้ป้ายชิปอ่านง่ายเป็นภาษามนุษย์ ไม่ใช่ชื่อฟิลด์ดิบ (เช่น Owner: Sam แทน owner_id=42) เมื่อคนบอกว่า "ผลหายไป" มักเป็นเพราะมีตัวกรองที่มองไม่เห็น

รูปแบบที่ช่วยให้รายการใหญ่ตอบสนองโดยไม่ซับซ้อน UI:

  • ค่าเริ่มต้นเป็นตัวกรองแคบและช่วงวันที่สั้น
  • debounce การค้นหาด้วยข้อความและยกเลิกคำขอที่กำลังรัน
  • แสดงชิปตัวกรองและปุ่ม Clear all
  • เสนอ saved views สำหรับงานที่ใช้บ่อย
  • กั้นตัวกรองที่แพงจนกว่าจะมีตัวกรองแคบอย่างน้อยหนึ่งอย่าง

Saved views เป็นฮีโร่ที่เงียบ ๆ แทนที่จะสอนผู้ใช้ให้สร้างคอมโบตัวกรองครั้งเดียว ให้พวกเขามีพรีเซ็ตไม่กี่แบบที่ตรงงานจริง ทีม ops อาจสลับระหว่าง Failed payments วันนี้ กับ High-value customers ได้ด้วยคลิกเดียว ชุดงานเหล่านี้เข้าใจง่ายและรักษาความเร็วฝั่ง backend ได้ง่ายกว่าการกรอง ad-hoc

ถ้าคุณสร้างเครื่องมือภายในด้วยบิลเดอร์แบบขับเคลื่อนด้วยแชทอย่าง Koder.ai ให้ถือว่าตัวกรองเป็นส่วนหนึ่งของโฟลว์ผลิตภัณฑ์ ไม่ใช่ของเสริม เริ่มจากคำถามที่พบบ่อยที่สุด แล้วออกแบบมุมมองเริ่มต้นและ saved views รอบ ๆ คำถามเหล่านั้น

การปรับรูปแบบคำสั่ง (Query shaping): ส่งน้อย คำนวณน้อย

หน้ารายการไม่ค่อยต้องการข้อมูลเท่าหน้ารายละเอียด ถ้า API คืนทุกอย่างเกี่ยวกับทุกอย่าง คุณจ่ายสองครั้ง: ฐานข้อมูลทำงานมากขึ้น และเบราว์เซอร์ได้รับและเรนเดอร์มากกว่าที่ใช้จริง Query shaping คือการขอเฉพาะสิ่งที่หน้าต้องการตอนนี้

เริ่มจากคืนเฉพาะคอลัมน์ที่จำเป็นในการเรนเดอร์แต่ละแถว สำหรับแดชบอร์ดส่วนใหญ่ นั่นคือ id, ป้ายสองสามอัน, สถานะ, เจ้าของ, และ timestamps ข้อความยาว JSON blobs และฟิลด์คำนวณสามารถรอจนกว่าผู้ใช้จะเปิดแถว

หลีกเลี่ยง join หนักสำหรับการแสดงผลครั้งแรก Join ใช้ได้เมื่อชนกับดัชนีและคืนผลเล็ก แต่จะแพงเมื่อคุณ join หลายตารางแล้วเรียงหรือกรองบนข้อมูลที่ join มา รูปแบบง่าย ๆ คือ: ดึงรายการจากตารางเดียวให้เร็ว แล้วโหลดรายละเอียดที่เกี่ยวข้องตามต้องการ (หรือโหลดเป็นกลุ่มสำหรับแถวที่มองเห็นเท่านั้น)

จำกัดตัวเลือกการเรียงและเรียงด้วยคอลัมน์ที่มีดัชนี "เรียงได้ทุกอย่าง" ฟังดูมีประโยชน์ แต่บ่อยครั้งบังคับให้ต้องเรียงช้าในชุดข้อมูลใหญ่ ให้เลือกไม่กี่ตัวเลือกที่คาดเดาได้ เช่น created_at, updated_at, หรือ status และตรวจสอบให้คอลัมน์เหล่านั้นมีดัชนี

ระวังการรวมผลทางฝั่งเซิร์ฟเวอร์ COUNT(*) บนชุดกรองใหญ่, DISTINCT บนคอลัมน์กว้าง, หรือการคำนวณหน้าทั้งหมดอาจกินเวลาตอบกลับมาก

แนวทางปฏิบัติ:

  • ปรับรูปแบบการตอบกลับของรายการให้เหลือ 5–10 ฟิลด์
  • ดึงรายละเอียดแถวเมื่อเปิดแถวเท่านั้น
  • อนุญาตการเรียง 2–4 แบบ โดยอิงดัชนี
  • ถือว่า COUNT และ DISTINCT เป็นทางเลือก และแคชหรือประมาณเมื่อเป็นไปได้

ถ้าคุณสร้างเครื่องมือภายในบน Koder.ai ให้กำหนด query รายการแบบเบาแยกจาก query รายละเอียดในขั้นตอนวางแผน เพื่อ UI จะยังคงลื่นเมื่อข้อมูลโต

กลยุทธ์ฐานข้อมูลสำหรับตารางใหญ่

ทดสอบประสิทธิภาพใน production
ปรับใช้และโฮสต์เครื่องมือภายในของคุณ เพื่อให้เพื่อนร่วมงานทดสอบบนข้อมูลจริงได้เร็วขึ้น

ถ้าต้องการหน้ารายการที่ยังเร็วที่ 100k แถว ฐานข้อมูลต้องทำงานน้อยลงต่อคำขอ รายการช้าที่สุดมักไม่ใช่เพราะข้อมูลมากเกินไป แต่เป็นเพราะรูปแบบการเข้าถึงข้อมูลผิด

เริ่มจากดัชนีที่สอดคล้องกับการใช้งานจริงของผู้ใช้ หากรายการมักถูกกรองด้วย status และเรียงด้วย created_at คุณต้องการดัชนีที่รองรับทั้งสองในลำดับนั้น ไม่เช่นนั้นฐานข้อมูลอาจสแกนแถวมากกว่าที่คิดแล้วค่อยเรียง ซึ่งแพงเร็ว

การแก้ที่มักให้ผลใหญ่:

  • เพิ่มดัชนีผสมที่สะท้อนตัวกรอง + การเรียงที่พบบ่อย (เช่น tenant_id, status, created_at)
  • ชอบ keyset (cursor) pagination มากกว่า OFFSET ลึก ๆ OFFSET ทำให้ฐานข้อมูลต้องก้าวผ่านแถวจำนวนมากเพื่อข้าม
  • ถือว่าจำนวนรวมเป็นทางเลือก ตัวจำนวนแม่นยำอาจช้า ให้แคช ล่วงหน้า หรือนำเสนอเป็นค่าประมาณ
  • เก็บแถวให้บาง (thin) อย่าเลือกฟิลด์ข้อความยาว JSON blobs หรืออ็อบเจ็กต์ซ้อนสำหรับหน้ารายการ
  • ปรับคำสั่งให้คืนเฉพาะที่ UI เรนเดอร์: คอลัมน์ไม่กี่อันสำหรับตาราง บวก cursor ของหน้าถัดไป

ตัวอย่างง่าย ๆ: ตาราง Orders ภายในที่แสดงชื่อผู้ใช้ สถานะ จำนวน และวันที่ อย่า join ทุกตารางที่เกี่ยวข้องและดึงบันทึกคำสั่งเต็มสำหรับมุมมองรายการ ให้คืนเฉพาะคอลัมน์ที่ใช้ในตาราง แล้วโหลดที่เหลือในคำขอแยกเมื่อลูกค้าเปิดดูคำสั่ง

ถ้าคุณสร้างด้วยแพลตฟอร์มอย่าง Koder.ai ให้ยึดแนวคิดนี้แม้ UI จะถูกสร้างจากแชท ตรวจสอบให้ endpoint ที่สร้างรองรับ cursor pagination และฟิลด์ที่เลือกได้ เพื่อให้งานฐานข้อมูลคาดเดาได้เมื่อโตขึ้น

แผนทีละขั้นตอนเพื่อเร่งหน้ารายการที่มีอยู่

ถ้าหน้ารายการรู้สึกช้าที่สุดตอนนี้ อย่าเริ่มด้วยการเขียนใหม่ทั้งหมด เริ่มจากล็อกลงว่าใช้งานปกติเป็นอย่างไร แล้วเพิ่มประสิทธิภาพเส้นทางนั้น

แผนปฏิบัติ 5 ขั้นตอน

  1. กำหนดมุมมองเริ่มต้น. เลือกตัวกรองเริ่มต้น การเรียง และคอลัมน์ที่มองเห็น หน้าจะช้าเมื่อพยายามโชว์ทุกอย่างเป็นค่าพื้นฐาน

  2. เลือกสไตล์การแบ่งหน้าที่ตรงกับการใช้งาน. ถ้าผู้ใช้สแกนแค่หน้าแรกสองสามหน้า การแบ่งหน้าแบบคลาสสิกพอเพียง ถ้าผู้ใช้ไปลึกมาก (หน้า 200+) หรือคุณต้องการประสิทธิภาพคงที่ ให้ใช้ keyset pagination (อิงการเรียงเสถียรเช่น created_at บวก id)

  3. เพิ่ม virtualization สำหรับส่วนของตาราง. แม้ backend จะเร็ว เบราว์เซอร์อาจล่มเมื่อเรนเดอร์แถวเยอะเกินไป

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

  5. วัดผล แล้วปรับคำสั่งและดัชนี. บันทึกเวลาเซิร์ฟเวอร์ เวลาในฐานข้อมูล ขนาด payload และเวลาเรนเดอร์ จากนั้นตัดคำสั่ง: เลือกเฉพาะคอลัมน์ที่แสดง ใช้ตัวกรองตั้งแต่ต้น และเพิ่มดัชนีที่สอดคล้องกับตัวกรองเริ่มต้น + การเรียง

ตัวอย่าง: แดชบอร์ดซัพพอร์ตภายในที่มี 100k ตั๋ว ตั้งเป็นค่าเริ่มต้นเป็น Open มอบหมายให้ทีมของฉัน เรียงตามใหม่สุด แสดงหกคอลัมน์ และดึงแค่ ticket id, subject, assignee, status, และ timestamps ด้วย keyset pagination และ virtualization คุณจะรักษาความคาดเดาได้ทั้งฝั่งฐานข้อมูลและ UI

ถ้าคุณสร้างเครื่องมือภายในใน Koder.ai แผนนี้จับคู่ได้ดีกับเวิร์กโฟลว์ iterate-and-check: ปรับมุมมอง ทดสอบการเลื่อนและการค้นหา แล้วจูนคำสั่งจนหน้าไม่ช้า

ข้อผิดพลาดทั่วไปที่ทำให้รายการช้ามาก

ปรับแต่งอย่างปลอดภัยด้วย snapshots
ทดลองปรับรูปแบบคำสั่งแล้วย้อนกลับอย่างรวดเร็วถ้ามีการเปลี่ยนแปลงทำให้หน้าช้าลง

วิธีที่เร็วที่สุดทำให้หน้ารายการพังคือถือว่า 100k แถวเป็นหน้าข้อมูลปกติ กับดักที่พบบ่อยมีไม่กี่แบบ

ข้อใหญ่ข้อหนึ่งคือเรนเดอร์ทุกอย่างแล้วซ่อนด้วย CSS แม้มันจะดูเหมือนแค่ 50 แถวที่มองเห็น เบราว์เซอร์ยังจ่ายค่าการสร้าง 100k โหนด DOM วัดพวกมัน และ repaint ตอนเลื่อน ถ้าต้องการรายการยาว ควรเรนเดอร์เฉพาะที่ผู้ใช้เห็น (virtualization) และทำให้คอมโพเนนต์แถวเรียบง่าย

การค้นหาก็สามารถทำลายประสิทธิภาพเงียบ ๆ เมื่อตัวพิมพ์ทุกตัวทริกเกอร์การสแกนตารางเต็ม นี่เกิดขึ้นเมื่อตัวกรองไม่มีดัชนี เมื่อคุณค้นหาหลายคอลัมน์ หรือเมื่อรัน contains queries บนฟิลด์ข้อความใหญ่โดยไม่มีแผน กฎที่ดีคือตัวกรองแรกที่ผู้ใช้จะใช้ควรถูกออกแบบให้ถูกในฐานข้อมูล ไม่ใช่แค่สะดวกใน UI

ปัญหาทั่วไปอีกอย่างคือดึงระเบียนเต็มเมื่อหน้าแค่ต้องการสรุป แถวรายหนึ่งมักต้องการ 5–12 ฟิลด์ ไม่ใช่อ็อบเจ็กต์ทั้งหมดหรือคำอธิบายยาว ๆ การดึงข้อมูลเกินจำเป็นเพิ่มงานฐานข้อมูล เวลาเครือข่าย และการแยกวิเคราะห์ฝั่ง frontend

การส่งออกและการคำนวณจำนวนรวมสามารถทำให้ UI ค้างถ้าคำนวณบน main thread หรือรอคำขอหนักก่อนตอบ ให้เก็บ UI โต้ตอบได้: เริ่มการส่งออกในพื้นหลัง แสดงความคืบหน้า และหลีกเลี่ยงการคำนวณจำนวนซ้ำทุกครั้งที่ตัวกรองเปลี่ยน

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

เช็ครวดเร็วด้วยสัญชาตญาณ:

  • ถ้าคุณเลื่อนได้เรื่อย ๆ โดยไม่โหลดแสดงว่าคุณน่าจะเรนเดอร์มากเกินไป
  • ถ้าการพิมพ์ในการค้นหาหน่วง คำสั่งของคุณน่าจะสแกน
  • ถ้า API รายการคืน JSON ใหญ่ คุณดึงมากเกินไป
  • ถ้าการส่งออกทำให้หน้าแขวน งานกำลังเกิดบนที่ผิด
  • ถ้าการเรียงทุกอย่างช้า ดัชนีและตัวเลือกการเรียงต้องปรับ

เช็คลิสต์ด่วนและขั้นตอนต่อไป

ถือว่าประสิทธิภาพหน้ารายการเป็นฟีเจ็ตผลิตภัณฑ์ ไม่ใช่การปรับแต่งครั้งเดียว หน้ารายการจะเร็วเมื่อมันรู้สึกเร็วขณะที่คนจริงเลื่อน กรอง และเรียงในข้อมูลจริง

ใช้เช็คลิสต์นี้เพื่อตรวจสอบว่าคุณแก้ถูกจุด:

  • First paint เร็ว: หน้าโหลด payload เล็กและแสดงแถวแรกทันที
  • การเลื่อนลื่น: เปิด virtualization, CPU เบราว์เซอร์ไม่กระโดด, และความสูงแถวคาดเดาได้
  • ตัวกรองตอบสนอง: การพิมพ์หรือเลือกตัวกรองอัปเดตผลเร็ว และตัวกรองไม่รีเซ็ตเมื่อคุณแบ่งหน้า/รีเฟรช
  • การเรียงสมเหตุสมผล: อนุญาตการเรียงที่มีดัชนีหรือฟิลด์ที่คำนวณไว้ และรักษาลำดับเรียงให้คงที่ข้ามหน้า
  • คำขอถูกปรับรูปแบบ: API คืนเฉพาะคอลัมน์ที่คุณแสดง บวก ID เสถียรและ cursors ไม่ใช่อ็อบเจ็กต์เต็มเผื่อไว้

การตรวจสอบง่าย: เปิดหน้ารายการ เลื่อน 10 วินาที แล้วใช้ตัวกรองที่ใช้บ่อย (เช่น Status: Open) ถ้า UI ค้าง ปัญหามักเป็นการเรนเดอร์ (เรนเดอร์ DOM เกิน) หรือการแปลงหนักฝั่งไคลเอนต์ (เรียง กลุ่ม จัดรูปแบบ) ที่เกิดขึ้นทุกอัปเดต

ขั้นตอนต่อไปตามลำดับเพื่อไม่เด้งระหว่างการแก้ไข:

  1. วัดหนึ่งฟลูว์ของผู้ใช้ตั้งแต่ต้นจนจบ (โหลด เลื่อน กรอง เรียง) และจดเวลาที่ต้องการ
  2. เปิด virtualization และจำกัดการจัดรูปแบบแพงในเซลล์ (วันที่ สกุลเงิน avatar)
  3. ย้ายการกรองและการเรียงไปที่เซิร์ฟเวอร์ และจำกัดตัวเลือกในสิ่งที่สามารถเร็วได้
  4. ตรึงคำสั่งและ payload แล้ววัดใหม่

ถ้าคุณสร้างกับ Koder.ai (koder.ai) ให้เริ่มที่ Planning Mode: กำหนดคอลัมน์รายการ ตัวฟิลเตอร์ และรูปแบบการตอบกลับก่อน แล้วทำซ้ำโดยใช้ snapshots และ rollback เมื่อการทดลองทำให้หน้าช้าลง

คำถามที่พบบ่อย

ทำไมหน้ารายการของฉันถึงดูโอเคตอนแรกแต่เริ่มช้าพอข้อมูลถึง 100k แถว?

เปลี่ยนเป้าหมายจาก “โหลดทุกอย่าง” เป็น “แสดงแถวที่เป็นประโยชน์แรกให้เร็ว” ให้เพิ่มประสิทธิภาพที่เวลาเพื่อแถวแรกและการตอบสนองที่ลื่นไหลเมื่อกรอง เรียง และเลื่อน ถึงแม้จะไม่ได้โหลดข้อมูลทั้งชุดพร้อมกันก็ตาม

ตัวชี้วัดอะไรที่ควรติดตามสำหรับหน้ารายการที่ช้า?

วัดเวลาเพื่อแถวแรกหลังการโหลดหรือเปลี่ยนตัวกรอง, เวลาที่ตัวกรอง/การเรียงใช้ผลใหม่, ขนาด payload ของการตอบกลับ, คำสั่งฐานข้อมูลช้า (โดยเฉพาะ SELECT กว้าง ๆ และ COUNT(*)), และการกระโดดของ main-thread ในเบราว์เซอร์ ตัวเลขเหล่านี้จะสื่อถึงสิ่งที่ผู้ใช้รับรู้ว่าเป็น “หน่วง” ได้ตรงจุด

ฉันจะบอกได้อย่างรวดเร็วว่า slowdown มาจาก frontend หรือ backend อย่างไร?

จำกัด API ชั่วคราวให้คืนแค่ 20 แถวโดยใช้ตัวกรองและการเรียงแบบเดิม หากหน้ารวดเร็วขึ้น แสดงว่าคอขวดมาจากขนาดการตอบกลับหรือเวลา query ถ้ายังช้าอยู่ ปัญหามักอยู่ที่การเรนเดอร์ การจัดรูปแบบ หรืองานฝั่งไคลเอนต์ต่อแถว

การเปลี่ยนแปลง frontend แบบง่าย ๆ อะไรที่ช่วยให้ตารางใหญ่เร็วขึ้นได้?

อย่าเรนเดอร์หลายพันแถวใน DOM พร้อมกัน, ทำให้คอมโพเนนต์แถวเรียบง่าย และควรใช้ความสูงแถวคงที่ นอกจากนี้หลีกเลี่ยงการประมวลผลหนัก ๆ สำหรับแถวที่อยู่นอกหน้าจอ; คำนวณและแคชการจัดรูปแบบเมื่อแถวปรากฏหรือเมื่อผู้ใช้เปิดดู

เมื่อไหร่ควรใช้ virtualization และจะมีอะไรที่พังเมื่อเพิ่มมัน?

Virtualization จะติดตั้งเฉพาะแถวที่มองเห็นได้ (และสำรองเล็กน้อย) แล้วนำ DOM element เดิมมาใช้ซ้ำเมื่อเลื่อน มันคุ้มเมื่อต้องเลื่อนเยอะหรือแถวหนัก แต่จะทำงานได้ดีที่สุดถ้าความสูงแถวสม่ำเสมอและเลย์เอาต์คาดเดาได้

การแบ่งหน้าดีกว่าการเลื่อนไม่รู้จบสำหรับชุดข้อมูลใหญ่หรือเปล่า?

การแบ่งหน้าเป็นค่าพื้นฐานที่ปลอดภัยสำหรับงานแอดมินและงานภายใน เพราะช่วยให้ผู้ใช้คงทิศทางและจำกัดงานฝั่งเซิร์ฟเวอร์ การเลื่อนไม่รู้จบเหมาะสำหรับการท่องแบบสบาย ๆ แต่จะทำให้การนำทางและการใช้หน่วยความจำแย่ลงถ้าไม่มีการจัดการสถานะที่ชัดเจน

ฉันควรใช้การแบ่งหน้าแบบ offset หรือ cursor (keyset)?

การแบ่งหน้าแบบ offset ง่ายแต่จะช้าลงเมื่อไปลึกเพราะฐานข้อมูลต้องข้ามแถวจำนวนมาก page=10&size=50 แบบคลาสสิกเหมาะกับรายการเล็กหรือเมื่อจำเป็นต้องไปยังหน้าที่ระบุ คีย์เซ็ต (cursor) pagination มักเร็วกว่าเพราะไม่ต้องข้ามแถวจำนวนมาก แต่ไม่เหมาะกับการกระโดดไปหน้าที่ระบุเสมอไป

ทำอย่างไรให้การค้นหาและการกรองรู้สึกทันทีโดยไม่ทำให้เซิร์ฟเวอร์หนักเกินไป?

อย่าเรียกคำขอทุกครั้งที่พิมพ์ ใส่ debounce ให้ช่องค้นหา ยกเลิกคำขอที่ยังรันเมื่อมีคำขอใหม่ และตั้งค่าเริ่มต้นเป็นตัวกรองที่แคบ (เช่น รายการ 7 วันที่ผ่านมา หรือ My items) เพื่อให้คำขอแรกมีขนาดเล็กและมีประโยชน์

API ของหน้ารายการควรคืนอะไรเพื่อให้ยังเร็ว?

API ควรคืนเฉพาะฟิลด์ที่หน้ารายการต้องแสดง เช่น id, label, status, owner และ timestamps ข้อมูลยาว ๆ, JSON blobs และฟิลด์คำนวณสามารถโหลดเมื่อเปิดแถวเพื่อให้การแสดงผลครั้งแรกเบาและคาดเดาได้

การเปลี่ยนแปลงฐานข้อมูลอะไรที่มักช่วยกับรายการ 100k แถวได้มากที่สุด?

ทำให้ตัวกรองและการเรียงสอดคล้องกับพฤติกรรมจริงของผู้ใช้ แล้วสร้างดัชนี (index) ที่รองรับรูปแบบนั้น เช่น ดัชนีผสมที่รวม tenant_id, status, created_at การนับแบบแม่นยำอาจช้า ให้เก็บแคช หรือนำเสนอเป็นค่าประมาณเมื่อไม่จำเป็นต้องแม่นยำทันที

Related posts