คำแนะนำทั่วไปเกี่ยวกับการทำให้แอป JavaScript ทำงานเร็วขึ้นมักประกอบด้วย "อย่าบล็อกเทรดหลัก" และ "แบ่งงานที่ใช้เวลานาน" หน้านี้จะอธิบายถึงความหมายของคำแนะนำและเหตุผลที่การเพิ่มประสิทธิภาพงานใน JavaScript จึงมีความสำคัญ
งานคืออะไร
งานคืองานใดก็ตามที่เบราว์เซอร์ทำ ซึ่งรวมถึงการแสดงผล การแยกวิเคราะห์ HTML และ CSS การเรียกใช้โค้ด JavaScript ที่คุณเขียน และอื่นๆ ที่คุณอาจควบคุมไม่ได้โดยตรง JavaScript ของหน้าเว็บเป็น งานหลักของเบราว์เซอร์
งานจะส่งผลต่อประสิทธิภาพในหลายๆ ด้าน เช่น เมื่อเบราว์เซอร์ดาวน์โหลดไฟล์ JavaScript ในระหว่างการเริ่มต้น ระบบจะจัดคิวงานให้แยกวิเคราะห์และคอมไพล์ JavaScript เพื่อให้สามารถเรียกใช้ได้ ต่อมาในวงจรของหน้าเว็บ งานอื่นๆ จะเริ่มเมื่อ JavaScript ทำงาน เช่น การขับเคลื่อนการโต้ตอบผ่านเครื่องจัดการเหตุการณ์ ภาพเคลื่อนไหวที่ขับเคลื่อนด้วย JavaScript และกิจกรรมในเบื้องหลัง เช่น การรวบรวมข้อมูลวิเคราะห์ ทั้งหมดนี้เกิดขึ้นในเทรดหลัก ยกเว้น Web Work และ API ที่คล้ายกัน
เทรดหลักคืออะไร
เทรดหลักคือที่ที่งานส่วนใหญ่ทำงานในเบราว์เซอร์ และเป็นที่ที่มีการเรียกใช้ JavaScript เกือบทั้งหมดที่คุณเขียน
เทรดหลักประมวลผลงานได้ครั้งละ 1 งานเท่านั้น งานที่ใช้เวลานานกว่า 50 มิลลิวินาทีจะนับเป็นงานที่ใช้เวลานาน หากผู้ใช้พยายามโต้ตอบกับหน้าเว็บระหว่างงานที่ใช้เวลานานหรืออัปเดตการแสดงผล เบราว์เซอร์จะต้องรอเพื่อจัดการการโต้ตอบดังกล่าว ซึ่งจะทำให้เวลาในการตอบสนอง
เพื่อป้องกันปัญหานี้ ให้แบ่งงานที่ใช้เวลานานแต่ละงานออกเป็นงานย่อยๆ ซึ่งแต่ละงานใช้เวลาในการเรียกใช้น้อยลง ซึ่งเรียกว่าการแบ่งงานที่ใช้เวลานาน
การแบ่งงานให้เบราว์เซอร์มีโอกาสมากขึ้นในการตอบสนองต่องานที่มีความสำคัญสูงกว่า ซึ่งรวมถึงการโต้ตอบของผู้ใช้ ในระหว่างงานอื่นๆ วิธีนี้จะช่วยให้การโต้ตอบเกิดขึ้นได้เร็วขึ้นมาก โดยที่ผู้ใช้อาจสังเกตเห็นความล่าช้าขณะที่เบราว์เซอร์รอการทำงานเป็นเวลานานจึงจะเสร็จสิ้น
กลยุทธ์การจัดการงาน
JavaScript จะถือว่าแต่ละฟังก์ชันเป็นงานเดียว เพราะใช้รูปแบบ run-to-completion ของการเรียกใช้งาน ซึ่งหมายความว่าฟังก์ชันที่เรียกใช้ฟังก์ชันอื่นๆ อีกหลายฟังก์ชันอย่างเช่นตัวอย่างต่อไปนี้ จะต้องทำงานจนกว่าฟังก์ชันที่เรียกทั้งหมดจะเสร็จสมบูรณ์ ซึ่งจะทำให้เบราว์เซอร์ทำงานช้าลง
function saveSettings () { //This is a long task.
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
หากโค้ดมีฟังก์ชันที่เรียกใช้เมธอดหลายเมธอด ให้แบ่งโค้ดออกเป็นหลายฟังก์ชัน วิธีนี้ไม่เพียงแต่ทำให้เบราว์เซอร์มีโอกาสตอบสนองการโต้ตอบมากขึ้นเท่านั้น แต่ยังทำให้โค้ดอ่าน ดูแลรักษา และเขียนการทดสอบได้ง่ายขึ้นอีกด้วย ส่วนต่อไปนี้จะอธิบายกลยุทธ์บางอย่างสำหรับการแบ่งหน้าที่ยาวและจัดลำดับความสำคัญของงานเหล่านั้น
เลื่อนการเรียกใช้โค้ดด้วยตนเอง
คุณเลื่อนการดำเนินการบางงานได้โดยส่งฟังก์ชันที่เกี่ยวข้องไปยัง setTimeout()
การดำเนินการนี้จะใช้ได้แม้ว่าคุณจะระบุระยะหมดเวลาของ 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);
}
ซึ่งทำงานได้ดีที่สุดกับชุดฟังก์ชันที่ต้องทำงานตามลำดับ โค้ดที่จัดระเบียบต่างกันต้องใช้แนวทางที่ต่างออกไป ตัวอย่างถัดไปคือฟังก์ชันที่ประมวลผลข้อมูลจำนวนมากโดยใช้ลูป ยิ่งชุดข้อมูลมีขนาดใหญ่เท่าไร ก็ยิ่งใช้เวลานานขึ้นเท่านั้น และอาจไม่ได้มีตำแหน่งที่ดีในลูปสำหรับการวาง setTimeout()
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
โชคดีที่มี API อื่นๆ อีก 2-3 รายการที่ช่วยให้คุณเลื่อนการดำเนินการโค้ดไปยังงานในภายหลังได้ เราขอแนะนำให้ใช้ postMessage()
เพื่อให้หมดเวลาเร็วขึ้น
นอกจากนี้ คุณยังแบ่งงานโดยใช้ requestIdleCallback()
ได้ด้วย แต่ระบบจะกำหนดเวลางานในลำดับความสำคัญต่ำสุดและในช่วงที่ไม่มีเบราว์เซอร์เท่านั้น ซึ่งหมายความว่าหากชุดข้อความหลักไม่ว่างเป็นพิเศษ งานที่กำหนดเวลาด้วย requestIdleCallback()
อาจไม่สามารถเรียกใช้ได้
ใช้ async
/await
เพื่อสร้างคะแนนผลตอบแทน
ให้กลับไปที่เทรดหลักโดยการรบกวนคิวงานสั้นๆ เพื่อให้เบราว์เซอร์มีโอกาสทำงานที่สำคัญกว่า เพื่อให้แน่ใจว่างานสำคัญที่แสดงต่อผู้ใช้จะเกิดขึ้นก่อนงานที่มีลำดับความสำคัญต่ำกว่า
วิธีที่ชัดเจนที่สุดในการดำเนินการนี้คือ Promise
ซึ่งแก้ไขปัญหาด้วยการโทรหา setTimeout()
:
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
ในฟังก์ชัน saveSettings()
คุณจะกลับไปที่เทรดหลักหลังจากแต่ละขั้นตอนได้ หากawait
ฟังก์ชัน 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:
await yieldToMain();
}
}
ประเด็นสำคัญ: คุณไม่จำเป็นต้องให้ผลตอบแทนหลังจากการเรียกใช้ฟังก์ชันทุกครั้ง ตัวอย่างเช่น หากเรียกใช้ 2 ฟังก์ชันซึ่งส่งผลให้เกิดการอัปเดตที่สําคัญต่ออินเทอร์เฟซผู้ใช้ คุณอาจไม่ต้องการผลตอบรับจากฟังก์ชันเหล่านั้น หากทำได้ ให้ปล่อยให้ระบบทำงานนั้นก่อน แล้วลองพิจารณาเลือกระหว่างฟังก์ชันที่ทำงานในเบื้องหลังหรืองานที่สำคัญน้อยกว่าที่ผู้ใช้มองไม่เห็น
API เครื่องจัดตารางเวลาโดยเฉพาะ
API ที่กล่าวถึงไปแล้วสามารถช่วยคุณแบ่งงานได้ แต่มีข้อเสียสำคัญคือเมื่อคุณกลับไปยังเทรดหลักโดยการเลื่อนโค้ดไปทำงานในงานถัดไป ระบบจะเพิ่มโค้ดนั้นต่อท้ายคิวงาน
หากควบคุมโค้ดทั้งหมดในหน้าเว็บ คุณสามารถสร้างเครื่องจัดตารางเวลาของคุณเองเพื่อจัดลำดับความสำคัญของงานได้ อย่างไรก็ตาม สคริปต์ของบุคคลที่สามจะไม่ใช้เครื่องจัดตารางเวลาของคุณ คุณจึงไม่สามารถจัดลำดับความสำคัญในการทำงานในกรณีนั้นได้ คุณสามารถแยกชิ้นส่วนหรือปล่อย ตามการโต้ตอบของผู้ใช้เท่านั้น
API เครื่องจัดตารางเวลามีฟังก์ชัน postTask()
ซึ่งช่วยให้จัดตารางเวลางานที่ละเอียดขึ้น และช่วยให้เบราว์เซอร์จัดลำดับความสำคัญของงานได้ เพื่อให้งานที่มีลำดับความสำคัญต่ำส่งกลับไปยังเทรดหลัก postTask()
ใช้คำสัญญาและยอมรับการตั้งค่า priority
postTask()
API มีลำดับความสำคัญ 3 ประการดังนี้
'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'});
};
ในส่วนนี้ ลำดับความสำคัญของงานจะได้รับการกำหนดเวลาเพื่อให้งานที่มีลำดับความสำคัญของเบราว์เซอร์มีผลใช้งาน เช่น การโต้ตอบของผู้ใช้เกิดขึ้นได้
นอกจากนี้ คุณยังสร้างอินสแตนซ์ TaskController
ที่แตกต่างกันที่มีลำดับความสำคัญระหว่างงานต่างๆ ได้ รวมถึงความสามารถในการเปลี่ยนลำดับความสำคัญของอินสแตนซ์ TaskController
ต่างๆ ตามที่จำเป็น
ผลตอบแทนในตัวที่มีการใช้งานอย่างต่อเนื่องโดยใช้ scheduler.yield()
API ที่กำลังจะเปิดตัว
ประเด็นสำคัญ: หากต้องการดูคำอธิบายโดยละเอียดเพิ่มเติมเกี่ยวกับ scheduler.yield()
ให้อ่านเกี่ยวกับช่วงทดลองใช้จากต้นทาง (ตั้งแต่สรุป) และคำอธิบาย
สิ่งหนึ่งที่เสนอให้เพิ่มใน 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
สูง คุณจึงใช้ทางเลือกดังกล่าวแทนจนกว่า scheduler.yield()
จะพร้อมใช้งานอย่างแพร่หลายมากขึ้น
การใช้ setTimeout()
(หรือ scheduler.postTask()
ที่มี priority: 'user-visible'
หรือไม่มี priority
อย่างชัดแจ้ง) จะกำหนดเวลางานที่อยู่ด้านหลังคิวเพื่อให้งานอื่นๆ ที่รอดำเนินการได้ทำงานก่อนการทำงานอย่างต่อเนื่อง
ผลตอบแทนตามอินพุตที่มี isInputPending()
การสนับสนุนเบราว์เซอร์
- 87
- 87
- x
- x
isInputPending()
API มีวิธีตรวจสอบว่าผู้ใช้ได้พยายามโต้ตอบกับหน้าเว็บหรือไม่ และให้ผลปรากฏเฉพาะเมื่ออินพุตรอดำเนินการอยู่
ซึ่งจะช่วยให้ JavaScript ดำเนินการต่อได้หากไม่มีอินพุตใดๆ ที่รอดำเนินการ แทนที่จะส่งคืนและจบลงที่ด้านหลังของคิวงาน ซึ่งอาจส่งผลให้มีการปรับปรุงประสิทธิภาพขึ้นอย่างน่าประทับใจ ตามที่อธิบายไว้ในความตั้งใจที่จะจัดส่งสำหรับเว็บไซต์ที่อาจไม่กลับมาที่ชุดข้อความหลัก
อย่างไรก็ตาม นับตั้งแต่เปิดตัว API นั้น ความเข้าใจเกี่ยวกับผลตอบแทนของเราก็ดีขึ้น โดยเฉพาะอย่างยิ่งหลังจากการเปิดตัว INP เราไม่แนะนำให้ใช้ API นี้อีกต่อไป แต่ขอแนะนำให้อนุญาตไม่ว่าอินพุตจะรอดำเนินการหรือไม่ก็ตาม การเปลี่ยนแปลงวิดีโอแนะนำนี้เกิดขึ้นได้จากหลายสาเหตุดังนี้
- API อาจแสดงผล
false
อย่างไม่ถูกต้องในบางกรณีที่ผู้ใช้โต้ตอบ - ข้อมูลที่ป้อนไม่ใช่กรณีเดียวที่งานควรให้ผล ภาพเคลื่อนไหวและการอัปเดตอินเทอร์เฟซผู้ใช้ทั่วไปอื่นๆ ก็มีความสำคัญไม่แพ้กันในการแสดงหน้าเว็บที่ปรับเปลี่ยนตามอุปกรณ์
- ตั้งแต่นั้นมามีการเปิดตัว API ที่ให้ผลตอบแทนได้ครอบคลุมยิ่งขึ้น เช่น
scheduler.postTask()
และscheduler.yield()
เพื่อจัดการกับข้อกังวลใจ
บทสรุป
การจัดการงานต่างๆ อาจเป็นเรื่องยาก แต่การทำเช่นนี้จะช่วยให้หน้าเว็บของคุณตอบสนองต่อการโต้ตอบของผู้ใช้ได้เร็วขึ้น มีเทคนิคมากมายในการจัดการและจัดลำดับความสำคัญของงานตามกรณีการใช้งาน ย้ำว่าสิ่งสำคัญที่คุณควรพิจารณาเมื่อจัดการงานมีดังนี้
- ใช้ได้กับเทรดหลักสำหรับงานสำคัญที่แสดงต่อผู้ใช้
- ลองทดสอบกับ
scheduler.yield()
- จัดลำดับความสำคัญของงานด้วย
postTask()
- สุดท้าย ให้ทำงานในฟังก์ชันให้น้อยที่สุด
ด้วยเครื่องมือเหล่านี้อย่างน้อย 1 อย่าง คุณควรสามารถจัดโครงสร้างงานในแอปพลิเคชันของคุณได้ เพื่อจัดลำดับความสำคัญของความต้องการของผู้ใช้ ในขณะเดียวกันก็ยังสามารถทำงานที่สำคัญน้อยลงได้ จะช่วยปรับปรุงประสบการณ์ของผู้ใช้ โดยทำให้แอปตอบสนองได้ดีและสนุกยิ่งขึ้น
ขอขอบคุณเป็นพิเศษที่ Philip Walton ตรวจสอบทางเทคนิคในเอกสารนี้
ภาพขนาดย่อมาจาก Unsplash โดยได้รับความเอื้อเฟื้อจาก Amirali Mirhashemian