บทความ

เอฟเฟกต์สวย ๆ ที่ไม่ไปกินแรงเครื่องคนอ่าน

เพื่อน ๆ รู้มั้ยครับว่าก่อนจะเป็นเรื่องความสวย แอนิเมชันคือการตัดสินใจเรื่องประสิทธิภาพ และเว็บส่วนใหญ่ก็ผลักภาระตรงนั้นไปไว้บนมือถือของผู้เข้าชม

เผยแพร่

ปกตอนที่ 4 “เอฟเฟกต์ที่ไม่ให้เครื่องคนอ่านจ่าย” มีวงโคจรสามวงกำกับเวลา 23s, 31s และ 41s บนภาพเส้นไฟรถยนต์ยามค่ำคืน

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

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

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

เบราว์เซอร์เขาวาดหน้าจอกันยังไง

พอเราเข้าใจลำดับการทำงานของเบราว์เซอร์แล้ว คำถามที่ว่า "เอฟเฟกต์นี้แพงมั้ย" คุณจะตอบได้เองเลยครับ โดยไม่ต้องเดาอีกต่อไป

ลำดับนั้นมีสี่ขั้น เริ่มจาก style คือคำนวณว่าแต่ละชิ้นได้ค่าอะไรบ้างจาก CSS ต่อด้วย layout คือคำนวณว่าแต่ละชิ้นอยู่ตรงไหนและกว้างยาวเท่าไร ต่อด้วย paint คือระบายสี เส้น เงา ลงเป็นพิกเซล และจบที่ composite คือเอาชั้นที่ระบายไว้แล้วมาวางซ้อนกันเป็นภาพสุดท้าย

ทีนี้ประเด็นมันอยู่ตรงนี้ครับ ถ้าคุณสั่งให้ width, height, top, left หรือ margin เปลี่ยนค่าไปเรื่อย ๆ เบราว์เซอร์ต้องกลับไปคำนวณ layout ใหม่แล้วระบายใหม่ในทุกเฟรม หกสิบครั้งต่อวินาที บนงานสองขั้นที่หนักที่สุดของทั้งกระบวนการ

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

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

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

แอนิเมชันตามการเลื่อนหน้า ที่ไม่ใช้ JavaScript เลยสักบรรทัด

การให้เนื้อหาค่อย ๆ ปรากฏตอนเลื่อนถึง เป็นสิ่งที่เกือบทุกเว็บทำด้วย JavaScript ครับ ไม่ก็ใช้ IntersectionObserver ไม่ก็ดัก event ตอนเลื่อนหน้า

เว็บนี้เราไม่ได้ใช้ทั้งสองอย่าง เราใช้ animation-timeline: view() ซึ่งผูกแอนิเมชันเข้ากับความคืบหน้าของชิ้นนั้นขณะเคลื่อนผ่านหน้าจอโดยตรง ไม่มี observer ไม่มี listener ไม่มีงานตกไปที่ main thread ซึ่งเป็นเธรดเดียวกับที่ต้องคอยตอบสนองการแตะและการพิมพ์ของผู้ใช้ น่าสนใจมากเลยแหละครับ

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

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

ทำไมการดัก event ตอนเลื่อนหน้าถึงเป็นค่าเริ่มต้นที่ไม่ดี

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

ที่หนักกว่านั้นคือถ้าข้างใน listener มีการอ่านค่าตำแหน่ง อย่างเช่น getBoundingClientRect หรือ offsetTop เบราว์เซอร์จะถูกบังคับให้คำนวณ layout ใหม่ทันทีตรงนั้นเพื่อตอบค่าที่ถูกต้องกลับมา เขาเรียกกันว่า synchronous reflow มันคือการหยุดทุกอย่างเอาไว้เพื่อวัดขนาด ในจังหวะที่ผู้ใช้กำลังเลื่อนนิ้วอยู่พอดี

ลูปที่หยุดตัวเองได้

แสงที่ตามเมาส์บนเว็บนี้ใช้ listener ของ pointermove ตัวเดียว และลูป requestAnimationFrame ตัวเดียวครับ

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

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

ส่วนการอ่านค่าตำแหน่งที่บอกไปว่าแพงนั้น ใน listener ตัวนี้มี getBoundingClientRect อยู่หนึ่งครั้ง และเรียกเฉพาะกับการ์ดใบที่เมาส์อยู่บนมันจริง ๆ เท่านั้น ไม่ใช่ทุกใบในหน้า

ไม่ไปกินแรงเครื่องที่ยังไงก็ใช้เอฟเฟกต์นี้ไม่ได้

