เจาะลึกเว็บเบราว์เซอร์สมัยใหม่ (ตอนที่ 4)

Mariko Kosaka

ข้อมูลเข้ากำลังส่งไปยัง Compositor

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

เหตุการณ์อินพุตจากมุมมองของเบราว์เซอร์

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

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

เหตุการณ์การป้อนข้อมูล
รูปที่ 1: เหตุการณ์อินพุตที่ส่งผ่านกระบวนการเบราว์เซอร์ไปยังกระบวนการแสดงผล

Compositor ได้รับเหตุการณ์การป้อนข้อมูล

รูปที่ 2: วิวพอร์ตที่วางเหนือเลเยอร์หน้าเว็บ

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

ทำความเข้าใจภูมิภาคที่เลื่อนได้แบบช้า

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

ภูมิภาคที่เลื่อนได้ช้า
รูปที่ 3: แผนภาพอินพุตที่อธิบายไปยังภูมิภาคที่เลื่อนแบบเร็วไม่ได้

โปรดระมัดระวังเมื่อเขียนเครื่องจัดการเหตุการณ์

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

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault();
    }
});

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

ภูมิภาคที่เลื่อนได้แบบไม่เร็วทั้งหน้า
รูปที่ 4: แผนภาพอินพุตที่อธิบายไปยังภูมิภาคที่เลื่อนไม่ได้อย่างรวดเร็วซึ่งครอบคลุมทั้งหน้า

คุณสามารถส่งตัวเลือก passive: true ใน event listener เพื่อลดปัญหานี้ ซึ่งบอกเป็นนัยแก่เบราว์เซอร์ว่าคุณยังคงต้องการฟังเหตุการณ์ในเธรดหลัก แต่คอมโพสิตสามารถดำเนินการต่อและคอมโพสเฟรมใหม่ได้เช่นกัน

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});

ตรวจสอบว่ายกเลิกเหตุการณ์ได้หรือไม่

การเลื่อนหน้าเว็บ
รูปที่ 5: หน้าเว็บที่มีบางส่วนของหน้าตรึงไว้ให้เลื่อนในแนวนอน

สมมติว่าคุณมีกล่องในหน้าเว็บที่ต้องการจำกัดทิศทางการเลื่อนให้เป็นการเลื่อนแนวนอนเท่านั้น

การใช้ตัวเลือก passive: true ในเหตุการณ์เคอร์เซอร์หมายความว่าการเลื่อนหน้าเว็บจะราบรื่น แต่การเลื่อนแนวตั้งอาจเริ่มต้นขึ้นเมื่อคุณต้องการ preventDefault เพื่อจำกัดทิศทางการเลื่อน คุณสามารถตรวจสอบข้อมูลนี้โดยใช้วิธีการ event.cancelable

document.body.addEventListener('pointermove', event => {
    if (event.cancelable) {
        event.preventDefault(); // block the native scroll
        /*
        *  do what you want the application to do here
        */
    }
}, {passive: true});

หรือจะใช้กฎ CSS เช่น touch-action เพื่อนำตัวแฮนเดิลเหตุการณ์ออกทั้งหมดก็ได้

#area {
  touch-action: pan-x;
}

การค้นหาเป้าหมายของเหตุการณ์

การทดสอบตัวชี้
รูปที่ 6: เทรดหลักที่มองไปที่บันทึกการวาดเพื่อถามว่ามีการวาดอะไรในจุด x.y

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

การลดการส่งเหตุการณ์ไปยังเธรดหลัก

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

หากมีการส่งเหตุการณ์ต่อเนื่อง เช่น touchmove ไปยังเธรดหลัก 120 ครั้งต่อวินาที ก็อาจทริกเกอร์การทดสอบ Hit และการเรียกใช้ JavaScript มากเกินไปเมื่อเทียบกับความช้าในการรีเฟรชหน้าจอ

