1. บทนำ
การสาธิตแบบอินเทอร์แอกทีฟและ Codelab สำหรับดูข้อมูลเกี่ยวกับการโต้ตอบกับ Next Paint (INP)
ข้อกำหนดเบื้องต้น
- ความรู้เกี่ยวกับการพัฒนา HTML และ JavaScript
- แนะนำ: อ่านเอกสารประกอบเกี่ยวกับ IP
สิ่งที่ได้เรียนรู้
- ความสัมพันธ์ระหว่างการโต้ตอบของผู้ใช้และการจัดการการโต้ตอบเหล่านั้นส่งผลต่อการตอบสนองของหน้าเว็บอย่างไร
- วิธีลดและขจัดความล่าช้าเพื่อประสบการณ์ของผู้ใช้ที่ราบรื่น
สิ่งที่ต้องมี
- คอมพิวเตอร์ที่สามารถโคลนโค้ดจาก 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 และ Interaction จะเปลี่ยนไปตามการเพิ่มแต่ละรายการไหม
INP วัดระยะเวลาที่ใช้นับตั้งแต่ผู้ใช้โต้ตอบจนกระทั่งหน้าเว็บแสดงการอัปเดตที่แสดงผลต่อผู้ใช้จริงๆ
3. การวัดการโต้ตอบกับเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome
เปิดเครื่องมือสำหรับนักพัฒนาเว็บจากเครื่องมือเพิ่มเติม > เมนูเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์โดยคลิกขวาที่หน้าเว็บ แล้วเลือกตรวจสอบ หรือโดยใช้แป้นพิมพ์ลัด
เปลี่ยนไปใช้แผงประสิทธิภาพที่คุณจะใช้วัดการโต้ตอบ
จากนั้น ให้บันทึกการโต้ตอบในแผงประสิทธิภาพ
- กด "บันทึก"
- โต้ตอบกับหน้าเว็บ (กดปุ่มเพิ่มขึ้น)
- หยุดการบันทึก
คุณจะเห็นแทร็กการโต้ตอบในไทม์ไลน์ที่เกิดขึ้น ขยายรูปสามเหลี่ยมที่อยู่ทางด้านซ้าย
การโต้ตอบ 2 อย่างจะปรากฏขึ้น ซูมเข้าไปยังปุ่มที่ 2 โดยเลื่อนหรือกดปุ่ม W ค้างไว้
เมื่อวางเมาส์เหนือการโต้ตอบ คุณจะเห็นว่าการโต้ตอบรวดเร็ว ไม่ใช้เวลาไปกับระยะเวลาการประมวลผล และระยะเวลาขั้นต่ำในการหน่วงเวลาอินพุตและการหน่วงเวลาการนำเสนอ โดยความยาวที่แน่นอนจะขึ้นอยู่กับความเร็วของเครื่อง
4. Listener เหตุการณ์ที่ยาวนาน
เปิดไฟล์ index.js
และยกเลิกการแสดงความคิดเห็นฟังก์ชัน blockFor
ใน Listener เหตุการณ์
ดูโค้ดแบบเต็ม: click_block.html
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
});
บันทึกไฟล์ เซิร์ฟเวอร์จะเห็นการเปลี่ยนแปลงและรีเฟรชหน้าเว็บให้คุณ
ลองโต้ตอบกับหน้าเว็บอีกครั้ง ตอนนี้การโต้ตอบจะช้าลงอย่างเห็นได้ชัด
การติดตามประสิทธิภาพ
บันทึกอีกครั้งในแผงประสิทธิภาพเพื่อดูว่าวิดีโอมีลักษณะเป็นอย่างไร
การโต้ตอบสั้นๆ ที่ครั้งหนึ่งเคยเป็นการโต้ตอบจะใช้เวลาเพียงครู่เดียว
เมื่อวางเมาส์เหนือการโต้ตอบ คุณจะสังเกตเห็นว่าเวลาเกือบทั้งหมดที่ใช้ใน "ระยะเวลาการประมวลผล" ซึ่งเป็นระยะเวลาที่ใช้ในการเรียกใช้ Callback ของ Listener เหตุการณ์ เนื่องจากการเรียก blockFor
ที่บล็อกไว้ทั้งหมดอยู่ใน Listener เหตุการณ์ ถึงเวลาแล้ว
5. การทดสอบ: ระยะเวลาในการประมวลผล
ลองหาวิธีจัดเรียงงานของผู้ฟังเหตุการณ์ใหม่เพื่อดูผลกระทบต่อ INP
อัปเดต UI ก่อน
จะเกิดอะไรขึ้นหากคุณสลับลำดับการเรียก JavaScript ให้อัปเดต 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 วินาทีจึงจะเสร็จสมบูรณ์
การติดตามประสิทธิภาพ: แยก Listener
ดูโค้ดแบบเต็ม: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
ขอย้ำอีกครั้งว่าฟังก์ชันการทํางานไม่ได้แตกต่างกัน การโต้ตอบยังคงใช้เวลาเต็มวินาที
หากซูมเข้าไปที่การโต้ตอบการคลิก คุณจะเห็นว่ามีการเรียกฟังก์ชันที่ต่างกัน 2 ฟังก์ชันอันเป็นผลมาจากเหตุการณ์ click
ตามที่คาดไว้ คุณลักษณะแรกที่อัปเดต UI จะทำงานอย่างรวดเร็วอย่างไม่น่าเชื่อ ส่วนที่สองจะใช้เวลาแบบเต็มวินาที แต่เมื่อรวมผลแล้ว ผู้ใช้ก็จะมีการโต้ตอบช้าเช่นเดียวกัน
การติดตามประสิทธิภาพ: เหตุการณ์ประเภทต่างๆ
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
ผลการค้นหาเหล่านี้คล้ายกันมาก การโต้ตอบยังคงเป็นวินาทีเต็ม ความแตกต่างเพียงอย่างเดียวคือตอนนี้ Listener click
ที่อัปเดตเฉพาะ UI ที่สั้นกว่านี้จะทำงานหลังจาก Listener pointerup
ที่บล็อก
การติดตามประสิทธิภาพ: ไม่มีการอัปเดต UI
ดูโค้ดแบบเต็ม: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
- คะแนนไม่อัปเดต แต่หน้าเว็บยังคงอัปเดตอยู่
- ภาพเคลื่อนไหว, เอฟเฟกต์ CSS, การดำเนินการเริ่มต้นของคอมโพเนนต์เว็บ (การป้อนแบบฟอร์ม), การป้อนข้อความ และข้อความที่ไฮไลต์จะอัปเดตต่อไป
ในกรณีนี้ ปุ่มจะมีสถานะเป็นทำงานอยู่และย้อนกลับเมื่อคลิก ซึ่งจำเป็นต้องมีการแสดงผลโดยเบราว์เซอร์ ซึ่งหมายความว่ายังคงมี INP อยู่
เนื่องจาก Listener เหตุการณ์บล็อกเทรดหลักเป็นเวลา 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
กำลังทำงานอยู่จะส่งผลให้เกิดการโต้ตอบระยะยาว แม้ว่าจะไม่มีการบล็อกในการโต้ตอบดังกล่าวก็ตาม
ระยะเวลาที่ยาวนานเหล่านี้มักเรียกว่างานที่ใช้เวลานาน
เมื่อวางเมาส์เหนือการโต้ตอบในเครื่องมือสำหรับนักพัฒนาเว็บ คุณจะเห็นว่าเวลาการโต้ตอบส่วนใหญ่เกิดจากความล่าช้าของอินพุต ไม่ใช่ระยะเวลาการประมวลผล
โปรดทราบว่าการตั้งค่านี้ไม่ได้ส่งผลต่อการโต้ตอบเสมอไป ถ้าคุณไม่คลิกเมื่องานดำเนินอยู่ คุณอาจโชคดีได้ "สุ่ม" อย่างนี้ การจามอาจฝันร้ายมากในการแก้ไขข้อบกพร่อง ทั้งๆ ที่การจามก่อให้เกิดปัญหาเพียงบางครั้ง
วิธีหนึ่งที่จะแก้ปัญหาเหล่านี้ได้คือการวัดงานที่ใช้เวลานาน (หรือเฟรมภาพเคลื่อนไหวขนาดยาว) และเวลาในการบล็อกทั้งหมด
9. การนำเสนอช้า
ที่ผ่านมา เราได้ดูประสิทธิภาพของ JavaScript ผ่านการเลื่อนอินพุตหรือ Listener เหตุการณ์แล้ว แต่ยังมีสิ่งใดอีกที่มีผลต่อการแสดงผล Next Paint
เอาล่ะ อัปเดตหน้าเว็บด้วยเอฟเฟ็กต์ราคาแพง
แม้ว่าการอัปเดตหน้าจะมาอย่างรวดเร็ว แต่เบราว์เซอร์อาจยังคงต้องทำงานหนักเพื่อแสดงผล
ในชุดข้อความหลัก ให้ทำดังนี้
- เฟรมเวิร์ก UI ที่จำเป็นต้องแสดงผลการอัปเดตหลังการเปลี่ยนแปลงสถานะ
- การเปลี่ยนแปลง DOM หรือการสลับตัวเลือกคำค้นหา CSS ราคาแพงจำนวนมากสามารถทำให้เกิดรูปแบบ เลย์เอาต์ และสี ได้มากมาย
อยู่นอกชุดข้อความหลัก:
- การใช้ CSS เพื่อขับเคลื่อนเอฟเฟกต์ GPU
- เพิ่มรูปภาพความละเอียดสูงที่มีขนาดใหญ่มาก
- ใช้ SVG/Canvas เพื่อวาดฉากที่ซับซ้อน
ตัวอย่างบางส่วนที่พบได้ทั่วไปบนเว็บมีดังนี้
- เว็บไซต์ 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 วินาทีเต็ม
อย่างไรก็ตาม มี 2 อย่างที่ควรทราบ ดังนี้
- เมื่อวางเมาส์เหนือ คุณจะเห็นเวลาที่ใช้ในการโต้ตอบทั้งหมดอยู่ใน "การหน่วงเวลาการนำเสนอ" เนื่องจากการบล็อกเทรดหลักจะเกิดขึ้นหลังจาก Listener เหตุการณ์กลับมา
- รูทของกิจกรรมเทรดหลักไม่ใช่เหตุการณ์การคลิกอีกต่อไป แต่เป็น "ภาพเคลื่อนไหวเฟรมเริ่มทำงาน"
12. กำลังวิเคราะห์การโต้ตอบ
ในหน้าทดสอบนี้ การตอบกลับจะแสดงให้เห็นอย่างชัดเจน ด้วยคะแนน ตัวจับเวลา และ UI ตัวนับ... แต่เมื่อทดสอบหน้าทั่วไปจะดูได้ลึกกว่า
เมื่อการโต้ตอบเกิดขึ้นเป็นเวลานาน เรามักจะไม่ทราบแน่ชัดว่าสาเหตุเกิดจากอะไร ใช่: ไหม
- ความล่าช้าของอินพุต
- ระยะเวลาการประมวลผลเหตุการณ์
- งานนำเสนอล่าช้าใช่ไหม
คุณจะใช้เครื่องมือสำหรับนักพัฒนาเว็บวัดการตอบสนองได้ในหน้าเว็บที่ต้องการ ลองทำตามขั้นตอนนี้เพื่อสร้างนิสัย
- ท่องเว็บตามปกติ
- ไม่บังคับ: เปิดคอนโซลเครื่องมือสำหรับนักพัฒนาเว็บทิ้งไว้ขณะที่ส่วนขยาย Web Vitals บันทึกการโต้ตอบ
- หากพบการโต้ตอบที่มีประสิทธิภาพต่ำ ให้ลองทำซ้ำตามวิธีต่อไปนี้
- หากทำซ้ำไม่ได้ ให้ใช้บันทึกของคอนโซลเพื่อรับข้อมูลเชิงลึก
- หากทำซ้ำได้ ให้บันทึกในแผงประสิทธิภาพ
ความล่าช้าทั้งหมด
ลองเพิ่มปัญหาเหล่านี้ในหน้าเว็บสักเล็กน้อย
ดูโค้ดแบบเต็ม: 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);
});
การโต้ตอบนี้สั้นเนื่องจากเทรดหลักจะพร้อมใช้งานทันทีหลังจากอัปเดต 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 วินาทีอยู่ในคิว เพื่อให้แน่ใจว่าเทรดหลักถูกบล็อกเป็นเวลานาน
เมื่องานที่ใช้เวลานานเหล่านั้นทับซ้อนกับการคลิกใหม่ที่เข้ามาใหม่ ก็จะส่งผลให้มีการโต้ตอบช้าแม้ว่า 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 เหตุการณ์จนกว่าจะแสดงผลครั้งถัดไป
- ตรวจสอบว่าการอัปเดตการแสดงผลสำหรับเบราว์เซอร์มีประสิทธิภาพดี