ความซับซ้อนของตัวเลื่อนได้ไม่รู้จบ

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

ตัวเลื่อนได้ไม่รู้จบปรากฏขึ้นทั่วอินเทอร์เน็ต รายชื่อศิลปินใน Google Music มีอยู่ หนึ่ง ไทม์ไลน์ของ Facebook ก็เป็นหนึ่งในฟีดสดของ Twitter เช่นกัน เมื่อเลื่อนลงมาด้านล่างแล้ว เนื้อหาใหม่จะปรากฎขึ้นมาอย่างน่าอัศจรรย์ ผู้ใช้จะได้รับประสบการณ์ที่ราบรื่นและเห็นการอุทธรณ์ได้ง่าย

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

สิ่งที่ใช่TM

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

เราจะใช้เทคนิค 3 อย่างเพื่อให้บรรลุเป้าหมาย ได้แก่ การรีไซเคิล DOM, ป้ายหลุมศพ และการยึดตำแหน่งการเลื่อน

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

ภาพหน้าจอของแอป Chat

การรีไซเคิล DOM

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

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

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

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

Tombstone

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

หลุมฝังศพดังกล่าว หินมาก ว้าว

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

การตรึงการเลื่อน

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

เลื่อนแผนภาพการตรึง

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

เลย์เอาต์

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

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

ปรับแต่งเลือดออกนอกขอบ

เมื่อเร็วๆ นี้ Chrome ได้เพิ่มการรองรับสำหรับ CSS Containment ซึ่งเป็นฟีเจอร์ที่เปิดโอกาสให้นักพัฒนาซอฟต์แวร์บอกเบราว์เซอร์ได้ว่าองค์ประกอบเป็นขอบเขตของการออกแบบและระบายสี เนื่องจากเรากำลังทำเลย์เอาต์ที่นี่ จึงเป็นแอปพลิเคชันหลัก สำหรับการยับยั้งชั่งใจ เมื่อใดก็ตามที่เราเพิ่มองค์ประกอบบนรันเวย์ เราจะรู้ว่ารายการอื่นๆ ไม่จำเป็นต้องได้รับผลกระทบจากการส่งต่อ ดังนั้นแต่ละรายการ ควรมี contain: layout นอกจากนี้ เราไม่ต้องการมีผลกระทบกับส่วนอื่นๆ ของเว็บไซต์ ดังนั้นตัวรันเวย์ก็ควรจะได้รับคำสั่งรูปแบบนี้เช่นกัน

อีกสิ่งหนึ่งที่เราพิจารณาคือการใช้ IntersectionObservers เป็นกลไกในการตรวจจับเวลาที่ผู้ใช้เลื่อนมาไกลพอที่เราจะเริ่มรีไซเคิลองค์ประกอบและโหลดข้อมูลใหม่ได้ อย่างไรก็ตาม IntersectionObservers ได้รับการระบุว่าใช้เวลาในการตอบสนองสูง (เหมือนกับว่าใช้ requestIdleCallback) เราจึงอาจรู้สึกตอบสนองกับ IntersectionObservers ได้น้อยกว่าเมื่อไม่พูด แม้แต่การติดตั้งใช้งานปัจจุบันที่ใช้เหตุการณ์ scroll ก็ยังพบปัญหานี้อยู่ เนื่องจากระบบจะส่งเหตุการณ์การเลื่อนอย่าง "ดีที่สุด" ในที่สุดแล้ว Houdini’s Compositor Worklet ก็น่าจะเป็นวิธีแก้ปัญหาที่มีความแม่นยำสูงสำหรับปัญหานี้

ยังไม่สมบูรณ์แบบ

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

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

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