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

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

Browser Support

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

Source

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

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

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

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

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

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

การดําเนินการต่อที่มีลําดับความสําคัญหลังจากให้สิทธิ์

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

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

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

ตัวอย่างคือ 2 ฟังก์ชันที่จัดคิวให้ทำงานใน 2 งานที่แตกต่างกันโดยใช้ setTimeout

setTimeout(myJob);
setTimeout(someoneElsesJob);

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

การทำงานดังกล่าวอาจมีลักษณะดังนี้ในเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์

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

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" รายการที่ 2 คืองานระยะยาวที่เรียกใช้ "someoneElsesJob" และสุดท้ายคืองานลำดับที่ 3 ที่เรียกใช้ "myJobPart2"

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

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

ป้อน scheduler.yield() ซึ่งจะจัดคิวการดําเนินการต่อของฟังก์ชันที่เรียกใช้ไว้ในคิวที่มีลําดับความสําคัญสูงกว่าการเริ่มงานอื่นๆ ที่คล้ายกันเล็กน้อย หากเปลี่ยน myJob ให้ใช้ 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 ทั้งสองเป็นงานที่ใช้เวลานาน โดยฟังก์ชัน "myJob" จะใช้เวลาดำเนินการทั้งหมดของงานแรก และ "someoneElsesJob" จะใช้เวลาดำเนินการทั้งหมดของงานที่สอง

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

การสืบทอดลําดับความสําคัญ

scheduler.yield() เป็นส่วนหนึ่งของ Prioritized Task Scheduling API ที่ใหญ่กว่า จึงทำงานร่วมกับลำดับความสำคัญแบบชัดเจนที่มีอยู่ใน scheduler.postTask() ได้เป็นอย่างดี หากไม่ได้ตั้งค่าลําดับความสําคัญไว้อย่างชัดเจน 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 คุณจึงต้องใช้การตรวจหาฟีเจอร์และเปลี่ยนไปใช้วิธีรองในการให้ผลลัพธ์สำหรับเบราว์เซอร์อื่นๆ

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

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

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

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

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

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