เหตุการณ์ที่ไม่มีการกรอง
รูปที่ 7: เหตุการณ์ที่ท่วมไทม์ไลน์เฟรมทําให้หน้าเว็บกระตุก

Chrome จะรวมเหตุการณ์ต่อเนื่อง (เช่น wheel, mousewheel, mousemove, pointermove, touchmove) และเลื่อนการส่งออกจนกว่าจะถึงเวลาก่อน requestAnimationFrame ถัดไป เพื่อลดการเรียกใช้เธรดหลักที่มากเกินไป

เหตุการณ์ที่รวม
รูปที่ 8: ไทม์ไลน์เดียวกันกับก่อนหน้านี้ แต่เหตุการณ์รวมกันและล่าช้า

ระบบจะส่งเหตุการณ์แบบไม่ต่อเนื่อง เช่น keydown, keyup, mouseup, mousedown, touchstart และ touchend ทันที

ใช้ getCoalescedEvents เพื่อรับเหตุการณ์ภายในเฟรม

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

getCoalescedEvents
รูปที่ 9: เส้นทางของท่าทางสัมผัสที่ราบรื่นทางด้านซ้าย และเส้นทางแบบจำกัดที่รวมกันทางด้านขวา
window.addEventListener('pointermove', event => {
    const events = event.getCoalescedEvents();
    for (let event of events) {
        const x = event.pageX;
        const y = event.pageY;
        // draw a line using x and y coordinates.
    }
});

ขั้นตอนถัดไป

ในซีรีส์นี้ เราได้พูดถึงวิธีการทำงานของเว็บเบราว์เซอร์ หากคุณไม่เคยสงสัยว่าเหตุใดเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์จึงแนะนำให้เพิ่ม {passive: true} ในตัวแฮนเดิลเหตุการณ์ หรือเหตุใดคุณจึงควรเขียนแอตทริบิวต์ async ในแท็กสคริปต์ เราหวังว่าชุดบทความนี้จะอธิบายเหตุผลที่เบราว์เซอร์ต้องใช้ข้อมูลเหล่านั้นเพื่อให้ประสบการณ์การใช้งานเว็บรวดเร็วและราบรื่นยิ่งขึ้น

ใช้ Lighthouse

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

ดูวิธีวัดประสิทธิภาพ

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

เพิ่มนโยบายฟีเจอร์ลงในเว็บไซต์

หากต้องการดำเนินการเพิ่มเติม นโยบายฟีเจอร์คือฟีเจอร์ใหม่ของแพลตฟอร์มเว็บที่จะช่วยควบคุมคุณเมื่อสร้างโปรเจ็กต์ การเปิดใช้นโยบายฟีเจอร์เป็นการรับประกันลักษณะการทํางานบางอย่างของแอปและป้องกันไม่ให้คุณทําผิดพลาด เช่น หากต้องการให้แอปไม่บล็อกการแยกวิเคราะห์ คุณสามารถเรียกใช้แอปในนโยบายสคริปต์แบบซิงค์ เมื่อเปิดใช้ sync-script: 'none' ระบบจะป้องกันไม่ให้ JavaScript ที่บล็อกโปรแกรมวิเคราะห์ทำงาน วิธีนี้จะช่วยป้องกันไม่ให้โค้ดบล็อกโปรแกรมแยกวิเคราะห์ และเบราว์เซอร์ไม่ต้องกังวลเกี่ยวกับการหยุดโปรแกรมแยกวิเคราะห์ชั่วคราว

สรุป

ขอบคุณ

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

ขอขอบคุณอย่างยิ่งทุกคนที่ได้อ่านฉบับร่างต้นๆ ของชุดนี้ ซึ่งรวมถึง (แต่ไม่จำกัดเพียง) Alex Russell, Paul Irish, Meggin Kearney, Eric Bidelman, Mathias Bynens, Addy Osmani, Kinuko Yasuda, Nasko Oskov และ Charlie Reis

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