บทนำ
คุณต้องการให้เว็บแอปตอบสนองและทำงานได้อย่างราบรื่นเมื่อแสดงภาพเคลื่อนไหว การเปลี่ยนภาพ และเอฟเฟกต์ UI เล็กๆ อื่นๆ การตรวจสอบว่าเอฟเฟกต์เหล่านี้ไม่มีอาการกระตุกอาจเป็นตัวชี้วัดความแตกต่างระหว่างความรู้สึก "เนทีฟ" หรือความรู้สึกที่ดูไม่เป็นมืออาชีพและไม่ได้รับการขัดเกลา
บทความนี้เป็นบทความแรกในชุดบทความที่ครอบคลุมการเพิ่มประสิทธิภาพการแสดงผลในเบราว์เซอร์ เราจะเริ่มต้นด้วยการอธิบายว่าทำไมการสร้างภาพเคลื่อนไหวที่ราบรื่นจึงเป็นเรื่องยากและต้องทำอย่างไรจึงจะทำได้ รวมถึงแนวทางปฏิบัติแนะนำง่ายๆ บางประการ แนวคิดเหล่านี้หลายอย่างได้รับการนำเสนอครั้งแรกใน "Jank Busters" ซึ่งเป็นการพูดคุยที่ Nat Duca และฉันได้พูดคุยกันในงาน Google I/O (วิดีโอ) ในปีนี้
ขอแนะนำ V-sync
เกมเมอร์ PC อาจคุ้นเคยกับคำนี้ แต่ไม่ค่อยพบในเว็บ v-sync คืออะไร
ลองนึกถึงจอแสดงผลของโทรศัพท์ที่รีเฟรชเป็นระยะๆ โดยปกติ (แต่ไม่เสมอไป) จะอยู่ที่ประมาณ 60 ครั้งต่อวินาที V-sync (หรือการซิงค์แนวตั้ง) หมายถึงแนวทางปฏิบัติในการสร้างเฟรมใหม่ระหว่างการรีเฟรชหน้าจอเท่านั้น คุณอาจมองว่าปัญหานี้เป็นเหมือนภาวะแข่งขันระหว่างกระบวนการที่เขียนข้อมูลลงในบัฟเฟอร์หน้าจอกับระบบปฏิบัติการที่อ่านข้อมูลนั้นเพื่อแสดงบนหน้าจอ เราต้องการให้เนื้อหาเฟรมที่บัฟเฟอร์เปลี่ยนระหว่างการรีเฟรชเหล่านี้ ไม่ใช่ในระหว่างการรีเฟรช มิฉะนั้นจอภาพจะแสดงเฟรมหนึ่งครึ่งหนึ่งและอีกเฟรมหนึ่งครึ่งหนึ่ง ซึ่งทำให้เกิด "การฉีกขาด"
หากต้องการให้ภาพเคลื่อนไหวราบรื่น คุณต้องเตรียมเฟรมใหม่ให้พร้อมทุกครั้งที่มีการรีเฟรชหน้าจอ ซึ่งส่งผลกระทบสำคัญ 2 ประการ ได้แก่ เวลาที่ใช้ในการแสดงผลเฟรม (นั่นคือเวลาที่เฟรมต้องพร้อม) และงบประมาณเฟรม (นั่นคือระยะเวลาที่เบราว์เซอร์ต้องสร้างเฟรม) คุณมีเวลาเพียงช่วงระหว่างการรีเฟรชหน้าจอเพื่อสร้างเฟรมให้เสร็จสมบูรณ์ (~16 มิลลิวินาทีบนหน้าจอ 60Hz) และคุณต้องการเริ่มสร้างเฟรมถัดไปทันทีที่เฟรมสุดท้ายปรากฏบนหน้าจอ
จังหวะเวลาคือสิ่งสำคัญ: requestAnimationFrame
นักพัฒนาเว็บหลายรายใช้ setInterval หรือ setTimeout ทุกๆ 16 มิลลิวินาทีเพื่อสร้างภาพเคลื่อนไหว ซึ่งเป็นปัญหาด้วยเหตุผลหลายประการ (และเราจะพูดถึงเพิ่มเติมในอีกสักครู่) แต่ข้อกังวลที่สำคัญมีดังนี้
- ความละเอียดของตัวจับเวลาจาก JavaScript อยู่ในลำดับหลายมิลลิวินาทีเท่านั้น
- อุปกรณ์แต่ละเครื่องมีอัตราการรีเฟรชที่แตกต่างกัน
โปรดระลึกถึงปัญหาเกี่ยวกับเวลาที่ใช้ในการแสดงผลเฟรมที่กล่าวถึงข้างต้น คุณต้องมีเฟรมภาพเคลื่อนไหวที่สมบูรณ์ ซึ่งเสร็จสิ้นด้วย JavaScript, การจัดการ DOM, เลย์เอาต์, การวาด ฯลฯ เพื่อให้พร้อมก่อนที่จะมีการรีเฟรชหน้าจอครั้งถัดไป ความละเอียดของตัวจับเวลาที่ต่ำอาจทำให้เฟรมภาพเคลื่อนไหวเสร็จสมบูรณ์ก่อนการรีเฟรชหน้าจอครั้งถัดไปได้ยาก แต่ความผันแปรของอัตราการรีเฟรชหน้าจอทำให้ไม่สามารถใช้ตัวจับเวลาแบบคงที่ได้ ไม่ว่าช่วงเวลาของตัวจับเวลาจะเป็นเท่าใด คุณจะค่อยๆ หลุดออกจากกรอบเวลาสำหรับเฟรมและจบลงด้วยการข้ามเฟรมไป ซึ่งจะเกิดขึ้นแม้ว่าตัวจับเวลาจะเริ่มทำงานด้วยความแม่นยำระดับมิลลิวินาที ซึ่งจะไม่เกิดขึ้น (ตามที่นักพัฒนาซอฟต์แวร์ค้นพบ) ความละเอียดของตัวจับเวลาจะแตกต่างกันไปขึ้นอยู่กับว่าเครื่องใช้แบตเตอรี่หรือเสียบปลั๊กอยู่ อาจได้รับผลกระทบจากแท็บที่ทำงานอยู่เบื้องหลังซึ่งใช้ทรัพยากรมากเกินไป เป็นต้น แม้ว่ากรณีนี้จะเกิดขึ้นไม่บ่อยนัก (เช่น ทุกๆ 16 เฟรมเนื่องจากคุณคลาดเคลื่อนไป 1 มิลลิวินาที) คุณจะสังเกตเห็นว่าเฟรมจะหายไปหลายเฟรมต่อวินาที นอกจากนี้ คุณยังต้องทำงานเพื่อสร้างเฟรมที่ไม่เคยแสดง ซึ่งจะทำให้สิ้นเปลืองพลังงานและเวลา CPU ที่คุณอาจใช้ทำสิ่งอื่นๆ ในแอปพลิเคชันได้
จอแสดงผลแต่ละจอมีอัตราการรีเฟรชที่แตกต่างกัน โดยทั่วไปคือ 60Hz แต่โทรศัพท์บางรุ่นมีอัตราการรีเฟรช 59Hz แล็ปท็อปบางรุ่นลดลงเหลือ 50Hz ในโหมดใช้พลังงานต่ำ และจอภาพเดสก์ท็อปบางรุ่นมีอัตราการรีเฟรช 70Hz
เรามักจะมุ่งเน้นที่เฟรมต่อวินาที (FPS) เมื่อพูดถึงประสิทธิภาพการแสดงผล แต่ความแปรปรวนอาจเป็นปัญหาที่ใหญ่กว่า ดวงตาของเราจะสังเกตเห็นความผิดปกติเล็กๆ น้อยๆ ในภาพเคลื่อนไหวที่อาจเกิดขึ้นได้หากภาพเคลื่อนไหวมีจังหวะที่ไม่เหมาะสม
วิธีรับเฟรมภาพเคลื่อนไหวที่มีเวลาถูกต้องคือใช้ requestAnimationFrame เมื่อใช้ API นี้ คุณกำลังขอเฟรมภาพเคลื่อนไหวจากเบราว์เซอร์ ระบบจะเรียกใช้ Callback เมื่อเบราว์เซอร์กำลังจะสร้างเฟรมใหม่ในเร็วๆ นี้ ปัญหานี้เกิดขึ้นไม่ว่าอัตราการรีเฟรชจะเป็นเท่าใดก็ตาม
requestAnimationFrame ยังมีคุณสมบัติที่ดีอื่นๆ ด้วย ดังนี้
- ภาพเคลื่อนไหวในแท็บที่ทำงานอยู่เบื้องหลังจะหยุดชั่วคราวเพื่อประหยัดทรัพยากรระบบและระยะเวลาการใช้งานแบตเตอรี่
- หากระบบไม่สามารถจัดการการแสดงผลที่อัตราการรีเฟรชของหน้าจอ ระบบจะจำกัดภาพเคลื่อนไหวและสร้าง Callback น้อยลง (เช่น 30 ครั้งต่อวินาทีบนหน้าจอ 60Hz) แม้ว่าวิธีนี้จะลดอัตราเฟรมลงครึ่งหนึ่ง แต่ก็ทำให้ภาพเคลื่อนไหวมีความสอดคล้องกัน และตามที่ระบุไว้ข้างต้น ดวงตาของเราจะปรับให้เข้ากับความแปรปรวนได้ดีกว่าอัตราเฟรมมาก 30Hz ที่เสถียรจะดูดีกว่า 60Hz ที่พลาดไป 2-3 เฟรมต่อวินาที
requestAnimationFrame มีการพูดถึงกันอย่างแพร่หลายแล้ว โปรดดูข้อมูลเพิ่มเติมจากบทความต่างๆ เช่น บทความนี้จาก JS ที่สร้างสรรค์ แต่ก็เป็นขั้นตอนแรกที่สำคัญในการสร้างภาพเคลื่อนไหวที่ราบรื่น
งบประมาณเฟรม
เนื่องจากเราต้องการให้เฟรมใหม่พร้อมใช้งานทุกครั้งที่รีเฟรชหน้าจอ จึงมีเพียงช่วงเวลาระหว่างการรีเฟรชเท่านั้นที่จะทำงานทั้งหมดเพื่อสร้างเฟรมใหม่ ในจอแสดงผล 60Hz หมายความว่าเรามีเวลาประมาณ 16 มิลลิวินาทีในการเรียกใช้ JavaScript ทั้งหมด จัดเลย์เอาต์ วาดภาพ และอื่นๆ ที่เบราว์เซอร์ต้องทำเพื่อแสดงเฟรม ซึ่งหมายความว่าหาก JavaScript ภายใน Callback requestAnimationFrame ของคุณใช้เวลาในการรันนานกว่า 16 มิลลิวินาที คุณก็ไม่มีทางสร้างเฟรมได้ทันเวลาสำหรับ V-Sync
16 มิลลิวินาทีเป็นเวลาไม่มากนัก โชคดีที่เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ของ Chrome ช่วยคุณติดตามได้ว่าคุณใช้ Frame Budget เกินระหว่างการเรียกกลับ requestAnimationFrame หรือไม่
การเปิดไทม์ไลน์เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์และบันทึกภาพเคลื่อนไหวนี้ขณะทำงานจะแสดงให้เห็นอย่างรวดเร็วว่าเราใช้ทรัพยากรเกินงบประมาณเมื่อสร้างภาพเคลื่อนไหว ในไทม์ไลน์ ให้เปลี่ยนเป็น "เฟรม" แล้วดูว่ามีอะไรบ้าง
การเรียกกลับ requestAnimationFrame (rAF) เหล่านั้นใช้เวลามากกว่า 200 มิลลิวินาที ซึ่งเป็นลำดับความสำคัญที่นานเกินไปที่จะแสดงเฟรมทุกๆ 16 มิลลิวินาที การเปิด Callback ของ rAF ที่ใช้เวลานานรายการหนึ่งจะแสดงสิ่งที่เกิดขึ้นภายใน ซึ่งในกรณีนี้คือเลย์เอาต์จำนวนมาก
วิดีโอของ Paul จะลงรายละเอียดเพิ่มเติมเกี่ยวกับสาเหตุที่เฉพาะเจาะจงของการจัดเลย์เอาต์ใหม่ (ซึ่งก็คือการอ่าน scrollTop) และวิธีหลีกเลี่ยง แต่ประเด็นคือคุณสามารถเจาะลึก Callback และตรวจสอบสาเหตุที่ใช้เวลานานได้
สังเกตเวลาที่ใช้ในการแสดงผลเฟรม 16 มิลลิวินาที พื้นที่ว่างในเฟรมคือพื้นที่ว่างที่คุณต้องทำงานเพิ่มเติม (หรือปล่อยให้เบราว์เซอร์ทำงานที่จำเป็นต้องทำในเบื้องหลัง) พื้นที่ว่างนั้นเป็นสิ่งที่ดี
แหล่งที่มาอื่นๆ ของอาการกระตุก
สาเหตุที่ใหญ่ที่สุดของปัญหาเมื่อพยายามเรียกใช้ภาพเคลื่อนไหวที่ทำงานด้วย JavaScript คือสิ่งอื่นๆ อาจขัดขวาง Callback ของ rAF และอาจทำให้ไม่สามารถเรียกใช้ได้เลย แม้ว่า rAF Callback จะมีขนาดเล็กและทำงานในเวลาเพียงไม่กี่มิลลิวินาที แต่กิจกรรมอื่นๆ (เช่น การประมวลผล XHR ที่เพิ่งเข้ามา การเรียกใช้ตัวแฮนเดิลเหตุการณ์อินพุต หรือการเรียกใช้การอัปเดตตามกำหนดเวลาในตัวจับเวลา) อาจเข้ามาอย่างกะทันหันและทำงานเป็นระยะเวลาใดก็ได้โดยไม่ยอมหยุด ในอุปกรณ์เคลื่อนที่ บางครั้งการประมวลผลเหตุการณ์เหล่านี้อาจใช้เวลาหลายร้อยมิลลิวินาที ซึ่งในระหว่างนั้นภาพเคลื่อนไหวจะหยุดทำงานโดยสมบูรณ์ เราเรียกการกระตุกของภาพเคลื่อนไหวเหล่านี้ว่า Jank
ไม่มีวิธีที่สมบูรณ์แบบในการหลีกเลี่ยงสถานการณ์เหล่านี้ แต่มีแนวทางปฏิบัติแนะนำด้านสถาปัตยกรรมบางอย่างที่จะช่วยให้คุณประสบความสำเร็จได้ ดังนี้
- อย่าประมวลผลจำนวนมากในตัวแฮนเดิลอินพุต การใช้ JS จำนวนมากหรือพยายามจัดเรียงทั้งหน้าใหม่ในระหว่างที่ใช้ตัวแฮนเดิล onscroll เป็นสาเหตุที่พบบ่อยมากที่ทำให้เกิดความล่าช้าอย่างมาก
- พยายามผลักดันการประมวลผล (อ่าน: สิ่งที่ใช้เวลานานในการรัน) ไปยัง Callback ของ rAF หรือ Web Worker ให้มากที่สุด
- หากคุณพุชงานลงใน Callback ของ rAF ให้ลองแบ่งงานออกเป็นส่วนๆ เพื่อให้คุณประมวลผลเพียงเล็กน้อยในแต่ละเฟรมหรือเลื่อนออกไปจนกว่าภาพเคลื่อนไหวที่สำคัญจะสิ้นสุดลง วิธีนี้จะช่วยให้คุณเรียกใช้ Callback ของ rAF แบบสั้นๆ และแสดงภาพเคลื่อนไหวได้อย่างราบรื่นต่อไป
ดูบทความของ Paul Lewis เรื่อง Leaner, Meaner, Faster Animations with requestAnimationFrame เพื่อดูบทแนะนำที่ยอดเยี่ยมเกี่ยวกับวิธีพุชการประมวลผลไปยัง Callback ของ requestAnimationFrame แทนตัวแฮนเดิลอินพุต
ภาพเคลื่อนไหว CSS
มีอะไรดีไปกว่า JS ที่มีน้ำหนักเบาใน Callback ของเหตุการณ์และ rAF ไม่มี JS
ก่อนหน้านี้เราได้กล่าวไว้ว่าไม่มีวิธีใดที่สมบูรณ์แบบในการหลีกเลี่ยงการขัดจังหวะ Callback ของ rAF แต่คุณสามารถใช้ภาพเคลื่อนไหว CSS เพื่อหลีกเลี่ยงความจำเป็นในการใช้ Callback เหล่านั้นได้ทั้งหมด โดยเฉพาะใน Chrome สำหรับ Android (และเบราว์เซอร์อื่นๆ ที่กำลังพัฒนาฟีเจอร์ที่คล้ายกัน) ภาพเคลื่อนไหว CSS มีพร็อพเพอร์ตี้ที่น่าสนใจมากซึ่งเบราว์เซอร์มักจะเรียกใช้ได้แม้ว่า JavaScript จะทำงานอยู่ก็ตาม
มีคำสั่งโดยนัยในส่วนด้านบนเกี่ยวกับอาการกระตุก นั่นคือเบราว์เซอร์ทำได้เพียงอย่างเดียวในแต่ละครั้ง ซึ่งไม่เป็นความจริงทั้งหมด แต่ก็เป็นสมมติฐานการทำงานที่ดีที่ควรมี นั่นคือ เบราว์เซอร์สามารถเรียกใช้ JS, จัดเลย์เอาต์ หรือวาดภาพได้ทุกเมื่อ แต่จะทำได้ทีละอย่างเท่านั้น ซึ่งสามารถยืนยันได้ในมุมมองไทม์ไลน์ของเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ ข้อยกเว้นอย่างหนึ่งของกฎนี้คือภาพเคลื่อนไหว CSS ใน Chrome สำหรับ Android (และ Chrome บนเดสก์ท็อปในเร็วๆ นี้ แต่ยังไม่พร้อมใช้งาน)
เมื่อเป็นไปได้ การใช้ภาพเคลื่อนไหว CSS จะช่วยให้แอปพลิเคชันของคุณเรียบง่ายขึ้นและช่วยให้ภาพเคลื่อนไหวทำงานได้อย่างราบรื่น แม้ในขณะที่ JavaScript ทำงานอยู่
// see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
rAF = window.requestAnimationFrame;
var degrees = 0;
function update(timestamp) {
document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
console.log('updated to degrees ' + degrees);
degrees = degrees + 1;
rAF(update);
}
rAF(update);
หากคุณคลิกปุ่ม JavaScript จะทำงานเป็นเวลา 180 มิลลิวินาที ซึ่งทำให้เกิดความกระตุก แต่หากเราขับเคลื่อนภาพเคลื่อนไหวนั้นด้วยภาพเคลื่อนไหว CSS แทน อาการกระตุกจะไม่เกิดขึ้นอีกต่อไป
(โปรดทราบว่าในขณะที่เขียนบทความนี้ ภาพเคลื่อนไหว CSS จะทำงานได้อย่างราบรื่นใน Chrome สำหรับ Android เท่านั้น ไม่ใช่ Chrome บนเดสก์ท็อป)
/* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
#foo {
+animation-duration: 3s;
+animation-timing-function: linear;
+animation-animation-iteration-count: infinite;
+animation-animation-name: rotate;
}
@+keyframes: rotate; {
from {
+transform: rotate(0deg);
}
to {
+transform: rotate(360deg);
}
}
ดูข้อมูลเพิ่มเติมเกี่ยวกับการใช้ภาพเคลื่อนไหว CSS ได้ในบทความต่างๆ เช่น บทความนี้ใน MDN
สรุป
สรุปสั้นๆ ได้ดังนี้
- เมื่อสร้างภาพเคลื่อนไหว การสร้างเฟรมสำหรับการรีเฟรชหน้าจอทุกครั้งเป็นสิ่งสำคัญ ภาพเคลื่อนไหวที่ซิงค์กับ Vsync ส่งผลดีอย่างมากต่อความรู้สึกของผู้ใช้ที่มีต่อแอป
- วิธีที่ดีที่สุดในการสร้างภาพเคลื่อนไหวที่ซิงค์กับ VSync ใน Chrome และเบราว์เซอร์ที่ทันสมัยอื่นๆ คือการใช้ภาพเคลื่อนไหว CSS เมื่อคุณต้องการความยืดหยุ่นมากกว่าที่ภาพเคลื่อนไหว CSS มีให้ เทคนิคที่ดีที่สุดคือภาพเคลื่อนไหวที่ใช้ requestAnimationFrame
- เพื่อให้ภาพเคลื่อนไหว rAF ทำงานได้อย่างราบรื่นและมีประสิทธิภาพ โปรดตรวจสอบว่าเครื่องจัดการเหตุการณ์อื่นๆ ไม่ได้ขัดขวางการทำงานของ Callback ของ rAF และทำให้ Callback ของ rAF สั้น (<15 มิลลิวินาที)
สุดท้าย ภาพเคลื่อนไหวที่ซิงค์กับ VSync ไม่ได้ใช้กับภาพเคลื่อนไหว UI แบบง่ายเท่านั้น แต่ยังใช้กับภาพเคลื่อนไหว Canvas2D, ภาพเคลื่อนไหว WebGL และแม้แต่การเลื่อนในหน้าเว็บแบบคงที่ด้วย ในบทความถัดไปของชุดนี้ เราจะเจาะลึกประสิทธิภาพการเลื่อนโดยคำนึงถึงแนวคิดเหล่านี้
ขอให้สนุกกับการสร้างภาพเคลื่อนไหว