ทำความเข้าใจการโต้ตอบกับ Next Paint (INP)

1. บทนำ

การสาธิตแบบอินเทอร์แอกทีฟและ Codelab สำหรับการเรียนรู้เกี่ยวกับ Interaction to Next Paint (INP)

แผนภาพที่แสดงการโต้ตอบในเทรดหลัก ผู้ใช้ป้อนข้อมูลขณะบล็อกการเรียกใช้ Task ระบบจะหน่วงเวลาอินพุตจนกว่างานเหล่านั้นจะเสร็จสมบูรณ์ หลังจากนั้นตัวแฮนเดิลเหตุการณ์ pointerup, mouseup และ click จะทำงาน จากนั้นระบบจะเริ่มงานการแสดงผลและการวาดจนกว่าจะแสดงเฟรมถัดไป

ข้อกำหนดเบื้องต้น

สิ่งที่คุณจะได้เรียนรู้

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

สิ่งที่ต้องมี

  • คอมพิวเตอร์ที่สามารถโคลนโค้ดจาก GitHub และเรียกใช้คำสั่ง npm ได้
  • เครื่องมือแก้ไขข้อความ
  • Chrome เวอร์ชันล่าสุดเพื่อให้การวัดการโต้ตอบทั้งหมดทํางานได้

2. ตั้งค่า

รับและเรียกใช้โค้ด

โค้ดนี้อยู่ในที่เก็บweb-vitals-codelabs

  1. โคลนที่เก็บในเทอร์มินัล: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. ไปยังไดเรกทอรีที่โคลน: cd web-vitals-codelabs/understanding-inp
  3. ติดตั้งการอ้างอิง: npm ci
  4. เริ่มเว็บเซิร์ฟเวอร์: npm run start
  5. ไปที่ http://localhost:5173/understanding-inp/ ในเบราว์เซอร์

ภาพรวมของแอป

ที่ด้านบนของหน้าจะมีตัวนับคะแนนและปุ่มเพิ่ม การสาธิตแบบคลาสสิกเกี่ยวกับความสามารถในการโต้ตอบและการตอบสนอง

ภาพหน้าจอของแอปเดโมสำหรับ Codelab นี้

ด้านล่างปุ่มจะมีค่าการวัด 4 รายการดังนี้

  • INP: คะแนน INP ปัจจุบัน ซึ่งโดยปกติแล้วคือการโต้ตอบที่แย่ที่สุด
  • การโต้ตอบ: คะแนนของการโต้ตอบครั้งล่าสุด
  • FPS: เฟรมต่อวินาทีของเทรดหลักของหน้าเว็บ
  • ตัวจับเวลา: ภาพเคลื่อนไหวตัวจับเวลาที่ทำงานอยู่เพื่อช่วยให้เห็นภาพความกระตุก

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

ลองเลย

ลองโต้ตอบกับปุ่มเพิ่มและดูคะแนนที่เพิ่มขึ้น ค่า INP และการโต้ตอบเปลี่ยนแปลงตามการเพิ่มแต่ละครั้งหรือไม่

INP จะวัดระยะเวลาตั้งแต่ที่ผู้ใช้โต้ตอบจนกว่าหน้าเว็บจะแสดงการอัปเดตที่แสดงผลต่อผู้ใช้จริง

3. การวัดการโต้ตอบด้วยเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome

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

เปลี่ยนไปที่แผงประสิทธิภาพ ซึ่งคุณจะใช้เพื่อวัดการโต้ตอบ

ภาพหน้าจอของแผงประสิทธิภาพของเครื่องมือสำหรับนักพัฒนาเว็บข้างแอป

จากนั้นบันทึกการโต้ตอบในแผงประสิทธิภาพ

  1. กด "บันทึก"
  2. โต้ตอบกับหน้าเว็บ (กดปุ่มเพิ่ม)
  3. หยุดการบันทึก

ในไทม์ไลน์ที่ได้ คุณจะเห็นแทร็กการโต้ตอบ ขยายโดยคลิกสามเหลี่ยมทางด้านซ้าย

การสาธิตแบบเคลื่อนไหวของการบันทึกการโต้ตอบโดยใช้แผงประสิทธิภาพของ DevTools

