เพิ่มประสิทธิภาพงานที่ใช้เวลานาน

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

เผยแพร่เมื่อวันที่ 30 กันยายน 2022 อัปเดตล่าสุดเมื่อวันที่ 19 ธันวาคม 2024

คำแนะนำทั่วไปในการทำให้แอป JavaScript ทำงานได้อย่างรวดเร็วมักจะสรุปได้ดังนี้

  • "อย่าบล็อกเทรดหลัก"
  • "แบ่งงานที่ใช้เวลานานออกเป็นส่วนๆ"

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

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

งานคืออะไร

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

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

งานที่เชื่อมโยงกับ JavaScript ส่งผลต่อประสิทธิภาพใน 2 ลักษณะดังนี้

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

สิ่งเหล่านี้ทั้งหมด ยกเว้น Web Worker และ API ที่คล้ายกัน จะเกิดขึ้นในเทรดหลัก

เทรดหลักคืออะไร

เทรดหลักคือที่ที่งานส่วนใหญ่ทำงานในเบราว์เซอร์ และเป็นที่ที่ JavaScript เกือบทั้งหมดที่คุณเขียนจะทำงาน

เทรดหลักจะประมวลผลงานได้ครั้งละ 1 งานเท่านั้น งานที่ใช้เวลานานกว่า 50 มิลลิวินาทีคืองานที่ใช้เวลานาน สำหรับงานที่ใช้เวลาเกิน 50 มิลลิวินาที เวลาทั้งหมดของงานลบด้วย 50 มิลลิวินาทีจะเรียกว่าระยะเวลาการบล็อกของงาน

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

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

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

งานที่ใช้เวลานานงานเดียวเทียบกับงานเดียวกันที่แบ่งออกเป็นงานที่สั้นลง งานที่ใช้เวลานานจะเป็นสี่เหลี่ยมผืนผ้าใหญ่ 1 รูป ส่วนงานที่แบ่งเป็นกลุ่มจะเป็นกล่องขนาดเล็ก 5 กล่องซึ่งมีความกว้างรวมกันเท่ากับงานที่ใช้เวลานาน
การแสดงข้อมูลผ่านภาพของงานที่ใช้เวลานาน 1 งานเทียบกับงานเดียวกันที่แบ่งออกเป็นงานที่สั้นลง 5 งาน

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

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

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

เมื่อทราบแล้วว่าเหตุใดการแบ่งงานจึงสำคัญ คุณก็ดูวิธีดำเนินการใน JavaScript ได้

กลยุทธ์การจัดการงาน

คำแนะนำที่พบบ่อยในสถาปัตยกรรมซอฟต์แวร์คือการแบ่งงานออกเป็นฟังก์ชันย่อยๆ ดังนี้

function saveSettings () {
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}

ในตัวอย่างนี้ มีฟังก์ชันชื่อ saveSettings() ที่เรียกใช้ฟังก์ชัน 5 รายการเพื่อตรวจสอบแบบฟอร์ม แสดงภาพหมุน ส่งข้อมูลไปยังแบ็กเอนด์ของแอปพลิเคชัน อัปเดตอินเทอร์เฟซผู้ใช้ และส่งข้อมูลวิเคราะห์

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

อย่างไรก็ตาม ปัญหาที่อาจเกิดขึ้นคือ JavaScript ไม่ได้เรียกใช้ฟังก์ชันแต่ละรายการเหล่านี้เป็นงานแยกกัน เนื่องจากฟังก์ชันเหล่านี้จะทำงานภายในฟังก์ชัน saveSettings() ซึ่งหมายความว่าทั้ง 5 ฟังก์ชันจะทำงานเป็นงานเดียว

ฟังก์ชัน saveSettings ตามที่แสดงในเครื่องมือวิเคราะห์ประสิทธิภาพของ Chrome แม้ว่าฟังก์ชันระดับบนสุดจะเรียกใช้ฟังก์ชันอื่นๆ อีก 5 ฟังก์ชัน แต่งานทั้งหมดจะเกิดขึ้นในงานที่ใช้เวลานานเพียงงานเดียว ซึ่งทำให้ผลลัพธ์ที่ผู้ใช้มองเห็นได้จากการเรียกใช้ฟังก์ชันจะไม่ปรากฏจนกว่างานทั้งหมดจะเสร็จสมบูรณ์
ฟังก์ชันเดียว saveSettings() ที่เรียกใช้ 5 ฟังก์ชัน ระบบจะเรียกใช้การทำงานเป็นส่วนหนึ่งของงานขนาดใหญ่ที่ใช้เวลานานงานเดียว ซึ่งจะบล็อกการตอบสนองแบบภาพจนกว่าฟังก์ชันทั้ง 5 จะทำงานเสร็จสมบูรณ์

