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

Mariko Kosaka

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

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

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

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

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

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

คอมโพสิตรับเหตุการณ์อินพุต

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

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

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

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

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

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

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

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

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

ภูมิภาคที่เลื่อนได้แบบไม่เร็วทั้งหน้า
รูปที่ 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: ชุดข้อความหลักที่ดูระเบียน Paint ถามว่าสิ่งที่วาดบนจุด x.y

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

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

ในโพสต์ก่อนหน้า เราได้พูดถึงวิธีที่จอแสดงผลทั่วไปรีเฟรชหน้าจอ 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