มีคนแจ้งให้ "อย่าบล็อกชุดข้อความหลัก" และ "แบ่งงานที่ใช้เวลานานออกไป" แต่การทำแบบนั้นหมายความว่าอย่างไร
คำแนะนำทั่วไปในการทำให้แอป JavaScript ทำงานได้เร็วมักจะเป็นไปตามคำแนะนำต่อไปนี้
- "อย่าบล็อกชุดข้อความหลัก"
- "แบ่งงานที่ใช้เวลานานออกไป"
นี่เป็นคำแนะนำที่ดี แต่จะเกี่ยวข้องกับงานใด การใช้ JavaScript น้อยลงเป็นสิ่งที่ดี แต่การทำเช่นนี้จะทำให้อินเทอร์เฟซผู้ใช้ตอบสนองมากขึ้นโดยอัตโนมัติหรือไม่ อาจจะ แต่อาจไม่ก็ได้นะ
หากต้องการทำความเข้าใจวิธีเพิ่มประสิทธิภาพงานใน JavaScript ก่อนอื่นคุณต้องทราบว่างานคืออะไรและเบราว์เซอร์จัดการกับงานเหล่านั้นอย่างไร
งานคืออะไร
งานคืองานส่วนที่ไม่ต่อเนื่องซึ่งเบราว์เซอร์ทำ ซึ่งรวมไปถึงการแสดงผล, การแยกวิเคราะห์ HTML และ CSS, การเรียกใช้ JavaScript และงานประเภทอื่นๆ ที่คุณไม่มีสิทธิ์ควบคุมโดยตรง จากทั้งหมดนี้ JavaScript ที่คุณเขียนอาจเป็นแหล่งที่มางานที่ใหญ่ที่สุด
งานที่เชื่อมโยงกับ JavaScript ส่งผลต่อประสิทธิภาพใน 2 วิธีต่อไปนี้
- เมื่อดาวน์โหลดไฟล์ JavaScript ระหว่างเริ่มต้นใช้งาน เบราว์เซอร์จะจัดคิวงานเพื่อแยกวิเคราะห์และคอมไพล์ JavaScript ดังกล่าวเพื่อให้เรียกใช้ภายหลังได้
- ในบางครั้งในช่วงชีวิตหน้าเว็บ งานจะจัดอยู่ในคิวเมื่อ JavaScript ทำงาน เช่น การกระตุ้นการโต้ตอบผ่านตัวแฮนเดิลเหตุการณ์ ภาพเคลื่อนไหวที่ขับเคลื่อนด้วย JavaScript และกิจกรรมในเบื้องหลังอย่างการรวบรวมข้อมูลข้อมูลวิเคราะห์
ทั้งหมดนี้เกิดขึ้นในเทรดหลัก ยกเว้นโปรแกรมทำงานบนเว็บและ API ที่คล้ายกัน
เทรดหลักคืออะไร
เทรดหลักคือที่ที่งานส่วนใหญ่ทำงานในเบราว์เซอร์และเป็นที่ที่เรียกใช้ JavaScript เกือบทั้งหมดที่คุณเขียน
โดยเทรดหลักจะประมวลผลงานได้ครั้งละ 1 รายการเท่านั้น งานที่ใช้เวลานานกว่า 50 มิลลิวินาทีถือเป็นงานที่ใช้เวลานาน สำหรับงานที่มีความยาวเกิน 50 มิลลิวินาที เวลารวมของงานลบด้วย 50 มิลลิวินาทีจะเรียกว่าระยะเวลาการบล็อกของงาน
เบราว์เซอร์จะบล็อกการโต้ตอบไม่ให้เกิดขึ้นขณะที่มีการทำงานที่มีความยาวเท่าใดก็ได้ แต่ผู้ใช้จะไม่สามารถรับรู้ได้ ตราบใดที่ไม่มีการทำงานนานเกินไป แต่เมื่อผู้ใช้พยายามโต้ตอบกับหน้าเว็บเมื่อมีงานยาวๆ หลายงาน อินเทอร์เฟซผู้ใช้จะรู้สึกว่าไม่ตอบสนอง และอาจทำงานเสียหายได้หากเทรดหลักถูกบล็อกเป็นเวลานาน
เพื่อป้องกันไม่ให้ชุดข้อความหลักถูกบล็อกนานเกินไป ให้แบ่งงานที่ใช้เวลานานเป็นงานย่อยๆ หลายรายการ
ซึ่งเป็นสิ่งที่สำคัญเนื่องจากเมื่อแบ่งงานออกเป็นย่อยๆ เบราว์เซอร์จะตอบสนองต่องานที่มีความสำคัญสูงได้เร็วขึ้น ซึ่งรวมถึงการโต้ตอบของผู้ใช้ด้วย หลังจากนั้น งานที่เหลือจะดำเนินการต่อจนเสร็จ เพื่อให้แน่ใจว่างานที่คุณจัดลำดับไว้ในคิวไว้จะเสร็จลุล่วง
ที่ด้านบนของตัวเลขก่อนหน้า ตัวแฮนเดิลเหตุการณ์ที่อยู่ในคิวโดยการโต้ตอบของผู้ใช้ต้องรองานเดียวที่ใช้เวลานานก่อนที่จะเริ่มต้น ซึ่งทำให้การโต้ตอบล่าช้าลง ในกรณีนี้ ผู้ใช้อาจสังเกตเห็นความล่าช้า ที่ด้านล่าง เครื่องจัดการเหตุการณ์จะเริ่มทำงานได้เร็วขึ้น และการโต้ตอบอาจรู้สึกทันที
ตอนนี้คุณรู้ถึงความสำคัญของการแบ่งงานแล้ว ต่อไปก็ดูวิธีทำใน JavaScript ได้เลย
กลยุทธ์การจัดการงาน
คำแนะนำทั่วไปอย่างหนึ่งในสถาปัตยกรรมซอฟต์แวร์คือการแบ่งงานออกเป็นฟังก์ชันย่อยๆ ดังนี้
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
ในตัวอย่างนี้ มีฟังก์ชันชื่อ saveSettings()
ซึ่งเรียกใช้ฟังก์ชัน 5 รายการเพื่อตรวจสอบความถูกต้องของฟอร์ม แสดงไอคอนหมุน ส่งข้อมูลไปยังแบ็กเอนด์ของแอปพลิเคชัน อัปเดตอินเทอร์เฟซผู้ใช้ และส่งข้อมูลวิเคราะห์
ตามหลักการแล้ว saveSettings()
มีสถาปัตยกรรมที่ดี หากต้องการแก้ไขข้อบกพร่องของฟังก์ชันใดฟังก์ชันหนึ่งเหล่านี้ คุณจะข้ามแผนผังโปรเจ็กต์เพื่อดูหน้าที่ของแต่ละฟังก์ชันได้ การแยกส่วนงานลักษณะนี้จะทำให้โปรเจ็กต์ใช้งานและบำรุงรักษาได้ง่ายขึ้น
อย่างไรก็ตาม ปัญหาที่อาจเกิดขึ้นคือ JavaScript ไม่เรียกใช้แต่ละฟังก์ชันเหล่านี้เป็นงานแยกจากกัน เนื่องจากมีการเรียกใช้งานภายในฟังก์ชัน saveSettings()
ซึ่งหมายความว่าฟังก์ชันทั้ง 5 รายการจะทำงานเดียวกัน
ในสถานการณ์ที่ดีที่สุด แม้แต่ฟังก์ชันหนึ่งก็สามารถมีส่วนร่วม 50 มิลลิวินาทีหรือมากกว่านั้นให้กับความยาวรวมของงานได้ ในกรณีที่แย่ที่สุด งานเหล่านั้นจำนวนมากขึ้นก็จะทำงานได้นานขึ้นมาก โดยเฉพาะในอุปกรณ์ที่มีข้อจำกัดด้านทรัพยากร
เลื่อนการเรียกใช้โค้ดด้วยตนเอง
วิธีหนึ่งที่นักพัฒนาแอปใช้เพื่อแบ่งงานออกเป็นงานย่อยๆ นั้นเกี่ยวข้องกับ 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()
ก็ไม่ใช่เครื่องมือที่เหมาะสำหรับงาน อย่างน้อยก็ไม่ใช้วิธีนี้อีก
ใช้ async
/await
เพื่อสร้างจุดผลตอบแทน
เพื่อให้แน่ใจว่างานสำคัญที่แสดงต่อผู้ใช้จะเกิดขึ้นก่อนงานที่มีลำดับความสำคัญต่ำกว่า คุณสามารถตอบกลับเทรดหลักด้วยการขัดจังหวะคิวงานเป็นเวลาสั้นๆ เพื่อให้สิทธิ์ ในการทำงานที่สำคัญมากขึ้น
ตามที่อธิบายไว้ก่อนหน้านี้ คุณใช้ setTimeout
เพื่อแสดงผลไปยังเทรดหลักได้ แต่เพื่อความสะดวกและการอ่านจะง่ายขึ้น คุณสามารถโทรหา setTimeout
ภายใน Promise
และส่งเมธอด resolve
เป็น Callback ได้
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
ประโยชน์ของฟังก์ชัน yieldToMain()
คือคุณสามารถawait
ในฟังก์ชัน async
ใดก็ได้ จากตัวอย่างก่อนหน้านี้ คุณสามารถสร้างอาร์เรย์ของฟังก์ชันที่จะเรียกใช้ และแสดงผลไปยังเทรดหลักหลังจากที่เรียกใช้แต่ละครั้งได้
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread:
await yieldToMain();
}
}
ผลที่ได้คือตอนนี้งานที่เคยเป็นโมโนลิธถูกแยกออกเป็นงานย่อยๆ
API เครื่องจัดตารางเวลาโดยเฉพาะ
setTimeout
เป็นวิธีที่มีประสิทธิภาพในการแบ่งงาน แต่ก็ก็มีข้อเสีย คือเมื่อคุณให้เทรดหลักโดยการเลื่อนโค้ดเพื่อทำงานในงานถัดไป งานนั้นจะเพิ่มลงในท้ายของคิว
หากคุณควบคุมโค้ดทั้งหมดในหน้าเว็บ คุณก็สามารถสร้างเครื่องจัดตารางเวลาของคุณเองได้ด้วยความสามารถในการจัดลำดับความสำคัญของงาน แต่สคริปต์ของบุคคลที่สามจะไม่ใช้เครื่องจัดตารางเวลาของคุณ คุณจึงไม่สามารถจัดลำดับความสำคัญในการทำงานในสภาพแวดล้อมแบบนี้ได้ คุณสามารถแบ่งย่อยหรือส่งต่อการโต้ตอบของผู้ใช้อย่างชัดแจ้งก็ได้
API เครื่องจัดตารางเวลามีฟังก์ชัน postTask()
ซึ่งช่วยให้กำหนดเวลางานได้ละเอียดยิ่งขึ้นและเป็นวิธีหนึ่งในการช่วยเบราว์เซอร์จัดลำดับความสำคัญของงานเพื่อให้งานที่มีลำดับความสำคัญต่ำส่งกลับไปยังเทรดหลัก postTask()
ใช้การสัญญาและยอมรับการตั้งค่า 1 ใน 3 รายการของ priority
ดังนี้
'background'
สำหรับงานที่มีลำดับความสำคัญต่ำสุด'user-visible'
สำหรับงานที่มีลำดับความสำคัญปานกลาง ตัวเลือกนี้จะเป็นค่าเริ่มต้นหากไม่ได้ตั้งค่าpriority
ไว้'user-blocking'
สำหรับงานสำคัญที่จำเป็นต้องเรียกใช้ในลำดับความสำคัญสูง
ลองดูโค้ดต่อไปนี้เป็นตัวอย่าง ซึ่ง API ของ postTask()
ใช้เพื่อเรียกใช้งาน 3 รายการที่มีลำดับความสำคัญสูงสุดเท่าที่จะเป็นไปได้ และงานที่มีลำดับความสำคัญต่ำสุดอีก 2 งาน
function saveSettings () {
// Validate the form at high priority
scheduler.postTask(validateForm, {priority: 'user-blocking'});
// Show the spinner at high priority:
scheduler.postTask(showSpinner, {priority: 'user-blocking'});
// Update the database in the background:
scheduler.postTask(saveToDatabase, {priority: 'background'});
// Update the user interface at high priority:
scheduler.postTask(updateUI, {priority: 'user-blocking'});
// Send analytics data in the background:
scheduler.postTask(sendAnalytics, {priority: 'background'});
};
ในส่วนนี้ เราจะจัดลำดับความสำคัญของงานให้อยู่ในรูปแบบที่งานต่างๆ ที่เบราว์เซอร์จัดลำดับความสำคัญ เช่น การโต้ตอบของผู้ใช้ สามารถทำงานระหว่างต่างๆ ได้ตามต้องการ
นี่คือตัวอย่างง่ายๆ เกี่ยวกับวิธีใช้ postTask()
คุณอาจสร้างอินสแตนซ์ของออบเจ็กต์ TaskController
ต่างๆ ที่แชร์ลำดับความสำคัญระหว่างงานได้ รวมถึงความสามารถในการเปลี่ยนลำดับความสำคัญสำหรับอินสแตนซ์ TaskController
ที่แตกต่างกันตามความจำเป็น
ผลตอบแทนในตัวที่มาพร้อมกับความต่อเนื่องโดยใช้ scheduler.yield()
API ที่กำลังจะเปิดตัว
สิ่งหนึ่งที่เสนอให้เพิ่ม API เครื่องจัดตารางเวลาคือ scheduler.yield()
ซึ่งเป็น API ที่ออกแบบมาเพื่อผลตอบแทนต่อเทรดหลักในเบราว์เซอร์โดยเฉพาะ การใช้งานจะคล้ายกับฟังก์ชัน yieldToMain()
ที่แสดงก่อนหน้านี้ในคู่มือนี้
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread with the scheduler
// API's own yielding mechanism:
await scheduler.yield();
}
}
คุ้นเคยกับโค้ดนี้เป็นส่วนใหญ่ แต่จะใช้ yieldToMain()
แทน
await scheduler.yield()
ประโยชน์ของ scheduler.yield()
คือความต่อเนื่อง ซึ่งหมายความว่าหากคุณให้ผลตอบแทนในระหว่างชุดงาน งานอื่นๆ ที่กำหนดเวลาไว้จะดำเนินไปตามลำดับเดิมหลังจากจุดผลตอบแทน วิธีนี้ช่วยป้องกันไม่ให้โค้ดจากสคริปต์ของบุคคลที่สามขัดจังหวะลำดับการเรียกใช้โค้ด
การใช้ scheduler.postTask()
กับ priority: 'user-blocking'
ก็มีแนวโน้มสูงที่จะดำเนินการอย่างต่อเนื่องเนื่องจากลำดับความสำคัญ user-blocking
สูง จึงอาจใช้แนวทางนี้เป็นทางเลือกได้ในระหว่างนี้
การใช้ setTimeout()
(หรือ scheduler.postTask()
ที่มี priority: 'user-visibile'
หรือไม่ระบุ priority
อย่างชัดเจน) จะกำหนดเวลางานไว้ที่ด้านหลังของคิวเพื่อให้งานอื่นๆ ที่รอดำเนินการทำงานก่อนการทำงานต่อเนื่องได้
ไม่ใช้ isInputPending()
การรองรับเบราว์เซอร์
- 87
- 87
- x
- x
isInputPending()
API ช่วยในการตรวจสอบว่าผู้ใช้ได้พยายามโต้ตอบกับหน้าเว็บหรือไม่ และจะตอบกลับเมื่อมีอินพุตรอดำเนินการเท่านั้น
ซึ่งจะทำให้ JavaScript ดำเนินต่อไปได้หากไม่มีอินพุตที่รอดำเนินการ แทนที่จะแสดงผลที่ท้ายคิวงาน ซึ่งอาจส่งผลให้มีการปรับปรุงประสิทธิภาพอย่างน่าประทับใจ ตามที่อธิบายไว้ในความตั้งใจที่จะจัดส่งสําหรับเว็บไซต์ที่อาจไม่ตอบกลับชุดข้อความหลัก
อย่างไรก็ตาม ตั้งแต่เปิดตัว API ดังกล่าว ความเข้าใจของเราเกี่ยวกับผลตอบแทนก็เพิ่มขึ้น โดยเฉพาะอย่างยิ่งเมื่อมีการเริ่มใช้ INP เราไม่แนะนำให้ใช้ API นี้อีกต่อไป และแนะนำให้ใช้โดยไม่คำนึงว่าข้อมูลจะอยู่ในสถานะรอดำเนินการหรือไม่ด้วยเหตุผลหลายประการดังนี้
isInputPending()
อาจแสดงผลfalse
อย่างไม่ถูกต้องแม้ว่าผู้ใช้จะโต้ตอบในบางสถานการณ์- อินพุตไม่ได้เป็นเพียงกรณีเดียวที่งานควรแสดงผล ภาพเคลื่อนไหวและการอัปเดตอินเทอร์เฟซผู้ใช้ตามปกติอื่นๆ ก็มีความสำคัญไม่แพ้กันเพื่อมอบหน้าเว็บที่ปรับเปลี่ยนตามอุปกรณ์
- ตั้งแต่นั้นเป็นต้นมา เราได้เปิดตัว API ผลตอบแทนที่ครอบคลุมมากขึ้น ซึ่งช่วยแก้ไขปัญหาเกี่ยวกับผลตอบแทน เช่น
scheduler.postTask()
และscheduler.yield()
บทสรุป
การจัดการงานอาจเป็นเรื่องท้าทาย แต่การทำเช่นนั้นก็ช่วยให้หน้าเว็บตอบสนองต่อการโต้ตอบของผู้ใช้ได้เร็วขึ้น การจัดการและจัดลำดับความสำคัญของงานนั้นไม่มีคำแนะนำเพียงวิธีเดียว แต่จะมีเทคนิคต่างๆ มากมาย กล่าวย้ำอีกครั้งคือสิ่งสำคัญที่คุณควรคำนึงถึงเมื่อจัดการงานมีดังนี้
- ให้เทรดหลักสำหรับงานสำคัญที่แสดงต่อผู้ใช้
- จัดลำดับความสำคัญของงานด้วย
postTask()
- พิจารณาทดลองใช้
scheduler.yield()
- สุดท้าย ให้ทำงานในส่วนต่างๆ ให้น้อยที่สุดเท่าที่จะทำได้
ด้วยเครื่องมือเหล่านี้อย่างน้อย 1 อย่าง คุณจะสามารถจัดโครงสร้างงานในแอปพลิเคชันของคุณ เพื่อให้งานนั้นให้ความสำคัญกับความต้องการของผู้ใช้ ขณะเดียวกันก็ดูแลให้งานที่สำคัญน้อยกว่ายังคงเสร็จสมบูรณ์ วิธีนี้จะช่วยให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดีขึ้น ตอบสนองได้ดีขึ้นและสนุกยิ่งขึ้น
ขอขอบคุณ Philip Walton ที่ช่วยตรวจสอบคู่มือนี้ทางเทคนิค
ภาพขนาดย่อที่มาจาก Unwash โดยได้รับความเอื้อเฟื้อจาก Amirali Mirhashemian