บทความ
เอฟเฟกต์สวย ๆ ที่ไม่ต้องให้เครื่องคนอ่านเป็นคนจ่าย
เพื่อน ๆ รู้มั้ยครับว่าก่อนจะเป็นเรื่องความสวย แอนิเมชันคือการตัดสินใจเรื่องประสิทธิภาพ และเว็บส่วนใหญ่ก็ปล่อยให้มือถือของผู้เข้าชมเป็นคนจ่ายบิลนั้นแทน
เผยแพร่
ลองนึกภาพตามนะครับ มีคนคนหนึ่งเปิดหน้าเว็บของบริษัทหนึ่งบนมือถือรุ่นที่ซื้อมาสามปีก่อน ระหว่างนั่งรอรถ หน้าจอโหลดขึ้นมาแล้ว ตัวหนังสือขึ้นครบแล้ว แต่พอเลื่อนนิ้ว ภาพมันสะดุดเป็นช่วง ๆ เหมือนมีอะไรบางอย่างกำลังคิดอยู่เบื้องหลัง
สิ่งที่กำลังคิดอยู่นั้นก็คือแอนิเมชันที่ตั้งใจใส่เข้าไปให้เว็บดูดีนั่นแหละครับ และมันก็ดูดีจริง ๆ ด้วย บนเครื่องของคนที่ออกแบบมัน
นี่คือความจริงที่ไม่ค่อยมีใครพูดถึงกันในวงการทำเว็บ ทุกเอฟเฟกต์ที่คุณใส่เข้าไปมีคนจ่ายเสมอ และคนจ่ายไม่ใช่คนทำ แต่คือเครื่องของผู้เข้าชม ตอนออกแบบเว็บนี้พวกเราเลยตกลงกันตั้งแต่แรกว่า จะถือแอนิเมชันเป็นการตัดสินใจเรื่องประสิทธิภาพก่อน แล้วค่อยเป็นเรื่องความสวยทีหลัง
เบราว์เซอร์เขาวาดหน้าจอกันยังไง
พอเราเข้าใจลำดับการทำงานของเบราว์เซอร์แล้ว คำถามที่ว่า "เอฟเฟกต์นี้แพงมั้ย" คุณจะตอบได้เองเลยครับ โดยไม่ต้องเดาอีกต่อไป
ลำดับนั้นมีสี่ขั้น เริ่มจาก style คือคำนวณว่าแต่ละชิ้นได้ค่าอะไรบ้างจาก CSS ต่อด้วย layout คือคำนวณว่าแต่ละชิ้นอยู่ตรงไหนและกว้างยาวเท่าไร ต่อด้วย paint คือระบายสี เส้น เงา ลงเป็นพิกเซล และจบที่ composite คือเอาชั้นที่ระบายไว้แล้วมาวางซ้อนกันเป็นภาพสุดท้าย
ทีนี้ประเด็นมันอยู่ตรงนี้ครับ ถ้าคุณสั่งให้ width, height, top, left หรือ margin เปลี่ยนค่าไปเรื่อย ๆ เบราว์เซอร์ต้องกลับไปคำนวณ layout ใหม่แล้วระบายใหม่ในทุกเฟรม หกสิบครั้งต่อวินาที บนงานสองขั้นที่หนักที่สุดของทั้งกระบวนการ
แต่ถ้าคุณเปลี่ยนแค่ transform กับ opacity งานนั้นตกไปอยู่ที่ compositor ซึ่งเป็นขั้นสุดท้ายและถูกที่สุด ชั้นภาพถูกระบายเอาไว้เรียบร้อยแล้ว สิ่งที่เหลือให้ทำคือขยับมันเฉย ๆ
นี่แหละครับคือเหตุผลที่แสงออโรราสามก้อนบนหน้าเว็บนี้ทำได้แค่สองอย่าง คือเลื่อนตำแหน่งกับย่อขยาย ไม่มีการเปลี่ยนความกว้าง ไม่มีการเปลี่ยนสี ก้อนพวกนี้เราใส่ blur(70px) ไว้ ซึ่งเป็นการคำนวณที่แพงมาก แต่มันแพงแค่ครั้งเดียว พอเบลอเสร็จแล้วผลลัพธ์ถูกเก็บไว้ การเลื่อนภาพที่เบลอเสร็จแล้วไปมาจึงแทบไม่มีต้นทุนอะไรเลย
ความแพงกับความหนักเป็นคนละเรื่องกันนะครับ สิ่งที่ทำครั้งเดียวแล้วขยับ ต่างจากสิ่งที่ต้องคิดใหม่ทุกเฟรมโดยสิ้นเชิง
แอนิเมชันตามการเลื่อนหน้า ที่ไม่ใช้ JavaScript เลยสักบรรทัด
การให้เนื้อหาค่อย ๆ ปรากฏตอนเลื่อนถึง เป็นสิ่งที่เกือบทุกเว็บทำด้วย JavaScript ครับ ไม่ก็ใช้ IntersectionObserver ไม่ก็ดักฟัง event การเลื่อน
เว็บนี้เราไม่ได้ใช้ทั้งสองอย่าง เราใช้ animation-timeline: view() ซึ่งผูกแอนิเมชันเข้ากับความคืบหน้าของชิ้นนั้นขณะเคลื่อนผ่านหน้าจอโดยตรง ไม่มี observer ไม่มีตัวดักฟัง ไม่มีงานตกไปที่ main thread ซึ่งเป็นเธรดเดียวกับที่ต้องคอยตอบสนองการแตะและการพิมพ์ของผู้ใช้ น่าสนใจมากเลยแหละครับ
แล้วในเบราว์เซอร์ที่ยังไม่รองรับล่ะ เนื้อหาก็แสดงอยู่ตรงนั้นตามปกติครับ ไม่มีอะไรพัง ไม่มีอะไรหาย มันเสื่อมลงไปเป็นคำว่า "มองเห็น" ซึ่งเป็นการล้มเหลวที่ถูกต้อง
ลองเทียบกับรูปแบบที่พบบ่อยกว่าดูนะครับ คือซ่อนเนื้อหาไว้ด้วย opacity ศูนย์ แล้วให้ JavaScript มาเปิดทีหลัง ถ้าสคริปต์ตัวนั้นโหลดไม่สำเร็จ ผู้เข้าชมจะได้หน้าว่างเปล่า นั่นคือความล้มเหลวที่ผิด
ทำไมการดักฟังการเลื่อนหน้าถึงเป็นค่าเริ่มต้นที่ไม่ดี
event การเลื่อนหน้าถูกยิงถี่กว่าที่หน้าจอจะวาดใหม่ทันเสียอีกครับ แปลว่าโค้ดของคุณถูกเรียกบ่อยกว่าจำนวนภาพที่ผู้ใช้มีโอกาสเห็น งานส่วนเกินตรงนั้นไม่ได้แปลงไปเป็นอะไรที่ใครมองเห็นเลยสักนิด
ที่หนักกว่านั้นคือถ้าข้างในตัวดักฟังมีการอ่านค่าตำแหน่ง อย่างเช่น getBoundingClientRect หรือ offsetTop เบราว์เซอร์จะถูกบังคับให้คำนวณ layout ใหม่ทันทีตรงนั้นเพื่อตอบค่าที่ถูกต้องกลับมา เขาเรียกกันว่า synchronous reflow มันคือการหยุดทุกอย่างเอาไว้เพื่อวัดขนาด ในจังหวะที่ผู้ใช้กำลังเลื่อนนิ้วอยู่พอดี
ลูปที่หยุดตัวเองได้
แสงที่ตามเมาส์บนเว็บนี้ใช้ตัวดักฟัง pointermove ตัวเดียว และลูป requestAnimationFrame ตัวเดียวครับ
แต่สิ่งที่สำคัญกว่าจำนวนคือเงื่อนไขการหยุด ทุกเฟรม แสงจะขยับเข้าหาตำแหน่งเมาส์ทีละส่วน แล้วเช็คว่ามันตามทันหรือยัง ถ้ายังห่างอยู่ก็นัดเฟรมถัดไป ถ้าตามทันแล้วก็ไม่นัดต่อ ลูปจบลงเงียบ ๆ
ผลก็คือหน้าเว็บที่ไม่มีใครแตะต้องจะไม่ทำงานอะไรเลย ไม่ใช่ทำงานน้อยนะครับ แต่คือศูนย์ ผมว่านี่คือหลักที่เราควรถือเป็นค่าเริ่มต้นกันได้แล้ว หน้าที่นิ่งอยู่ควรมีต้นทุนเป็นศูนย์ แท็บที่เปิดค้างไว้ไม่ควรไปกินแบตเตอรี่ของใคร
ส่วนการอ่านค่าตำแหน่งที่บอกไปว่าแพงนั้น ในตัวดักฟังนี้มี getBoundingClientRect อยู่หนึ่งครั้ง และเรียกเฉพาะกับการ์ดใบที่เมาส์อยู่บนมันจริง ๆ เท่านั้น ไม่ใช่ทุกใบในหน้า
ไม่เก็บเงินจากเครื่องที่ยังไงก็ใช้มันไม่ได้
ก่อนจะสร้างอะไรขึ้นมา โค้ดถามคำถามเดียวครับว่า (hover: hover) and (pointer: fine) หรือเปล่า แปลเป็นภาษาคนก็คือ เครื่องนี้มีเคอร์เซอร์ที่ชี้ได้แม่นยำมั้ย
มือถือไม่มีครับ ไม่มีเคอร์เซอร์ให้ตาม เอฟเฟกต์นี้ไม่มีความหมายอะไรเลยบนปลายนิ้ว ดังนั้นมันจึงไม่ถูกสร้างขึ้นมาตั้งแต่แรก
ความต่างตรงนี้ละเอียดมากแต่สำคัญนะครับ การใช้ media query ซ่อนเอฟเฟกต์ หมายความว่าโค้ดยังโหลด ยังทำงาน ยังผูกตัวดักฟัง ยังกินหน่วยความจำ เพียงแต่ผลลัพธ์มองไม่เห็น ผู้ใช้มือถือก็ยังจ่ายค่าเอฟเฟกต์ที่เขาไม่มีวันได้เห็นอยู่ดี ส่วนการเช็คก่อนแล้วไม่ทำอะไรเลย คือไม่มีอะไรให้ต้องจ่ายตั้งแต่ต้น
เว็บนี้เรายังเช็ค prefers-reduced-motion ด้วยเงื่อนไขเดียวกัน ท่านผู้อ่านที่ตั้งค่าเครื่องไว้ว่าขอความเคลื่อนไหวน้อยลง ไม่ได้แค่เห็นน้อยลงนะครับ แต่ไม่มีอะไรถูกสร้างขึ้นมาเลย
ทำไมต้อง 23, 31 และ 41 วินาที
ตรงนี้ผมชอบมากครับ ออโรราสามก้อนมีรอบการเคลื่อนที่ 23 วินาที 31 วินาที และ 41 วินาที ตัวเลขพวกนี้ไม่ได้สุ่มมา และก็ไม่ได้เลือกเพราะฟังดูแปลกด้วย
ลองสมมติว่าเราใช้ 20, 30 และ 40 วินาทีแทนดูนะครับ ทุก ๆ 120 วินาที ทั้งสามก้อนจะกลับมาอยู่ในตำแหน่งเดิมพร้อมกันเป๊ะ คนที่อ่านหน้าเว็บอยู่สักพักจะเริ่มรู้สึกได้ว่าฉากหลังมันวนซ้ำ และพอรู้สึกได้เมื่อไหร่ มันก็หยุดเป็นบรรยากาศทันที กลายเป็นภาพ GIF สั้น ๆ ที่เล่นวนอยู่หลังตัวหนังสือ
แต่ตัวเลขที่ไม่มีตัวประกอบร่วมกันจะใช้เวลานานกว่ามากกว่าจะกลับมาเรียงตัวแบบเดิม นานพอที่คนคนหนึ่งจะอ่านหน้าเว็บจนจบแล้วปิดไป โดยไม่เคยเห็นการซ้ำเลยสักครั้ง
ต้นทุนของการเลือกแบบนี้เท่ากับศูนย์ครับ มันคือตัวเลขสามตัวใน CSS เท่านั้นเอง
ความยับยั้งชั่งใจนี่แหละคือทักษะจริง ๆ
ส่วนหัวของหน้าแรกเอียงตามตำแหน่งเมาส์อยู่ไม่ถึงสองเปอร์เซ็นต์ครับ น้อยจนถ้าเอาไปวางเทียบกัน คนส่วนใหญ่จะบอกไม่ได้ด้วยซ้ำว่าอันไหนขยับ
นั่นคือความตั้งใจครับ หลักที่พวกเราใช้คือ เอฟเฟกต์ที่ผู้เข้าชมรู้ตัวว่าเห็น มักจะแรงเกินไปแล้ว เป้าหมายไม่ใช่ให้เขาเห็นว่ามีอะไรเคลื่อนไหว แต่ให้พื้นผิวรู้สึกเหมือนกำลังรับแสงอยู่
ส่วนที่ยากของงานนี้ไม่ใช่การทำเอฟเฟกต์ให้ได้นะครับ เดี๋ยวนี้เครื่องมือทำให้เรื่องนั้นง่ายมาก ส่วนที่ยากคือการหรี่มันลงเรื่อย ๆ จนเกือบหายไป แล้วหยุดตรงนั้น
สิ่งที่เพื่อน ๆ เอาไปใช้กับงานอื่นได้
หลักคิดสามข้อนี้ใช้ได้กับทุกหน้าจอครับ ไม่ใช่แค่เว็บบริษัท
ข้อแรก ถามตัวเองว่าใครเป็นคนจ่ายค่าเอฟเฟกต์นี้ ถ้าคำตอบคือเครื่องของผู้ใช้ที่เก่าที่สุด ก็ทบทวนกันใหม่
ข้อสอง ถามว่าถ้าสิ่งนี้ทำงานไม่ได้ ผู้ใช้จะเจออะไร ถ้าคำตอบคือหน้าว่าง แปลว่าเราออกแบบการล้มเหลวผิดทาง
และข้อสาม ถามว่าตอนที่ไม่มีใครทำอะไรเลย ระบบยังทำงานอยู่มั้ย ถ้ายัง แปลว่ายังมีอะไรให้ตัดออกได้อีกครับ
ถ้าอยากคุยเรื่องหน้าจอหรือระบบที่คุณกำลังทำอยู่ ติดต่อเราได้
เกี่ยวกับผู้เขียน
ฟิล์ม — วิสิฏฐ์ ครับ ทำธุรกิจเป็นหลัก แต่ยังลงมือทำ BA เองอยู่บ่อยกว่าที่ควร และเขียนโค้ดได้ประปรายทั้งหน้าบ้านหลังบ้าน มีธุรกิจเล็ก ๆ อยู่สองสามบริษัท ติดตามวงการเทคโนโลยีและธุรกิจแบบคลั่งไคล้ ทั้งในไทยและต่างประเทศ นอกเวลางานชอบฟิสิกส์ ดาราศาสตร์ งาน DIY และเรื่องเน็ตเวิร์ก เปิดเพลงฟังตลอดเวลาแต่ร้องไม่เป็น ชอบเล่นดนตรีทั้งที่เล่นได้ไม่ค่อยดี เล่นกีฬาหลายอย่าง โดยเฉพาะกีฬาแร็กเกต เสียอยู่อย่างเดียวคือแทบไม่เล่นเกม
เขียนร่วมกับ Claude Opus 5