การกำหนดเวลา JS ที่ดีขึ้นด้วย isInputPending()

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

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

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

Facebook จึงเสนอและติดตั้งใช้งาน isInputPending() API ใน Chromium เพื่อปรับปรุงการตอบสนองโดยไม่สูญเสียประสิทธิภาพ เราได้ทำอัปเดต API หลายรายการตามความคิดเห็นที่ได้รับจากช่วงทดลองใช้จากต้นทาง และยินดีที่จะแจ้งให้ทราบว่าตอนนี้ API พร้อมใช้งานโดยค่าเริ่มต้นใน Chromium 87 แล้ว

ความเข้ากันได้กับเบราว์เซอร์

การรองรับเบราว์เซอร์

  • Chrome: 87
  • Edge: 87
  • Firefox: ไม่รองรับ
  • Safari: ไม่รองรับ

แหล่งที่มา

isInputPending() มาพร้อมกับเบราว์เซอร์ที่ใช้ Chromium ตั้งแต่เวอร์ชัน 87 เป็นต้นไป ไม่มีเบราว์เซอร์อื่นที่ส่งสัญญาณความตั้งใจที่จะเปิดตัว API

ข้อมูลเบื้องต้น

งานส่วนใหญ่ในระบบนิเวศ JS ในปัจจุบันจะทําในเธรดเดียวคือเธรดหลัก การดำเนินการนี้จะมอบรูปแบบการดำเนินการที่มีประสิทธิภาพให้แก่นักพัฒนาซอฟต์แวร์ แต่ประสบการณ์ของผู้ใช้ (โดยเฉพาะการตอบสนอง) อาจแย่ลงอย่างมากหากสคริปต์ทำงานเป็นเวลานาน หากหน้าเว็บทํางานหลายอย่างขณะที่เหตุการณ์อินพุตเริ่มทำงาน เช่น หน้าเว็บจะไม่จัดการเหตุการณ์อินพุตการคลิกจนกว่างานดังกล่าวจะเสร็จสมบูรณ์

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

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

แผนภาพที่แสดงให้เห็นว่าเมื่อคุณเรียกใช้งาน JS ที่ใช้เวลานาน เบราว์เซอร์จะมีเวลาน้อยลงในการกระจายเหตุการณ์

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

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

เนื่องจากมีความสนใจใน API นี้ เราจึงร่วมมือกับเพื่อนร่วมงานที่ Chrome เพื่อติดตั้งใช้งานและเปิดตัวฟีเจอร์นี้ใน Chromium เราได้ติดตั้งแพตช์ไว้หลังช่วงทดลองใช้จากต้นทาง (ซึ่งเป็นวิธีที่ Chrome ใช้ทดสอบการเปลี่ยนแปลงและรับความคิดเห็นจากนักพัฒนาซอฟต์แวร์ก่อนที่จะเปิดตัว API อย่างเต็มรูปแบบ) ด้วยความความช่วยเหลือจากวิศวกรของ Chrome

ตอนนี้เราได้นำความคิดเห็นจากการทดลองใช้ต้นทางและจากสมาชิกคนอื่นๆ ของกลุ่มทํางานด้านประสิทธิภาพเว็บของ W3C มาปรับใช้กับ API แล้ว