ก่อนจะสร้างอะไรขึ้นมา โค้ดถามคำถามเดียวครับว่า (hover: hover) and (pointer: fine) หรือเปล่า แปลเป็นภาษาคนก็คือ เครื่องนี้มีเคอร์เซอร์ที่ชี้ได้แม่นยำมั้ย

มือถือไม่มีครับ ไม่มีเคอร์เซอร์ให้ตาม เอฟเฟกต์นี้ไม่มีความหมายอะไรเลยบนปลายนิ้ว ดังนั้นมันจึงไม่ถูกสร้างขึ้นมาตั้งแต่แรก

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

เว็บนี้เรายังเช็ค prefers-reduced-motion ด้วยเงื่อนไขเดียวกัน ท่านผู้อ่านที่ตั้งค่าเครื่องไว้ว่าขอความเคลื่อนไหวน้อยลง ไม่ได้แค่เห็นน้อยลงนะครับ แต่ไม่มีอะไรถูกสร้างขึ้นมาเลย

ทำไมต้อง 23, 31 และ 41 วินาที

ตรงนี้ผมชอบมากครับ ออโรราสามก้อนมีรอบการเคลื่อนที่ 23 วินาที 31 วินาที และ 41 วินาที ตัวเลขพวกนี้ไม่ได้สุ่มมา และก็ไม่ได้เลือกเพราะฟังดูแปลกด้วย

ลองสมมติว่าเราใช้ 20, 30 และ 40 วินาทีแทนดูนะครับ ทุก ๆ 120 วินาที ทั้งสามก้อนจะกลับมาอยู่ในตำแหน่งเดิมพร้อมกันเป๊ะ คนที่อ่านหน้าเว็บอยู่สักพักจะเริ่มรู้สึกได้ว่าฉากหลังมันวนซ้ำ และพอรู้สึกได้เมื่อไหร่ มันก็หยุดเป็นบรรยากาศทันที กลายเป็นภาพ GIF สั้น ๆ ที่เล่นวนอยู่หลังตัวหนังสือ

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

ต้นทุนของการเลือกแบบนี้เท่ากับศูนย์ครับ มันคือตัวเลขสามตัวใน CSS เท่านั้นเอง

ความยับยั้งชั่งใจนี่แหละคือทักษะจริง ๆ

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

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

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

สิ่งที่เพื่อน ๆ เอาไปใช้กับงานอื่นได้

หลักคิดสามข้อนี้ใช้ได้กับทุกหน้าจอครับ ไม่ใช่แค่เว็บบริษัท

ข้อแรก ถามตัวเองว่าเครื่องไหนต้องออกแรงให้เอฟเฟกต์นี้ ถ้าคำตอบคือเครื่องที่เก่าที่สุดของผู้ใช้ ก็ทบทวนกันใหม่

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

และข้อสาม ถามว่าตอนที่ไม่มีใครทำอะไรเลย ระบบยังทำงานอยู่มั้ย ถ้ายัง แปลว่ายังมีอะไรให้ตัดออกได้อีกครับ

ถ้าอยากคุยเรื่องหน้าจอหรือระบบที่คุณกำลังทำอยู่ ติดต่อเราได้

เกี่ยวกับผู้เขียน

ภาพถ่ายของ Film

ฟิล์ม — วิสิฏฐ์ ครับ ทำธุรกิจเป็นหลัก แต่ยังลงมือทำ BA เองอยู่บ่อยกว่าที่ควร และเขียนโค้ดได้ประปรายทั้งหน้าบ้านหลังบ้าน มีธุรกิจเล็ก ๆ อยู่สองสามบริษัท ติดตามวงการเทคโนโลยีและธุรกิจแบบคลั่งไคล้ ทั้งในไทยและต่างประเทศ นอกเวลางานชอบฟิสิกส์ ดาราศาสตร์ งาน DIY และเรื่องเน็ตเวิร์ก เปิดเพลงฟังตลอดเวลาแต่ร้องไม่เป็น ชอบเล่นดนตรีทั้งที่เล่นได้ไม่ค่อยดี เล่นกีฬาหลายอย่าง โดยเฉพาะกีฬาที่ใช้แร็กเกต ไม่ได้ชอบเที่ยวเท่าไร แต่ชอบพามิ้นท์ไปเที่ยวที่ต่าง ๆ ทั่วโลก เป็นคนกลัว เอ้ย รักเมีย เสียอยู่อย่างเดียวคือแทบไม่เล่นเกม

เขียนร่วมกับ Claude Opus 5