การโต้ตอบ 2 รายการจะปรากฏขึ้น ซูมเข้าที่รูปที่ 2 โดยการเลื่อนหรือกดปุ่ม W ค้างไว้

ภาพหน้าจอของแผงประสิทธิภาพของเครื่องมือสำหรับนักพัฒนาเว็บ เคอร์เซอร์วางเหนือการโต้ตอบในแผง และเคล็ดลับเครื่องมือที่แสดงระยะเวลาสั้นๆ ของการโต้ตอบ

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

4. Listener เหตุการณ์ที่ทำงานเป็นเวลานาน

เปิดไฟล์ index.js แล้วยกเลิกการแสดงความคิดเห็นของฟังก์ชัน blockFor ภายในเครื่องมือฟังเหตุการณ์

ดูโค้ดทั้งหมด: click_block.html

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();
});

บันทึกไฟล์ เซิร์ฟเวอร์จะเห็นการเปลี่ยนแปลงและรีเฟรชหน้าเว็บให้คุณ

ลองโต้ตอบกับหน้าเว็บอีกครั้ง ตอนนี้การโต้ตอบจะช้าลงอย่างเห็นได้ชัด

การติดตามประสิทธิภาพ

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

การโต้ตอบที่ยาว 1 วินาทีในแผงประสิทธิภาพ

การโต้ตอบที่เคยใช้เวลาเพียงเล็กน้อยตอนนี้ใช้เวลาเต็ม 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 วินาทีในการดำเนินการให้เสร็จสมบูรณ์

การโต้ตอบแบบคงที่ยาว 1 วินาทีในแผงประสิทธิภาพ

การติดตามประสิทธิภาพ: ผู้ฟังที่ไม่ซ้ำ

ดูโค้ดทั้งหมด: two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

ขอย้ำอีกครั้งว่าฟังก์ชันการทำงานนั้นไม่แตกต่างกัน การโต้ตอบยังคงใช้เวลา 1 วินาทีเต็ม

หากซูมเข้าไปที่การโต้ตอบการคลิก คุณจะเห็นว่ามีการเรียกใช้ฟังก์ชัน 2 อย่างที่แตกต่างกันจริงอันเป็นผลมาจากclickเหตุการณ์

ตามที่คาดไว้ งานแรกซึ่งเป็นการอัปเดต UI จะทำงานเร็วมาก ส่วนงานที่ 2 จะใช้เวลา 1 วินาทีเต็ม อย่างไรก็ตาม ผลรวมของผลกระทบเหล่านี้ส่งผลให้เกิดการโต้ตอบที่ช้าเหมือนกันกับผู้ใช้ปลายทาง

ดูการโต้ตอบที่ยาว 1 วินาทีในตัวอย่างนี้แบบซูมเข้า ซึ่งแสดงให้เห็นว่าการเรียกฟังก์ชันครั้งแรกใช้เวลาไม่ถึงมิลลิวินาทีจึงจะเสร็จสมบูรณ์

การติดตามประสิทธิภาพ: เหตุการณ์ประเภทต่างๆ

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

ผลลัพธ์เหล่านี้คล้ายกันมาก การโต้ตอบยังคงใช้เวลา 1 วินาทีเต็ม ข้อแตกต่างเพียงอย่างเดียวคือตอนนี้ Listener click ที่อัปเดต UI เท่านั้นซึ่งสั้นกว่าจะทำงานหลังจาก Listener pointerup ที่บล็อก

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

การติดตามประสิทธิภาพ: ไม่มีการอัปเดต UI

ดูโค้ดฉบับเต็ม: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • คะแนนจะไม่ได้รับการอัปเดต แต่หน้าเว็บจะยังคงได้รับการอัปเดต
  • ภาพเคลื่อนไหว เอฟเฟกต์ CSS การดำเนินการเริ่มต้นของคอมโพเนนต์เว็บ (อินพุตแบบฟอร์ม) การป้อนข้อความ และการไฮไลต์ข้อความจะยังคงได้รับการอัปเดตต่อไป

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

เนื่องจากตัวแฮนเดิลเหตุการณ์บล็อกเทรดหลักเป็นเวลา 1 วินาที ทำให้หน้าเว็บไม่แสดงผล การโต้ตอบจึงยังคงใช้เวลา 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งานการบล็อกกำลังทำงานจะส่งผลให้เกิดการโต้ตอบที่ใช้เวลานาน แม้ว่าจะไม่มีการบล็อกในตัวการโต้ตอบเองก็ตาม