ตัวอย่างเช่น ตัวจัดตารางเวลาของ Yieldier

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

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (performance.now() >= DEADLINE) {
    // Yield the event loop if we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

การเรียกใช้ processWorkQueue() ในภายหลังในมาโครแทสก์ใหม่ผ่าน setTimeout() จะช่วยให้เบราว์เซอร์ยังคงตอบสนองต่ออินพุตได้อยู่บ้าง (สามารถเรียกใช้ตัวแฮนเดิลเหตุการณ์ได้ก่อนที่จะทํางานต่อ) ขณะเดียวกันก็ยังคงทํางานได้อย่างต่อเนื่อง อย่างไรก็ตาม เราอาจถูกยกเลิกกำหนดการเป็นเวลานานเนื่องจากงานอื่นๆ ที่ต้องการให้ควบคุมลูปเหตุการณ์ หรืออาจเพิ่มเวลาในการตอบสนองของเหตุการณ์ได้สูงสุด QUANTUM มิลลิวินาที

คุณภาพนี้โอเค แต่เราทำได้ดีกว่านี้ไหม แน่นอน!

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event, or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

การใช้การเรียก navigator.scheduling.isInputPending() ช่วยให้เราสามารถตอบสนองต่ออินพุตได้เร็วขึ้น ในขณะเดียวกันก็ยังคงทำงานบล็อกการแสดงผลได้อย่างต่อเนื่อง หากเราไม่สนใจที่จะจัดการสิ่งใดนอกจากอินพุต (เช่น การวาดภาพ) จนกว่างานจะเสร็จสมบูรณ์ เราก็สามารถเพิ่มความยาวของ QUANTUM ได้อย่างง่ายดายเช่นกัน

โดยค่าเริ่มต้น ระบบจะไม่แสดงเหตุการณ์ "ต่อเนื่อง" จาก isInputPending() ซึ่งรวมถึง mousemove, pointermove และอื่นๆ หากสนใจที่จะให้ผลตอบแทนสำหรับรายการเหล่านี้ด้วย ก็ไม่เป็นปัญหา เพียงระบุออบเจ็กต์ให้กับ isInputPending() โดยตั้งค่า includeContinuous เป็น true เราก็พร้อมใช้งาน

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

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

การยอมแพ้ไม่ได้แย่เสมอไป

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

อาจมีกรณีที่เบราว์เซอร์ระบุแหล่งที่มาของเหตุการณ์อินพุตที่รอดำเนินการอย่างไม่ถูกต้อง โดยเฉพาะอย่างยิ่ง การตั้งค่าคลิปและมาสก์ที่ซับซ้อนสําหรับ iframe จากแหล่งที่มาต่างๆ อาจรายงานผลเป็นลบที่ไม่ถูกต้อง (นั่นคือ isInputPending() อาจแสดงผลเป็นเท็จโดยไม่คาดคิดเมื่อกําหนดเป้าหมายเฟรมเหล่านี้) ตรวจสอบว่าคุณให้ผลลัพธ์บ่อยพอหากเว็บไซต์ต้องมีการโต้ตอบกับเฟรมย่อยที่มีสไตล์

โปรดคำนึงถึงหน้าอื่นๆ ที่แชร์ลูปเหตุการณ์ด้วย ในแพลตฟอร์มต่างๆ เช่น Chrome สําหรับ Android การที่ต้นทางหลายแห่งใช้ Event Loop ร่วมกันนั้นเป็นเรื่องปกติ isInputPending() จะไม่แสดงผลเป็น true เลยหากมีการส่งอินพุตไปยังเฟรมข้ามแหล่งที่มา และหน้าเว็บที่ทำงานอยู่เบื้องหลังอาจรบกวนการตอบสนองของหน้าเว็บที่ทำงานอยู่เบื้องหน้า คุณอาจต้องลด เลื่อนเวลา หรือลดเวลาทำงานบ่อยขึ้นเมื่อทํางานในเบื้องหลังโดยใช้ Page Visibility API

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

ความคิดเห็น

  • แสดงความคิดเห็นเกี่ยวกับข้อกำหนดในที่เก็บข้อมูล is-input-pending
  • ติดต่อ @acomminos (หนึ่งในผู้เขียนข้อกำหนด) ทาง Twitter

บทสรุป

เรายินดีที่ isInputPending() เปิดตัวแล้ว และนักพัฒนาแอปเริ่มใช้งานได้ในวันนี้ API นี้เป็น API บนเว็บใหม่ที่ Facebook สร้างขึ้นเป็นครั้งแรก และนำแนวคิดนี้ไปพัฒนาต่อจากการสร้างต้นแบบเป็นข้อเสนอมาตรฐานเพื่อนำไปใช้งานจริงในเบราว์เซอร์ ขอขอบคุณทุกคนที่ช่วยให้เราได้มาถึงจุดนี้ และขอขอบคุณเป็นพิเศษสำหรับทุกคนในทีม Chrome ที่ช่วยเราพัฒนาไอเดียนี้และทำให้พร้อมใช้งาน

รูปภาพหลักโดย Will H McMahan จาก Unsplash