เกี่ยวกับ Codelab นี้
1 บทนำ
การสาธิตแบบอินเทอร์แอกทีฟและ Codelab สำหรับการเรียนรู้เกี่ยวกับ Interaction to Next Paint (INP)
ข้อกำหนดเบื้องต้น
- มีความรู้ด้านการพัฒนา HTML และ JavaScript
- แนะนำ: อ่านเอกสารประกอบเกี่ยวกับ INP
สิ่งที่คุณจะได้เรียนรู้
- การโต้ตอบของผู้ใช้และการจัดการการโต้ตอบเหล่านั้นส่งผลต่อการตอบสนองของหน้าเว็บอย่างไร
- วิธีลดและขจัดความล่าช้าเพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ราบรื่น
สิ่งที่ต้องมี
- คอมพิวเตอร์ที่สามารถโคลนโค้ดจาก GitHub และเรียกใช้คำสั่ง npm
- โปรแกรมแก้ไขข้อความ
- Chrome เวอร์ชันล่าสุดเพื่อให้การวัดการโต้ตอบทั้งหมดทํางานได้
2 ตั้งค่า
รับและเรียกใช้โค้ด
คุณจะพบโค้ดในที่เก็บweb-vitals-codelabs
- โคลนที่เก็บในเทอร์มินัล:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
- ไปยังไดเรกทอรีที่โคลน:
cd web-vitals-codelabs/understanding-inp
- ติดตั้งการอ้างอิง:
npm ci
- เริ่มเว็บเซิร์ฟเวอร์:
npm run start
- ไปที่ http://localhost:5173/understanding-inp/ ในเบราว์เซอร์
ภาพรวมของแอป
ที่ด้านบนของหน้าจะมีตัวนับคะแนนและปุ่มเพิ่ม การสาธิตการตอบสนองและการตอบสนองแบบคลาสสิก
ด้านล่างปุ่มจะมีข้อมูลการวัด 4 รายการดังนี้
- INP: คะแนน INP ปัจจุบัน ซึ่งโดยปกติแล้วคือการโต้ตอบที่แย่ที่สุด
- การโต้ตอบ: คะแนนของการโต้ตอบครั้งล่าสุด
- FPS: เฟรมต่อวินาทีของเทรดหลักของหน้าเว็บ
- ตัวจับเวลา: ภาพเคลื่อนไหวของตัวจับเวลาที่ทำงานอยู่เพื่อช่วยให้เห็นภาพความหน่วง
รายการ FPS และตัวจับเวลาไม่จำเป็นต่อการวัดการโต้ตอบ เราเพิ่มเส้นเหล่านี้เพื่อให้เห็นภาพการตอบสนองได้ง่ายขึ้น
ลองเลย
ลองโต้ตอบกับปุ่มเพิ่มและดูคะแนนที่เพิ่มขึ้น ค่า INP และการโต้ตอบเปลี่ยนแปลงตามการเพิ่มแต่ละครั้งหรือไม่
INP จะวัดระยะเวลาตั้งแต่ที่ผู้ใช้โต้ตอบจนกว่าหน้าเว็บจะแสดงการอัปเดตที่แสดงผลต่อผู้ใช้จริง
3 การวัดการโต้ตอบด้วยเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome
เปิดเครื่องมือสำหรับนักพัฒนาเว็บจากเมนูเครื่องมือเพิ่มเติม > เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ โดยคลิกขวาที่หน้าเว็บแล้วเลือกตรวจสอบ หรือใช้แป้นพิมพ์ลัด
เปลี่ยนไปที่แผงประสิทธิภาพ ซึ่งคุณจะใช้เพื่อวัดการโต้ตอบ
จากนั้นบันทึกการโต้ตอบในแผงประสิทธิภาพ
- กด "บันทึก"
- โต้ตอบกับหน้าเว็บ (กดปุ่มเพิ่ม)
- หยุดการบันทึก
ในไทม์ไลน์ที่ได้ คุณจะเห็นแทร็กการโต้ตอบ ขยายโดยคลิกสามเหลี่ยมทางด้านซ้าย
การโต้ตอบ 2 รายการจะปรากฏขึ้น ซูมเข้าที่รูปที่ 2 โดยการเลื่อนหรือกดปุ่ม W ค้างไว้
เมื่อวางเมาส์เหนือการโต้ตอบ คุณจะเห็นว่าการโต้ตอบนั้นรวดเร็ว ไม่ใช้เวลาในระยะเวลาการประมวลผล และใช้เวลาขั้นต่ำในความล่าช้าของอินพุตและความล่าช้าในการนำเสนอ ซึ่งระยะเวลาที่แน่นอนจะขึ้นอยู่กับความเร็วของเครื่อง
4 Listener เหตุการณ์ที่ทำงานเป็นเวลานาน
เปิดไฟล์ index.js
แล้วยกเลิกการแสดงความคิดเห็นของฟังก์ชัน blockFor
ภายในเครื่องมือฟังเหตุการณ์
ดูโค้ดฉบับเต็ม: click_block.html
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
});
บันทึกไฟล์ เซิร์ฟเวอร์จะเห็นการเปลี่ยนแปลงและรีเฟรชหน้าเว็บให้คุณ
ลองโต้ตอบกับหน้าเว็บอีกครั้ง ตอนนี้การโต้ตอบจะช้าลงอย่างเห็นได้ชัด
การติดตามประสิทธิภาพ
บันทึกอีกครั้งในแผงประสิทธิภาพเพื่อดูว่าลักษณะการทำงานเป็นอย่างไร
การโต้ตอบที่เคยใช้เวลาเพียงเล็กน้อยตอนนี้ใช้เวลาเต็ม 1 วินาที
เมื่อวางเมาส์เหนือการโต้ตอบ ให้สังเกตว่าเวลาเกือบทั้งหมดใช้ไปกับ "ระยะเวลาการประมวลผล" ซึ่งเป็นระยะเวลาที่ใช้ในการเรียกกลับของเครื่องมือฟังเหตุการณ์ เนื่องจากblockFor
การเรียกใช้ที่บล็อกอยู่ทั้งหมดอยู่ใน Listener เหตุการณ์ เวลาจึงอยู่ที่นั่น
5 การทดสอบ: ระยะเวลาในการประมวลผล
ลองจัดเรียงงานของ Listener เหตุการณ์ใหม่เพื่อดูผลกระทบต่อ INP
อัปเดต UI ก่อน
จะเกิดอะไรขึ้นหากคุณสลับลำดับการเรียกใช้ JS โดยอัปเดต UI ก่อน แล้วจึงบล็อก
ดูโค้ดฉบับเต็ม: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
คุณสังเกตเห็นว่า UI ปรากฏขึ้นก่อนหน้านี้ไหม ลำดับมีผลต่อคะแนน INP ไหม
ลองทำการติดตามและตรวจสอบการโต้ตอบเพื่อดูว่ามีความแตกต่างหรือไม่
ผู้ฟังที่แยกกัน
จะเกิดอะไรขึ้นหากคุณย้ายงานไปยังตัวแฮนเดิลเหตุการณ์แยกต่างหาก อัปเดต UI ใน Listener เหตุการณ์เดียว และบล็อกหน้าเว็บจาก Listener แยกต่างหาก
ดูโค้ดฉบับเต็ม: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
ตอนนี้แผงประสิทธิภาพมีลักษณะอย่างไร
ประเภทเหตุการณ์ต่างๆ
การโต้ตอบส่วนใหญ่จะทริกเกอร์เหตุการณ์หลายประเภท ตั้งแต่เหตุการณ์ของเคอร์เซอร์หรือคีย์ ไปจนถึงเหตุการณ์การวางเมาส์ โฟกัส/เบลอ และเหตุการณ์สังเคราะห์ เช่น beforechange และ beforeinput
หน้าเว็บจริงจำนวนมากมี Listener สำหรับเหตุการณ์ต่างๆ มากมาย
จะเกิดอะไรขึ้นหากคุณเปลี่ยนประเภทเหตุการณ์สำหรับ Listener เหตุการณ์ เช่น แทนที่ Event Listener click
รายการใดรายการหนึ่งด้วย pointerup
หรือ mouseup
ดูโค้ดฉบับเต็ม: diff_handlers.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
ไม่มีการอัปเดต UI
จะเกิดอะไรขึ้นหากคุณนำการเรียกเพื่ออัปเดต UI ออกจาก Listener เหตุการณ์
ดูโค้ดฉบับเต็ม: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
6 ผลการทดสอบระยะเวลาในการประมวลผล
การติดตามประสิทธิภาพ: อัปเดต UI ก่อน
ดูโค้ดฉบับเต็ม: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
เมื่อดูการบันทึกแผงประสิทธิภาพของการคลิกปุ่ม คุณจะเห็นว่าผลลัพธ์ไม่เปลี่ยนแปลง แม้ว่าการอัปเดต UI จะทริกเกอร์ก่อนโค้ดบล็อก แต่เบราว์เซอร์ไม่ได้อัปเดตสิ่งที่วาดบนหน้าจอจนกว่า Listener เหตุการณ์จะเสร็จสมบูรณ์ ซึ่งหมายความว่าการโต้ตอบยังคงใช้เวลาเพียง 1 วินาทีในการดำเนินการให้เสร็จสมบูรณ์
การติดตามประสิทธิภาพ: ผู้ฟังที่แยกกัน
ดูโค้ดฉบับเต็ม: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
ขอย้ำอีกครั้งว่าฟังก์ชันการทำงานนั้นไม่แตกต่างกัน การโต้ตอบยังคงใช้เวลา 1 วินาทีเต็ม
หากซูมเข้าไปที่การโต้ตอบการคลิก คุณจะเห็นว่ามีการเรียกใช้ฟังก์ชัน 2 อย่างที่แตกต่างกันจริงอันเป็นผลมาจากเหตุการณ์ click
ตามที่คาดไว้ การอัปเดต UI ซึ่งเป็นงานแรกจะทำงานได้อย่างรวดเร็ว ส่วนงานที่ 2 จะใช้เวลา 1 วินาทีเต็ม อย่างไรก็ตาม ผลรวมของผลกระทบเหล่านี้ส่งผลให้เกิดการโต้ตอบที่ช้าเหมือนกันกับผู้ใช้ปลายทาง
การติดตามประสิทธิภาพ: เหตุการณ์ประเภทต่างๆ
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
ผลลัพธ์เหล่านี้คล้ายกันมาก การโต้ตอบยังคงใช้เวลา 1 วินาทีเต็ม ข้อแตกต่างเพียงอย่างเดียวคือตอนนี้ Listener click
ที่อัปเดต UI เท่านั้นซึ่งสั้นกว่าจะทำงานหลังจาก Listener pointerup
ที่บล็อก
การติดตามประสิทธิภาพ: ไม่มีการอัปเดต UI
ดูโค้ดฉบับเต็ม: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
- คะแนนจะไม่ได้รับการอัปเดต แต่หน้าเว็บจะยังคงได้รับการอัปเดต
- ภาพเคลื่อนไหว เอฟเฟกต์ CSS การดำเนินการเริ่มต้นของคอมโพเนนต์เว็บ (อินพุตแบบฟอร์ม) การป้อนข้อความ และการไฮไลต์ข้อความจะยังคงได้รับการอัปเดตต่อไป
ในกรณีนี้ ปุ่มจะเปลี่ยนเป็นสถานะที่ใช้งานอยู่และกลับมาเมื่อคลิก ซึ่งต้องมีการระบายสีโดยเบราว์เซอร์ ซึ่งหมายความว่ายังมี INP อยู่
เนื่องจากตัวแฮนเดิลเหตุการณ์บล็อกเทรดหลักเป็นเวลา 1 วินาที ทำให้หน้าเว็บไม่แสดงผล การโต้ตอบจึงยังคงใช้เวลา 1 วินาทีเต็ม
การบันทึกแผงประสิทธิภาพจะแสดงการโต้ตอบที่แทบจะเหมือนกับการโต้ตอบก่อนหน้า
สั่งกลับบ้าน
โค้ดใดๆ ที่ทํางานใน Listener เหตุการณ์ใดๆ จะทําให้การโต้ตอบล่าช้า
- ซึ่งรวมถึง Listener ที่ลงทะเบียนจากสคริปต์และเฟรมเวิร์กต่างๆ หรือโค้ดไลบรารีที่ทำงานใน Listener เช่น การอัปเดตสถานะที่ทริกเกอร์การแสดงผลคอมโพเนนต์
- ไม่เพียงแต่โค้ดของคุณเองเท่านั้น แต่ยังรวมถึงสคริปต์ของบุคคลที่สามทั้งหมดด้วย
นี่เป็นปัญหาที่พบบ่อย
สุดท้ายนี้ เพียงเพราะโค้ดไม่ได้ทริกเกอร์การแสดงผลไม่ได้หมายความว่าการแสดงผลจะไม่รอให้เครื่องมือฟังเหตุการณ์ที่ทำงานช้าเสร็จสมบูรณ์
7 การทดสอบ: ความล่าช้าของอินพุต
แล้วโค้ดที่ใช้เวลานานซึ่งอยู่นอก Listener เหตุการณ์ล่ะ เช่น
- หากคุณมี
<script>
ที่โหลดช้าซึ่งบล็อกหน้าเว็บแบบสุ่มระหว่างการโหลด - การเรียก API เช่น
setInterval
ที่บล็อกหน้าเว็บเป็นระยะๆ
ลองนำ blockFor
ออกจาก Listener เหตุการณ์ แล้วเพิ่มลงใน setInterval()
ดูโค้ดฉบับเต็ม: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
สิ่งที่เกิดขึ้น
8 ผลการทดสอบความล่าช้าของอินพุต
ดูโค้ดฉบับเต็ม: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
การบันทึกการคลิกปุ่มที่เกิดขึ้นขณะที่setInterval
งานการบล็อกกำลังทำงานจะส่งผลให้เกิดการโต้ตอบที่ใช้เวลานาน แม้ว่าจะไม่มีการบล็อกในการโต้ตอบนั้นก็ตาม
ระยะเวลาที่ใช้เวลานานเหล่านี้มักเรียกว่างานที่ใช้เวลานาน
เมื่อวางเมาส์เหนือการโต้ตอบในเครื่องมือสำหรับนักพัฒนาเว็บ คุณจะเห็นว่าตอนนี้เวลาในการโต้ตอบส่วนใหญ่เป็นผลมาจากเวลาหน่วงของอินพุต ไม่ใช่ระยะเวลาการประมวลผล
โปรดทราบว่าการตั้งค่านี้ไม่ได้ส่งผลต่อการโต้ตอบเสมอไป หากคุณไม่คลิกขณะที่งานกำลังทำงาน คุณอาจโชคดี การจามแบบ "สุ่ม" ดังกล่าวอาจเป็นฝันร้ายในการแก้ไขข้อบกพร่องเมื่อบางครั้งก็ทำให้เกิดปัญหา
วิธีหนึ่งในการติดตามปัญหาเหล่านี้คือการวัดงานที่ใช้เวลานาน (หรือเฟรมภาพเคลื่อนไหวที่ใช้เวลานาน) และเวลาที่ถูกบล็อกทั้งหมด
9 งานนำเสนอช้า
ที่ผ่านมา เราได้ดูประสิทธิภาพของ JavaScript ผ่านการหน่วงเวลาอินพุตหรือเครื่องมือฟังเหตุการณ์ แต่มีอะไรอีกบ้างที่ส่งผลต่อการแสดงผล Next Paint
การอัปเดตหน้าเว็บด้วยเอฟเฟกต์ราคาแพง
แม้ว่าการอัปเดตหน้าเว็บจะเกิดขึ้นอย่างรวดเร็ว แต่เบราว์เซอร์ก็อาจยังต้องทำงานอย่างหนักเพื่อแสดงผลหน้าเว็บ
ในเทรดหลัก ให้ทำดังนี้
- เฟรมเวิร์ก UI ที่ต้องแสดงผลการอัปเดตหลังจากมีการเปลี่ยนแปลงสถานะ
- การเปลี่ยนแปลง DOM หรือการสลับตัวเลือกการค้นหา CSS ที่มีค่าใช้จ่ายสูงหลายรายการอาจทําให้เกิดการจัดรูปแบบ เลย์เอาต์ และการวาดจำนวนมาก
นอกเทรดหลัก
- การใช้ CSS เพื่อขับเคลื่อนเอฟเฟกต์ GPU
- การเพิ่มรูปภาพความละเอียดสูงขนาดใหญ่มาก
- การใช้ SVG/Canvas เพื่อวาดฉากที่ซับซ้อน
ตัวอย่างที่พบได้ทั่วไปบนเว็บ
- เว็บไซต์ SPA ที่สร้าง DOM ทั้งหมดใหม่หลังจากคลิกลิงก์ โดยไม่หยุดชั่วคราวเพื่อแสดงความคิดเห็นด้วยภาพเริ่มต้น
- หน้าค้นหาที่มีตัวกรองการค้นหาที่ซับซ้อนพร้อมอินเทอร์เฟซผู้ใช้แบบไดนามิก แต่ต้องเรียกใช้ Listener ที่มีค่าใช้จ่ายสูงจึงจะทำได้
- ปุ่มเปิด/ปิดโหมดมืดที่ทริกเกอร์สไตล์/เลย์เอาต์สำหรับทั้งหน้า
10 การทดสอบ: ความล่าช้าของงานนำเสนอ
requestAnimationFrame
ทำงานช้า
มาจำลองการนำเสนอที่ล่าช้าเป็นเวลานานโดยใช้ requestAnimationFrame()
API กัน
ย้ายการเรียก blockFor
ไปยัง Callback ของ requestAnimationFrame
เพื่อให้ทำงานหลังจากที่ Listener เหตุการณ์แสดงผล
ดูโค้ดทั้งหมด: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
สิ่งที่เกิดขึ้น
11 ผลการทดสอบความล่าช้าของงานนำเสนอ
ดูโค้ดทั้งหมด: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
การโต้ตอบยังคงยาว 1 วินาที แล้วเกิดอะไรขึ้น
requestAnimationFrame
ขอการเรียกกลับก่อนการแสดงผลครั้งถัดไป เนื่องจาก INP วัดเวลาตั้งแต่การโต้ตอบจนถึงการแสดงผลครั้งถัดไป blockFor(1000)
ใน requestAnimationFrame
จึงยังคงบล็อกการแสดงผลครั้งถัดไปเป็นเวลา 1 วินาทีเต็ม
อย่างไรก็ตาม โปรดทราบ 2 สิ่งต่อไปนี้
- เมื่อวางเมาส์เหนือ คุณจะเห็นว่าเวลาในการโต้ตอบทั้งหมดจะใช้ไปกับ "ความล่าช้าในการนำเสนอ" เนื่องจากมีการบล็อกเทรดหลักหลังจากที่เครื่องมือฟังเหตุการณ์กลับมา
- รูทของกิจกรรมในเทรดหลักไม่ใช่เหตุการณ์คลิกอีกต่อไป แต่เป็น "Animation Frame Fired"
12 การวินิจฉัยการโต้ตอบ
ในหน้าทดสอบนี้ การตอบสนองจะมองเห็นได้ชัดเจนมาก โดยมีคะแนน ตัวจับเวลา และ UI ตัวนับ แต่เมื่อทดสอบหน้าเว็บโดยเฉลี่ย การตอบสนองจะสังเกตได้ยากกว่า
เมื่อการโต้ตอบใช้เวลานาน เราอาจไม่ทราบเสมอไปว่าสาเหตุเกิดจากอะไร ซึ่งอาจเป็น
- ความล่าช้าของอินพุต
- ระยะเวลาการประมวลผลเหตุการณ์
- ความล่าช้าของงานนำเสนอ
คุณใช้เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome เพื่อช่วยวัดการตอบสนองได้ในหน้าใดก็ได้ที่ต้องการ หากต้องการสร้างนิสัยนี้ ให้ลองทำตามขั้นตอนต่อไปนี้
- ท่องเว็บตามปกติ
- คอยดูบันทึกการโต้ตอบในมุมมองเมตริกแบบเรียลไทม์ของแผงประสิทธิภาพในเครื่องมือสำหรับนักพัฒนาเว็บ
- หากเห็นการโต้ตอบที่มีประสิทธิภาพต่ำ ให้ลองทำซ้ำดังนี้
- หากทำซ้ำไม่ได้ ให้ใช้บันทึกการโต้ตอบเพื่อรับข้อมูลเชิงลึก
- หากทำซ้ำได้ ให้บันทึกการติดตามในแผงประสิทธิภาพ
ความล่าช้าทั้งหมด
ลองเพิ่มปัญหาเหล่านี้ลงในหน้าเว็บ
ดูโค้ดฉบับเต็ม: all_the_things.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
จากนั้นใช้แผงคอนโซลและแผงประสิทธิภาพเพื่อวินิจฉัยปัญหา
13 การทดลอง: งานที่ไม่พร้อมกัน
เนื่องจากคุณสามารถเริ่มเอฟเฟกต์ที่ไม่ใช่ภาพภายในอินเทอร์แอกชันได้ เช่น การส่งคำขอเครือข่าย การเริ่มตัวนับ หรือเพียงแค่การอัปเดตสถานะส่วนกลาง จะเกิดอะไรขึ้นเมื่อเอฟเฟกต์เหล่านั้นอัปเดตหน้าเว็บในที่สุด
ตราบใดที่Next Paint หลังจากอนุญาตให้แสดงผลการโต้ตอบได้ แม้ว่าเบราว์เซอร์จะตัดสินใจว่าไม่จำเป็นต้องมีการอัปเดตการแสดงผลใหม่ การวัดการโต้ตอบก็จะหยุดลง
หากต้องการลองใช้ ให้ดำเนินการอัปเดต UI ต่อจากเครื่องมือฟังการคลิก แต่เรียกใช้การบล็อกจากระยะหมดเวลา
ดูโค้ดฉบับเต็ม: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
เกิดอะไรขึ้นกับคำบรรยายที่ส่งไป
14 ผลการทดสอบการทำงานแบบไม่พร้อมกัน
ดูโค้ดฉบับเต็ม: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
ตอนนี้การโต้ตอบจึงสั้นลงเนื่องจากชุดข้อความหลักพร้อมใช้งานทันทีหลังจากอัปเดต UI งานที่บล็อกนานยังคงทำงานต่อไป แต่จะทำงานหลังจาก Paint สักระยะหนึ่ง ผู้ใช้จึงจะได้รับความคิดเห็น UI ทันที
บทเรียน: หากนำออกไม่ได้ อย่างน้อยก็ย้าย
เมธอด
เราจะทำได้ดีกว่า 100 มิลลิวินาทีที่กำหนดsetTimeout
ไหม เราอาจยังคงต้องการให้โค้ดทำงานโดยเร็วที่สุด ไม่เช่นนั้นเราก็ควรนำโค้ดออกไปเลย
เป้าหมาย:
- การโต้ตอบจะทำงาน
incrementAndUpdateUI()
blockFor()
จะทำงานโดยเร็วที่สุด แต่จะไม่บล็อกการวาดภาพครั้งถัดไป- ซึ่งจะส่งผลให้เกิดลักษณะการทำงานที่คาดการณ์ได้โดยไม่มี "ระยะหมดเวลาที่คาดไม่ถึง"
วิธีดำเนินการนี้มีดังนี้
setTimeout(0)
Promise.then()
requestAnimationFrame
requestIdleCallback
scheduler.postTask()
"requestPostAnimationFrame"
requestAnimationFrame
เพียงอย่างเดียวจะพยายามเรียกใช้ก่อนการแสดงผลครั้งถัดไปและมักจะยังทำให้เกิดการโต้ตอบที่ช้า แต่ requestAnimationFrame
+ setTimeout
จะทำให้เกิด Polyfill ที่เรียบง่ายสำหรับ requestPostAnimationFrame
โดยเรียกใช้ Callback หลังจากการแสดงผลครั้งถัดไป
ดูโค้ดฉบับเต็ม: raf+task.html
function afterNextPaint(callback) {
requestAnimationFrame(() => {
setTimeout(callback, 0);
});
}
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
afterNextPaint(() => {
blockFor(1000);
});
});
คุณยังห่อหุ้มด้วย Promise เพื่อให้ใช้งานง่ายได้ด้วย
ดูโค้ดฉบับเต็ม: raf+task2.html
async function nextPaint() {
return new Promise(resolve => afterNextPaint(resolve));
}
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
await nextPaint();
blockFor(1000);
});
15 การโต้ตอบหลายครั้ง (และการคลิกอย่างรวดเร็ว)
การย้ายงานที่บล็อกเป็นเวลานานอาจช่วยได้ แต่ก็ยังบล็อกหน้าเว็บอยู่ ซึ่งจะส่งผลต่อการโต้ตอบในอนาคต รวมถึงภาพเคลื่อนไหวและการอัปเดตอื่นๆ ของหน้าเว็บด้วย
ลองใช้เวอร์ชันการบล็อกแบบไม่พร้อมกันของหน้าอีกครั้ง (หรือเวอร์ชันของคุณเองหากคุณคิดรูปแบบการเลื่อนงานในขั้นตอนสุดท้าย)
ดูโค้ดฉบับเต็ม: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
จะเกิดอะไรขึ้นหากคุณคลิกหลายครั้งอย่างรวดเร็ว
การติดตามประสิทธิภาพ
การคลิกแต่ละครั้งจะทำให้มีการจัดคิวงานที่ใช้เวลา 1 วินาที ซึ่งทำให้มั่นใจได้ว่าเทรดหลักจะถูกบล็อกเป็นระยะเวลาหนึ่ง
เมื่อมีการทับซ้อนกันระหว่างงานที่ใช้เวลานานกับการคลิกใหม่ที่เข้ามา การโต้ตอบจะช้าแม้ว่าตัว Listener ของเหตุการณ์จะตอบกลับเกือบจะทันทีก็ตาม เราได้สร้างสถานการณ์เดียวกันกับการทดสอบก่อนหน้านี้ที่มีความล่าช้าในการป้อนข้อมูล แต่ครั้งนี้ ความล่าช้าของอินพุตไม่ได้มาจาก setInterval
แต่มาจากการทำงานที่ทริกเกอร์โดยเครื่องมือฟังเหตุการณ์ก่อนหน้านี้
กลยุทธ์
เราต้องการนำงานที่ใช้เวลานานออกโดยสมบูรณ์
- นำโค้ดที่ไม่จำเป็นออกทั้งหมด โดยเฉพาะสคริปต์
- เพิ่มประสิทธิภาพโค้ดเพื่อหลีกเลี่ยงการเรียกใช้งานที่ใช้เวลานาน
- ยกเลิกงานที่ล้าสมัยเมื่อมีการโต้ตอบใหม่
16 กลยุทธ์ที่ 1: การดีบาวซ์
กลยุทธ์คลาสสิก เมื่อมีการโต้ตอบอย่างรวดเร็วและต่อเนื่อง และการประมวลผลหรือผลกระทบจากเครือข่ายมีค่าใช้จ่ายสูง ให้ตั้งใจหน่วงเวลาการเริ่มงานเพื่อให้คุณยกเลิกและเริ่มใหม่ได้ รูปแบบนี้มีประโยชน์สำหรับอินเทอร์เฟซผู้ใช้ เช่น ช่องเติมข้อความอัตโนมัติ
- ใช้
setTimeout
เพื่อหน่วงเวลาการเริ่มงานที่มีค่าใช้จ่ายสูงด้วยตัวจับเวลา อาจเป็น 500-1,000 มิลลิวินาที - บันทึกรหัสตัวจับเวลาเมื่อดำเนินการ
- หากมีการโต้ตอบใหม่ ให้ยกเลิกตัวจับเวลาก่อนหน้าโดยใช้
clearTimeout
ดูโค้ดทั้งหมด: debounce.html
let timer;
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
blockFor(1000);
}, 1000);
});
การติดตามประสิทธิภาพ
แม้จะมีการคลิกหลายครั้ง แต่จะมีเพียงงาน blockFor
เดียวเท่านั้นที่ทำงาน โดยจะรอจนกว่าจะไม่มีการคลิกเป็นเวลา 1 วินาทีเต็มก่อนจึงจะทำงาน สําหรับการโต้ตอบที่เกิดขึ้นเป็นชุด เช่น การพิมพ์ในช่องป้อนข้อความหรือรายการเป้าหมายที่คาดว่าจะได้รับการคลิกอย่างรวดเร็วหลายครั้ง กลยุทธ์นี้เป็นกลยุทธ์ที่เหมาะสําหรับใช้โดยค่าเริ่มต้น
17 กลยุทธ์ที่ 2: ขัดจังหวะงานที่ใช้เวลานาน
อย่างไรก็ตาม ยังคงมีโอกาสที่การคลิกครั้งต่อไปจะเกิดขึ้นหลังจากช่วงการดีบาวซ์ผ่านไปแล้ว ซึ่งจะทำให้เกิดการคลิกในระหว่างงานที่ใช้เวลานาน และกลายเป็นการโต้ตอบที่ช้ามากเนื่องจากความล่าช้าในการป้อนข้อมูล
ในกรณีที่การโต้ตอบเกิดขึ้นในระหว่างที่เรากำลังทำงาน เราต้องการหยุดพักจากงานที่ยุ่งเพื่อให้จัดการกับการโต้ตอบใหม่ๆ ได้ทันที เราจะทำได้อย่างไร
มี API บางอย่าง เช่น isInputPending
แต่โดยทั่วไปแล้วการแบ่งงานที่ยาวออกเป็นส่วนๆ จะดีกว่า
setTimeout
จำนวนมาก
ความพยายามครั้งแรก: ทำอะไรที่เรียบง่าย
ดูโค้ดฉบับเต็ม: small_tasks.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
});
});
โดยจะช่วยให้เบราว์เซอร์จัดกำหนดการแต่ละงานแยกกันได้ และการป้อนข้อมูลจะมีลำดับความสำคัญสูงขึ้น
เรากลับมาใช้เวลาทำงานเต็ม 5 วินาทีสำหรับการคลิก 5 ครั้ง แต่ระบบจะแบ่งงาน 1 วินาทีต่อการคลิกแต่ละครั้งออกเป็นงาน 100 มิลลิวินาที 10 งาน ด้วยเหตุนี้ แม้ว่าการโต้ตอบหลายรายการจะทับซ้อนกับงานเหล่านั้น แต่การโต้ตอบใดๆ ก็ไม่มีเวลาในการตอบสนองที่นานกว่า 100 มิลลิวินาที เบราว์เซอร์จะให้ความสำคัญกับเครื่องมือฟังเหตุการณ์ขาเข้ามากกว่าsetTimeout
งาน และการโต้ตอบจะยังคงตอบสนองได้
กลยุทธ์นี้ได้ผลดีโดยเฉพาะอย่างยิ่งเมื่อกำหนดเวลาจุดแรกเข้าแยกกัน เช่น หากคุณมีฟีเจอร์อิสระหลายอย่างที่ต้องเรียกใช้ในเวลาโหลดแอปพลิเคชัน การโหลดสคริปต์และการเรียกใช้ทุกอย่างในเวลาประเมินสคริปต์อาจทำให้ทุกอย่างทำงานในงานที่ใช้เวลานานมากโดยค่าเริ่มต้น
อย่างไรก็ตาม กลยุทธ์นี้ใช้ไม่ได้กับโค้ดที่เชื่อมโยงกันอย่างแน่นหนา เช่น ลูป for
ที่ใช้สถานะที่ใช้ร่วมกัน
ตอนนี้พร้อมใช้งานใน yield()
อย่างไรก็ตาม เราสามารถใช้ประโยชน์จาก async
และ await
สมัยใหม่เพื่อเพิ่ม "จุดผลตอบแทน" ลงในฟังก์ชัน JavaScript ใดก็ได้ได้อย่างง่ายดาย
เช่น
ดูโค้ดทั้งหมด: yieldy.html
// Polyfill for scheduler.yield()
async function schedulerDotYield() {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
async function blockInPiecesYieldy(ms) {
const ms_per_part = 10;
const parts = ms / ms_per_part;
for (let i = 0; i < parts; i++) {
await schedulerDotYield();
blockFor(ms_per_part);
}
}
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
await blockInPiecesYieldy(1000);
});
เช่นเดียวกับก่อนหน้านี้ เธรดหลักจะหยุดทำงานหลังจากทำงานเสร็จเป็นกลุ่ม และเบราว์เซอร์จะตอบสนองต่อการโต้ตอบที่เข้ามาได้ แต่ตอนนี้สิ่งที่คุณต้องทำคือใช้ await schedulerDotYield()
แทน setTimeout
แยกกัน ซึ่งทำให้ใช้งานได้สะดวกแม้ในระหว่างลูป for
ตอนนี้พร้อมใช้งานใน AbortContoller()
ซึ่งก็ใช้ได้ แต่การโต้ตอบแต่ละครั้งจะกำหนดเวลาให้ทำงานมากขึ้น แม้ว่าจะมีปฏิสัมพันธ์ใหม่ๆ เข้ามาและอาจเปลี่ยนงานที่ต้องทำก็ตาม
ด้วยกลยุทธ์การดีบาวซ์ เราจึงยกเลิกการหมดเวลาก่อนหน้าทุกครั้งที่มีการโต้ตอบใหม่ เราทำอะไรที่คล้ายกันนี้ได้ไหม วิธีหนึ่งในการทำเช่นนี้คือการใช้AbortController()
ดูโค้ดฉบับเต็ม: aborty.html
// Polyfill for scheduler.yield()
async function schedulerDotYield() {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
async function blockInPiecesYieldyAborty(ms, signal) {
const parts = ms / 10;
for (let i = 0; i < parts; i++) {
// If AbortController has been asked to stop, abandon the current loop.
if (signal.aborted) return;
await schedulerDotYield();
blockFor(10);
}
}
let abortController = new AbortController();
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
abortController.abort();
abortController = new AbortController();
await blockInPiecesYieldyAborty(1000, abortController.signal);
});
เมื่อมีการคลิก ระบบจะเริ่มลูป blockInPiecesYieldyAborty
for
เพื่อทำงานที่ต้องทำพร้อมกับส่งต่อเธรดหลักเป็นระยะๆ เพื่อให้เบราว์เซอร์ยังคงตอบสนองต่อการโต้ตอบใหม่ๆ ได้
เมื่อมีการคลิกครั้งที่ 2 ระบบจะแจ้งว่าลูปแรกถูกยกเลิกด้วย AbortController
และจะเริ่มลูป blockInPiecesYieldyAborty
ใหม่ ในครั้งถัดไปที่กำหนดเวลาให้ลูปแรกทำงานอีกครั้ง ระบบจะสังเกตเห็นว่า signal.aborted
กลายเป็น true
แล้ว และจะกลับทันทีโดยไม่ต้องดำเนินการใดๆ เพิ่มเติม
18 บทสรุป
การแบ่งงานที่ใช้เวลานานทั้งหมดจะช่วยให้เว็บไซต์ตอบสนองต่อการโต้ตอบใหม่ๆ ได้ ซึ่งช่วยให้คุณแสดงความคิดเห็นเบื้องต้นได้อย่างรวดเร็ว และยังช่วยให้คุณตัดสินใจได้ เช่น การยกเลิกงานที่กำลังดำเนินการอยู่ ซึ่งบางครั้งอาจหมายถึงการกำหนดเวลาจุดแรกเข้าเป็นงานแยกต่างหาก ซึ่งบางครั้งก็หมายถึงการเพิ่มจุด "ผลตอบแทน" ในที่ที่สะดวก
โปรดทราบ
- INP วัดการโต้ตอบทั้งหมด
- การโต้ตอบแต่ละครั้งจะวัดจากอินพุตไปยัง Next Paint ซึ่งเป็นวิธีที่ผู้ใช้เห็นการตอบสนอง
- ความล่าช้าของอินพุต ระยะเวลาการประมวลผลเหตุการณ์ และความล่าช้าในการนำเสนอทั้งหมดส่งผลต่อการตอบสนองต่อการโต้ตอบ
- คุณวัด INP และรายละเอียดการโต้ตอบด้วยเครื่องมือสำหรับนักพัฒนาเว็บได้อย่างง่ายดาย
กลยุทธ์
- อย่ามีโค้ดที่ทำงานนาน (งานที่ใช้เวลานาน) ในหน้าเว็บ
- ย้ายโค้ดที่ไม่จำเป็นออกจาก Listener เหตุการณ์จนกว่าจะมีการแสดงผลครั้งถัดไป
- ตรวจสอบว่าการอัปเดตการแสดงผลมีประสิทธิภาพสำหรับเบราว์เซอร์