ในกรณีที่ดีที่สุด แม้แต่ฟังก์ชันเดียวก็อาจเพิ่มเวลา 50 มิลลิวินาทีขึ้นไปให้กับความยาวทั้งหมดของงาน ในกรณีที่แย่ที่สุด งานเหล่านั้นอาจใช้เวลานานขึ้นมาก โดยเฉพาะในอุปกรณ์ที่มีทรัพยากรจำกัด

ในกรณีนี้ saveSettings() จะทริกเกอร์โดยการคลิกของผู้ใช้ และเนื่องจากเบราว์เซอร์ไม่สามารถแสดงการตอบสนองจนกว่าฟังก์ชันทั้งหมดจะทำงานเสร็จ ผลลัพธ์ของงานที่ใช้เวลานานนี้คือ UI ที่ช้าและไม่ตอบสนอง และจะวัดเป็น Interaction to Next Paint (INP) ที่ไม่ดี

เลื่อนการเรียกใช้โค้ดด้วยตนเอง

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

วิธีหนึ่งที่นักพัฒนาแอปใช้เพื่อแบ่งงานออกเป็นงานย่อยๆ คือ setTimeout() เทคนิคนี้จะส่งฟังก์ชันไปยัง setTimeout() ซึ่งจะเลื่อนการดำเนินการของ Callback ไปยังงานแยกต่างหาก แม้ว่าคุณจะระบุระยะหมดเวลาเป็น 0 ก็ตาม

function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Defer work that isn't user-visible to a separate task:
  setTimeout(() => {
    saveToDatabase();
    sendAnalytics();
  }, 0);
}

ซึ่งเรียกว่าการยอมให้ทำงาน และวิธีนี้เหมาะที่สุดสำหรับชุดฟังก์ชันที่ต้องทำงานตามลำดับ

อย่างไรก็ตาม โค้ดของคุณอาจไม่ได้จัดระเบียบในลักษณะนี้เสมอไป เช่น คุณอาจมีข้อมูลจำนวนมากที่ต้องประมวลผลในลูป และงานนั้นอาจใช้เวลานานมากหากมีการทำซ้ำหลายครั้ง

function processData () {
  for (const item of largeDataArray) {
    // Process the individual item here.
  }
}

การใช้ setTimeout() ที่นี่เป็นปัญหาเนื่องจากหลักสรีรศาสตร์ของนักพัฒนาซอฟต์แวร์ และหลังจาก setTimeout() ที่ซ้อนกัน 5 รอบ เบราว์เซอร์จะเริ่มกำหนดความล่าช้าขั้นต่ำ 5 มิลลิวินาทีสำหรับ setTimeout() เพิ่มเติมแต่ละรายการ

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

API การยอมให้ทำงานโดยเฉพาะ: scheduler.yield()

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: 142.
  • Safari: not supported.

Source

scheduler.yield() เป็น API ที่ออกแบบมาเพื่อส่งต่อการควบคุมไปยังเทรดหลักในเบราว์เซอร์โดยเฉพาะ

ซึ่งไม่ใช่ไวยากรณ์ระดับภาษาหรือโครงสร้างพิเศษ scheduler.yield() เป็นเพียงฟังก์ชันที่แสดงผล Promise ซึ่งจะได้รับการแก้ไขในงานในอนาคต โค้ดที่เชื่อมโยงให้ทำงานหลังจากที่ Promise ได้รับการแก้ไข (ไม่ว่าจะอยู่ในเชน .then() ที่ชัดเจนหรือหลังจาก await ในฟังก์ชันแบบไม่พร้อมกัน) จะทำงานในงานในอนาคตนั้น

ในทางปฏิบัติ: แทรก await scheduler.yield() แล้วฟังก์ชันจะหยุดการดำเนินการชั่วคราว ณ จุดนั้นและส่งต่อไปยังเทรดหลัก การดำเนินการส่วนที่เหลือของฟังก์ชัน ซึ่งเรียกว่าความต่อเนื่องของฟังก์ชัน จะได้รับการกำหนดเวลาให้ทำงานในงานวนรอบเหตุการณ์ใหม่ เมื่องานนั้นเริ่มขึ้น ระบบจะแก้ไข Promise ที่รออยู่ และฟังก์ชันจะดำเนินการต่อจากจุดที่หยุดไว้