ระยะเวลาที่ใช้เวลานานเหล่านี้มักเรียกว่างานที่ใช้เวลานาน

เมื่อวางเมาส์เหนือการโต้ตอบในเครื่องมือสำหรับนักพัฒนาเว็บ คุณจะเห็นว่าตอนนี้เวลาในการโต้ตอบส่วนใหญ่เกิดจากเวลาหน่วงของอินพุต ไม่ใช่ระยะเวลาการประมวลผล

แผงประสิทธิภาพของเครื่องมือสำหรับนักพัฒนาเว็บแสดงงานที่บล็อก 1 วินาที การโต้ตอบที่เกิดขึ้นระหว่างงานนั้น และการโต้ตอบ 642 มิลลิวินาที ซึ่งส่วนใหญ่เกิดจากความล่าช้าของอินพุต

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

วิธีหนึ่งในการติดตามปัญหาเหล่านี้คือการวัดงานที่ใช้เวลานาน (หรือเฟรมภาพเคลื่อนไหวที่ใช้เวลานาน) และเวลาที่ถูกบล็อกทั้งหมด

9. งานนำเสนอช้า

ที่ผ่านมา เราได้ดูประสิทธิภาพของ JavaScript ผ่านการหน่วงเวลาอินพุตหรือเครื่องมือฟังเหตุการณ์ แต่มีอะไรอีกบ้างที่ส่งผลต่อการแสดงผล Next Paint

การอัปเดตหน้าเว็บด้วยเอฟเฟกต์ราคาแพง

แม้ว่าการอัปเดตหน้าเว็บจะเกิดขึ้นอย่างรวดเร็ว แต่เบราว์เซอร์ก็อาจยังต้องทำงานอย่างหนักเพื่อแสดงผลหน้าเว็บ

ในเทรดหลัก

  • เฟรมเวิร์ก UI ที่ต้องแสดงผลการอัปเดตหลังจากมีการเปลี่ยนแปลงสถานะ
  • การเปลี่ยนแปลง DOM หรือการสลับตัวเลือกการค้นหา CSS ที่มีราคาแพงหลายรายการอาจทําให้เกิดการจัดรูปแบบ เลย์เอาต์ และการระบายสีจำนวนมาก

นอกเทรดหลัก

  • การใช้ CSS เพื่อขับเคลื่อนเอฟเฟกต์ GPU
  • การเพิ่มรูปภาพความละเอียดสูงขนาดใหญ่มาก
  • การใช้ SVG/Canvas เพื่อวาดฉากที่ซับซ้อน

ภาพร่างขององค์ประกอบต่างๆ ของการแสดงผลบนเว็บ

RenderingNG

ตัวอย่างที่พบบ่อยบนเว็บ

  • เว็บไซต์ SPA ที่สร้าง DOM ทั้งหมดใหม่หลังจากคลิกลิงก์ โดยไม่หยุดชั่วคราวเพื่อแสดงความคิดเห็นด้วยภาพเริ่มต้น
  • หน้าค้นหาที่มีตัวกรองการค้นหาที่ซับซ้อนพร้อมอินเทอร์เฟซผู้ใช้แบบไดนามิก แต่ต้องเรียกใช้ Listener ที่มีค่าใช้จ่ายสูงจึงจะทำได้
  • ปุ่มเปิด/ปิดโหมดมืดที่ทริกเกอร์สไตล์/เลย์เอาต์สำหรับทั้งหน้า

10. การทดสอบ: ความล่าช้าของงานนำเสนอ

requestAnimationFrame ทำงานช้า

มาจำลองการหน่วงเวลาของงานนำเสนอที่ยาวนานโดยใช้ requestAnimationFrame() API กัน

ย้ายการเรียก blockFor ไปยัง requestAnimationFrame Callback เพื่อให้ทำงานหลังจากที่ 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 วัดเวลาตั้งแต่การโต้ตอบจนถึง Next Paint blockFor(1000) ใน requestAnimationFrame จะบล็อก Next Paint ต่อไปเป็นเวลา 1 วินาทีเต็ม

