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

1. บทนำ

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

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

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

สิ่งที่ได้เรียนรู้

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

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

  • คอมพิวเตอร์ที่สามารถโคลนโค้ดจาก 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 และ Interaction จะเปลี่ยนไปตามการเพิ่มแต่ละรายการไหม

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

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

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

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

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

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

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

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

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

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

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

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

4. Listener เหตุการณ์ที่ยาวนาน

เปิดไฟล์ index.js และยกเลิกการแสดงความคิดเห็นฟังก์ชัน blockFor ใน Listener เหตุการณ์

ดูโค้ดแบบเต็ม: click_block.html

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

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

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

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

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

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

การโต้ตอบสั้นๆ ที่ครั้งหนึ่งเคยเป็นการโต้ตอบจะใช้เวลาเพียงครู่เดียว

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

5. การทดสอบ: ระยะเวลาในการประมวลผล

ลองหาวิธีจัดเรียงงานของผู้ฟังเหตุการณ์ใหม่เพื่อดูผลกระทบต่อ INP

อัปเดต UI ก่อน

จะเกิดอะไรขึ้นหากคุณสลับลำดับการเรียก js ให้อัปเดต UI ก่อนแล้วจึงบล็อก

ดูโค้ดแบบเต็ม: ui_first.html

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

คุณสังเกตเห็น UI ปรากฏขึ้นก่อนหน้านี้หรือไม่ คำสั่งซื้อส่งผลต่อคะแนน INP ไหม

ลองติดตามและตรวจสอบการโต้ตอบว่ามีความแตกต่างใดๆ หรือไม่

แยก Listener

จะเกิดอะไรขึ้นหากคุณย้ายงานไปยัง Listener เหตุการณ์แยกต่างหาก อัปเดต UI ใน Listener เหตุการณ์เดียว และบล็อกหน้าเว็บจาก Listener แยกต่างหาก

ดูโค้ดแบบเต็ม: two_click.html

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

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

แผงประสิทธิภาพตอนนี้มีลักษณะอย่างไร

ประเภทเหตุการณ์ต่างๆ

การโต้ตอบส่วนใหญ่จะเรียกให้เหตุการณ์หลายประเภทเริ่มทำงาน ตั้งแต่ตัวชี้หรือเหตุการณ์สำคัญ ไปจนถึงการวางเคอร์เซอร์ โฟกัส/เบลอ และเหตุการณ์สังเคราะห์ เช่น beforechange และ beforeinput

หน้าเว็บจริงหลายหน้ามีผู้ฟังสำหรับเหตุการณ์ต่างๆ มากมาย

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

การติดตามประสิทธิภาพ: แยก Listener

ดูโค้ดแบบเต็ม: two_click.html

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

การบันทึกแผงประสิทธิภาพจะแสดงการโต้ตอบแบบเสมือนจริงกับที่เคยเกิดขึ้นก่อนหน้านี้

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

สั่งกลับบ้าน

โค้ดใดๆ ที่ทำงานใน Listener เหตุการณ์ใดก็ได้จะทำให้การโต้ตอบล่าช้า

  • ซึ่งรวมถึง 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 ผ่านการเลื่อนอินพุตหรือ Listener เหตุการณ์แล้ว แต่ยังมีสิ่งใดอีกที่มีผลต่อการแสดงผล Next Paint

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

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

ในชุดข้อความหลัก ให้ทำดังนี้

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

อยู่นอกชุดข้อความหลัก:

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

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

RenderingNG

ตัวอย่างบางส่วนที่พบได้ทั่วไปบนเว็บมีดังนี้

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

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

requestAnimationFrame ทำงานช้า

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

ย้ายการโทร blockFor ไปยัง Callback requestAnimationFrame เพื่อให้เรียกใช้หลังจาก Listener เหตุการณ์กลับมา

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

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

สิ่งที่เกิดขึ้น

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

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

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

การโต้ตอบยังคงเหลือเวลาอีกเพียงวินาทีเดียว แล้วเกิดอะไรขึ้น

requestAnimationFrame ขอให้ติดต่อกลับก่อนการแสดงผลครั้งถัดไป เนื่องจาก INP วัดเวลาจากการโต้ตอบไปจนถึงการแสดงผลถัดไป blockFor(1000) ใน requestAnimationFrame จะยังคงบล็อกการแสดงผลถัดไปเป็นเวลา 1 วินาทีเต็ม

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