async function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Yield to the main thread:
  await scheduler.yield()

  // Work that isn't user-visible, continued in a separate task:
  saveToDatabase();
  sendAnalytics();
}
ฟังก์ชัน saveSettings ตามที่แสดงในเครื่องมือวิเคราะห์ประสิทธิภาพของ Chrome ตอนนี้แบ่งออกเป็น 2 งาน งานแรกจะเรียกใช้ฟังก์ชัน 2 ฟังก์ชัน จากนั้นจะหยุดชั่วคราวเพื่อให้เลย์เอาต์และการวาดเกิดขึ้น และให้การตอบสนองที่มองเห็นได้แก่ผู้ใช้ ด้วยเหตุนี้ กิจกรรมการคลิกจึงเสร็จสิ้นเร็วขึ้นมากที่ 64 มิลลิวินาที งานที่ 2 จะเรียกใช้ฟังก์ชัน 3 รายการสุดท้าย
ตอนนี้การดำเนินการของฟังก์ชัน saveSettings() จะแบ่งออกเป็น 2 งาน ด้วยเหตุนี้ เลย์เอาต์และการแสดงผลจึงทำงานระหว่างงานได้ ซึ่งช่วยให้ผู้ใช้ได้รับการตอบสนองด้วยภาพที่รวดเร็วขึ้น โดยวัดจากการโต้ตอบกับตัวชี้ที่สั้นลงมากในตอนนี้

อย่างไรก็ตาม ประโยชน์ที่แท้จริงของ scheduler.yield() เหนือแนวทางการให้สิทธิ์อื่นๆ คือการให้ความสำคัญกับความต่อเนื่อง ซึ่งหมายความว่าหากคุณให้สิทธิ์ในระหว่างงาน ความต่อเนื่องของงานปัจจุบันจะทำงานก่อนที่จะเริ่มงานอื่นๆ ที่คล้ายกัน

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

แผนภาพ 3 รูปที่แสดงงานที่ไม่มีการ Yield, มีการ Yield, และมีการ Yield และการดำเนินการต่อ หากไม่มีการส่งต่อ จะมีงานที่ใช้เวลานาน เมื่อใช้การยอมให้ใช้ทรัพยากร งานจะมีจำนวนมากขึ้นและใช้เวลาสั้นลง แต่อาจถูกขัดจังหวะด้วยงานอื่นๆ ที่ไม่เกี่ยวข้อง เมื่อใช้การยอมให้ทำงานและการดำเนินการต่อ จะมีงานที่สั้นลงมากขึ้น แต่ระบบจะยังคงรักษาลำดับการดำเนินการไว้
เมื่อคุณใช้ scheduler.yield() การสนทนาต่อจะเริ่มจากจุดที่ค้างไว้ก่อนที่จะไปยังงานอื่นๆ

การรองรับข้ามเบราว์เซอร์

เบราว์เซอร์บางประเภทยังไม่รองรับ scheduler.yield() จึงจำเป็นต้องมีทางเลือกสำรอง

วิธีแก้ปัญหาหนึ่งคือการวาง scheduler-polyfill ลงในบิลด์ จากนั้นจะใช้ scheduler.yield() ได้โดยตรง โดย Polyfill จะจัดการการกลับไปใช้ฟังก์ชันการจัดกำหนดการงานอื่นๆ เพื่อให้ทำงานในลักษณะเดียวกันในเบราว์เซอร์ต่างๆ

หรือจะเขียนเวอร์ชันที่ซับซ้อนน้อยกว่าในโค้ดไม่กี่บรรทัดก็ได้ โดยใช้เพียง setTimeout ที่รวมอยู่ใน Promise เป็นทางเลือกสำรองหากไม่มี scheduler.yield()

function yieldToMain () {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }

  // Fall back to yielding with setTimeout.
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

แม้ว่าเบราว์เซอร์ที่ไม่มีการรองรับ scheduler.yield() จะไม่ได้รับการจัดลำดับความสำคัญให้ทำงานต่อ แต่เบราว์เซอร์จะยังคงทำงานต่อไปเพื่อตอบสนองต่อผู้ใช้

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

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

// Yield to the main thread if scheduler.yield() is available.
await globalThis.scheduler?.yield?.();

แบ่งงานที่ใช้เวลานานด้วย scheduler.yield()

ประโยชน์ของการใช้วิธีการเหล่านี้ในการใช้ scheduler.yield() คือคุณสามารถawaitในฟังก์ชัน async ใดก็ได้

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

async function runJobs(jobQueue) {
  for (const job of jobQueue) {
    // Run the job:
    job();

    // Yield to the main thread:
    await yieldToMain();
  }
}

ระบบจะจัดลำดับความสำคัญของการดำเนินการต่อของ runJobs() แต่ยังคงอนุญาตให้งานที่มีลำดับความสำคัญสูงกว่า เช่น การตอบสนองต่ออินพุตของผู้ใช้ด้วยภาพ ทำงานได้ โดยไม่ต้องรอให้รายการงานที่อาจใช้เวลานานเสร็จสิ้น

