ใช้ scheduler.yield() เพื่อแบ่งงานที่มีระยะเวลานาน

Brendan Kenny
Brendan Kenny

เผยแพร่: 6 มีนาคม 2025

Browser Support

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

Source

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

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

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

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // Yield to the main thread.
  slowerComputation();
}

ประโยชน์ที่เฉพาะเจาะจงของ scheduler.yield คือการกำหนดเวลาให้ดำเนินการต่อหลังจากที่ได้ผลลัพธ์ก่อนที่จะเรียกใช้ทาคล้ายกันอื่นๆ ที่หน้าเว็บจัดคิวไว้ โดยจะให้ความสำคัญกับการทำงานที่ค้างอยู่มากกว่าการเริ่มงานใหม่

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

การดำเนินการต่อที่จัดลำดับความสำคัญหลังจากส่งผล

scheduler.yield เป็นส่วนหนึ่งของ Prioritized Task Scheduling API ในฐานะนักพัฒนาเว็บ โดยปกติแล้วเราจะไม่พูดถึงลำดับที่ Event Loop เรียกใช้ Task ในแง่ของลำดับความสำคัญที่ชัดเจน แต่ลำดับความสำคัญที่เกี่ยวข้องจะมีอยู่เสมอ เช่น requestIdleCallbackCallback ที่ทำงานหลังจาก Callback ที่อยู่ในคิวsetTimeout หรือ Listener ของเหตุการณ์อินพุตที่ทริกเกอร์มักจะทำงานก่อน Task ที่อยู่ในคิวด้วย setTimeout(callback, 0)

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

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

ตัวอย่างเช่น ฟังก์ชัน 2 รายการที่จัดคิวไว้เพื่อเรียกใช้ในงานต่างๆ โดยใช้ setTimeout

setTimeout(myJob);
setTimeout(someoneElsesJob);

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

ซึ่งใน DevTools จะมีลักษณะดังนี้

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

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

function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with setTimeout() to break up long task, then run part2.
  setTimeout(myJobPart2, 0);
}

เนื่องจากมีการกำหนดเวลาให้ myJobPart2 ทำงานร่วมกับ setTimeout ภายใน myJob แต่การกำหนดเวลานั้นจะทำงานหลังจากที่มีการกำหนดเวลา someoneElsesJob ไปแล้ว การดำเนินการจึงเป็นดังนี้

งาน 3 อย่างที่แสดงในแผงประสิทธิภาพของเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome งานแรกคือการเรียกใช้ฟังก์ชัน "myJobPart1" งานที่สองคืองานที่ใช้เวลานานซึ่งเรียกใช้ "someoneElsesJob" และงานที่สามคือการเรียกใช้ "myJobPart2"

เราได้แบ่งงานด้วย setTimeout เพื่อให้เบราว์เซอร์ตอบสนองได้ในระหว่างmyJob แต่ตอนนี้ส่วนที่ 2 ของ myJob จะทำงานหลังจากที่ someoneElsesJob เสร็จสิ้นแล้วเท่านั้น

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

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

async function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with scheduler.yield() to break up long task, then run part2.
  await scheduler.yield();
  myJobPart2();
}

ตอนนี้การดำเนินการจะมีลักษณะดังนี้

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

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

การรับช่วงลำดับความสำคัญ

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

อย่างไรก็ตาม หากตั้งค่าลำดับความสำคัญ เช่น ใช้'background'ลำดับความสำคัญต่ำ

async function lowPriorityJob() {
  part1();
  await scheduler.yield();
  part2();
}

scheduler.postTask(lowPriorityJob, {priority: 'background'});

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

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

วิธีใช้ API

ปัจจุบัน scheduler.yield() ใช้ได้เฉพาะในเบราว์เซอร์แบบ Chromium ดังนั้นหากต้องการใช้ คุณจะต้องตรวจหาฟีเจอร์และย้อนกลับไปใช้วิธีที่ 2 ในการส่งผลลัพธ์สำหรับเบราว์เซอร์อื่นๆ

scheduler-polyfill เป็น Polyfill ขนาดเล็กสำหรับ scheduler.postTask และ scheduler.yield ซึ่งใช้ชุดเมธอดภายในเพื่อจำลองความสามารถส่วนใหญ่ของ Scheduling API ในเบราว์เซอร์อื่นๆ (แม้ว่าจะไม่รองรับการรับค่าลำดับความสำคัญของ scheduler.yield())

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

นอกจากนี้ยังใช้wicg-task-schedulingประเภทเพื่อรับการตรวจสอบประเภทและการรองรับ IDE ได้ด้วย หากคุณตรวจหาฟีเจอร์ scheduler.yield() และเพิ่มการสำรองด้วยตนเอง

ดูข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับ API และวิธีที่ API โต้ตอบกับลำดับความสำคัญของงานและ scheduler.postTask() ได้ในเอกสาร scheduler.yield() และการจัดกำหนดเวลางานที่มีลำดับความสำคัญใน MDN

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