อย่างไรก็ตาม มี 2 อย่างที่ควรทราบ ดังนี้

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

12. กำลังวิเคราะห์การโต้ตอบ

ในหน้าทดสอบนี้ การตอบสนองนั้นมองเห็นได้อย่างชัดเจน พร้อมกับแสดงคะแนน ตัวจับเวลา และ UI ตัวนับ... แต่เมื่อทดสอบหน้าทั่วไป จะละเอียดยิ่งขึ้น

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

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

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

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

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

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

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

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

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

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

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

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

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

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

หากต้องการลองใช้ฟีเจอร์นี้ ให้อัปเดต UI จาก Click Listener ต่อไป แต่ให้เรียกใช้การบล็อกจากระยะหมดเวลา

ดูโค้ดแบบเต็ม: ระยะหมดเวลา_100.html

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

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

เกิดอะไรขึ้นกับคำบรรยายที่ส่งไป

14. ผลการทดสอบการทำงานแบบไม่พร้อมกัน

ดูโค้ดแบบเต็ม: ระยะหมดเวลา_100.html

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

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

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

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

บทเรียน: หากนำความคิดเห็นดังกล่าวออกไม่ได้ อย่างน้อยก็ให้ย้ายตำแหน่งนั่นแหละ!

เมธอด

เราทำได้ดีกว่า setTimeout แบบคงที่ 100 มิลลิวินาทีได้ไหม เรายังต้องการให้โค้ดทำงานโดยเร็วที่สุด มิฉะนั้นเราควรนำโค้ดนั้นออก!

เป้าหมาย:

  • การโต้ตอบนี้จะเรียกใช้ incrementAndUpdateUI()
  • blockFor() จะทํางานโดยเร็วที่สุด แต่ไม่บล็อกการแสดงผลถัดไป
  • ซึ่งจะทำให้เกิดลักษณะการทำงานที่คาดการณ์ได้โดยไม่มี "ระยะหมดเวลาที่ไม่อาจถึงที่สุด"

ซึ่งวิธีดำเนินการมีดังนี้

  • setTimeout(0)
  • Promise.then()
  • requestAnimationFrame
  • requestIdleCallback
  • scheduler.postTask()

"requestPostAnimationFrame"

requestAnimationFrame + setTimeout สร้าง Polyfill แบบง่ายๆ สำหรับ requestPostAnimationFrame โดยเรียกใช้ Callback หลังจากการลงสีครั้งถัดไป ซึ่งต่างจาก requestAnimationFrame (ซึ่งจะพยายามเรียกใช้ก่อนการลงสีครั้งถัดไป

ดูโค้ดแบบเต็ม: raf+task.html

function afterNextPaint(callback) {
  requestAnimationFrame(() => {
    setTimeout(callback, 0);
  });
}

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

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

สำหรับการอ้างอิงจากหลักการยศาสตร์ คุณยังสามารถรวมข้อความเหล่านั้นเป็นคำสัญญาได้ ดังนี้

ดูโค้ดแบบเต็ม: raf+task2.html

async function nextPaint() {
  return new Promise(resolve => afterNextPaint(resolve));
}

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

  await nextPaint();
  blockFor(1000);
});

15. การโต้ตอบหลายครั้ง (และการคลิกที่รุนแรง)

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

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

ดูโค้ดแบบเต็ม: ระยะหมดเวลา_100.html

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

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

จะเกิดอะไรขึ้นหากคุณคลิกหลายครั้งอย่างรวดเร็ว

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

สำหรับแต่ละคลิก จะมีงานยาว 1 วินาทีอยู่ในคิว เพื่อให้แน่ใจว่าเทรดหลักถูกบล็อกเป็นเวลานาน

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

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

กลยุทธ์

โดยหลักการแล้ว เราต้องการนำงานที่ใช้เวลานานออกทั้งหมด

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

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

กลยุทธ์

  • อย่ามีโค้ดที่ใช้เวลานาน (งานที่ใช้เวลานาน) บนหน้าเว็บ
  • ย้ายโค้ดที่ไม่จำเป็นออกจาก Listener เหตุการณ์จนกว่าจะแสดงผลครั้งถัดไป
  • ตรวจสอบว่าการอัปเดตการแสดงผลสำหรับเบราว์เซอร์มีประสิทธิภาพดี

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