อย่างไรก็ตาม วิธีนี้ไม่ใช่การใช้การให้สิทธิ์อย่างมีประสิทธิภาพ scheduler.yield() เป็นวิธีที่รวดเร็วและมีประสิทธิภาพ แต่ก็มีค่าใช้จ่ายเพิ่มเติมบางอย่าง หากงานบางอย่างใน jobQueue สั้นมาก ค่าใช้จ่ายในการดำเนินการอาจเพิ่มขึ้นอย่างรวดเร็วเป็นเวลาที่ใช้ในการยอมให้ใช้ทรัพยากรและกลับมาทำงานต่อมากกว่าการทำงานจริง

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

async function runJobs(jobQueue, deadline=50) {
  let lastYield = performance.now();

  for (const job of jobQueue) {
    // Run the job:
    job();

    // If it's been longer than the deadline, yield to the main thread:
    if (performance.now() - lastYield > deadline) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}

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

ชุดฟังก์ชันงานที่แสดงในแผงประสิทธิภาพของเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome โดยมีการแบ่งการดำเนินการออกเป็นหลายงาน
งานที่จัดกลุ่มเป็นหลายๆ งาน

ไม่ใช้ isInputPending()

Browser Support

  • Chrome: 87.
  • Edge: 87.
  • Firefox: not supported.
  • Safari: not supported.

Source

isInputPending() API มีวิธีตรวจสอบว่าผู้ใช้พยายามโต้ตอบกับหน้าเว็บหรือไม่ และจะยอมรับก็ต่อเมื่อมีอินพุตที่รอดำเนินการอยู่

ซึ่งจะช่วยให้ JavaScript ทำงานต่อไปได้หากไม่มีอินพุตที่รอดำเนินการ แทนที่จะหยุดทำงานและไปต่อท้ายคิวงาน ซึ่งอาจส่งผลให้ประสิทธิภาพดีขึ้นอย่างน่าประทับใจ ดังที่อธิบายไว้ใน Intent to Ship สำหรับเว็บไซต์ที่อาจไม่ส่งกลับไปยังเทรดหลัก

อย่างไรก็ตาม นับตั้งแต่เปิดตัว API ดังกล่าว เรามีความเข้าใจเกี่ยวกับการยอมให้ทำงานมากขึ้น โดยเฉพาะอย่างยิ่งเมื่อมีการเปิดตัว INP เราไม่แนะนำให้ใช้ API นี้อีกต่อไป และแนะนำให้ใช้ Yield ไม่ว่าอินพุตจะรอดำเนินการหรือไม่ก็ตามด้วยเหตุผลหลายประการดังนี้

  • isInputPending() อาจแสดงผล false อย่างไม่ถูกต้องแม้ว่าผู้ใช้จะโต้ตอบในบางสถานการณ์ก็ตาม
  • อินพุตไม่ใช่กรณีเดียวที่งานควรยอมให้ดำเนินการ ภาพเคลื่อนไหวและการอัปเดตอินเทอร์เฟซผู้ใช้ตามปกติอื่นๆ อาจมีความสำคัญไม่แพ้กันในการสร้างหน้าเว็บที่ตอบสนองได้ดี
  • ตั้งแต่นั้นมา มีการเปิดตัว API การสร้างผลลัพธ์ที่ครอบคลุมมากขึ้นซึ่งช่วยแก้ปัญหาเกี่ยวกับผลลัพธ์ เช่น scheduler.postTask() และ scheduler.yield()

บทสรุป

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

  • ส่งต่อให้เทรดหลักสำหรับงานสำคัญที่แสดงต่อผู้ใช้
  • ใช้ scheduler.yield() (พร้อมการสำรองข้อมูลข้ามเบราว์เซอร์) เพื่อให้ได้ผลลัพธ์ตามหลักสรีรศาสตร์และได้รับการดำเนินการต่อตามลำดับความสำคัญ
  • สุดท้าย ให้ทำงานในฟังก์ชันให้น้อยที่สุด

ดูข้อมูลเพิ่มเติมเกี่ยวกับ scheduler.yield(), scheduler.postTask() ที่เกี่ยวข้องกับการจัดตารางเวลางานอย่างชัดเจน และการจัดลำดับความสำคัญของงานได้ที่เอกสารประกอบเกี่ยวกับ Prioritized Task Scheduling API

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

ขอขอบคุณ Philip Walton เป็นพิเศษสำหรับการตรวจสอบทางเทคนิคของคู่มือนี้