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

ทำความเข้าใจ Interaction to Next Paint (INP)

เกี่ยวกับ Codelab นี้

subjectอัปเดตล่าสุดเมื่อ ม.ค. 9, 2025
account_circleเขียนโดย Michal Mocny, Brendan Kenny

1 บทนำ

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

แผนภาพที่แสดงการโต้ตอบในเทรดหลัก ผู้ใช้ป้อนข้อมูลขณะบล็อกการเรียกใช้ Task อินพุตจะล่าช้าจนกว่างานเหล่านั้นจะเสร็จสมบูรณ์ หลังจากนั้น Listener ของเหตุการณ์ 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 ไปยัง 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 วินาทีเต็ม

การโต้ตอบที่ยาว 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 การทดลอง: งานที่ไม่พร้อมกัน

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

ตราบใดที่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);
});

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

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