การโต้ตอบแบบคงที่ยาว 1 วินาทีในแผงประสิทธิภาพ

อย่างไรก็ตาม โปรดทราบ 2 สิ่งต่อไปนี้

  • เมื่อวางเมาส์เหนือการโต้ตอบ คุณจะเห็นว่าตอนนี้เวลาในการโต้ตอบทั้งหมดใช้ไปกับ "ความล่าช้าในการนำเสนอ" เนื่องจากมีการบล็อกเทรดหลักหลังจากที่เครื่องมือฟังเหตุการณ์กลับมา
  • รูทของกิจกรรมในเทรดหลักไม่ใช่เหตุการณ์คลิกอีกต่อไป แต่เป็น "Animation Frame Fired"

12. การวินิจฉัยการโต้ตอบ

ในหน้าทดสอบนี้ การตอบสนองจะมองเห็นได้ชัดเจนมาก โดยมีคะแนน ตัวจับเวลา และ UI ตัวนับ แต่เมื่อทดสอบหน้าเว็บโดยเฉลี่ย การตอบสนองจะมองเห็นได้ยากกว่า

เมื่อการโต้ตอบใช้เวลานาน เราอาจไม่ทราบเสมอไปว่าสาเหตุเกิดจากอะไร ซึ่งอาจเป็น

  • ความล่าช้าของอินพุต
  • ระยะเวลาการประมวลผลเหตุการณ์
  • ความล่าช้าของงานนำเสนอ

คุณใช้เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome เพื่อช่วยวัดการตอบสนองได้ในหน้าใดก็ได้ที่ต้องการ หากต้องการสร้างนิสัยนี้ ให้ลองทำตามขั้นตอนต่อไปนี้

  1. ท่องเว็บตามปกติ
  2. คอยดูบันทึกการโต้ตอบในมุมมองเมตริกแบบเรียลไทม์ของแผงประสิทธิภาพในเครื่องมือสำหรับนักพัฒนาเว็บ
  3. หากเห็นการโต้ตอบที่มีประสิทธิภาพต่ำ ให้ลองทำซ้ำดังนี้
  • หากทำซ้ำไม่ได้ ให้ใช้บันทึกการโต้ตอบเพื่อรับข้อมูลเชิงลึก
  • หากทำซ้ำได้ ให้บันทึกการติดตามในแผงประสิทธิภาพ

ความล่าช้าทั้งหมด

ลองเพิ่มปัญหาเหล่านี้ลงในหน้าเว็บ

ดูโค้ดฉบับเต็ม: all_the_things.html

setInterval(() => {
  blockFor(1000);
}, 3000);

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

จากนั้นใช้แผงคอนโซลและแผงประสิทธิภาพเพื่อวินิจฉัยปัญหา

13. การทดลอง: งานที่ไม่พร้อมกัน

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

ตราบใดที่ระบบอนุญาตให้แสดงผลการแสดงผลครั้งถัดไปหลังจากการโต้ตอบ แม้ว่าเบราว์เซอร์จะตัดสินใจว่าไม่จำเป็นต้องมีการอัปเดตการแสดงผลใหม่ การวัดการโต้ตอบก็จะหยุดลง

หากต้องการลองใช้ฟีเจอร์นี้ ให้อัปเดต 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);
});

การโต้ตอบ 27 มิลลิวินาทีกับงานที่ใช้เวลา 1 วินาทีซึ่งเกิดขึ้นในภายหลังในเทรซ

ตอนนี้การโต้ตอบจึงสั้นลงเนื่องจากชุดข้อความหลักพร้อมใช้งานทันทีหลังจากที่ 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 วินาที ซึ่งจะบล็อกเทรดหลักเป็นระยะเวลาหนึ่ง

งานที่ใช้เวลานานหลายวินาทีในเทรดหลัก ทำให้การโต้ตอบช้าถึง 800 มิลลิวินาที

เมื่องานที่ใช้เวลานานเหล่านั้นทับซ้อนกับการคลิกใหม่ที่เข้ามา การโต้ตอบจะช้าแม้ว่าตัว 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 เหตุการณ์จนกว่าจะมีการแสดงผลครั้งถัดไป
  • ตรวจสอบว่าการอัปเดตการแสดงผลมีประสิทธิภาพสำหรับเบราว์เซอร์

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