การป้องกันภาพสั่นไหวเพื่อการแสดงผลที่ดียิ่งขึ้น

Tom Wiltzius
Tom Wiltzius

บทนำ

คุณต้องการให้เว็บแอปตอบสนองและทำงานได้อย่างราบรื่นเมื่อแสดงภาพเคลื่อนไหว การเปลี่ยนภาพ และเอฟเฟกต์ 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

สรุป

สรุปสั้นๆ ได้ดังนี้

  1. เมื่อสร้างภาพเคลื่อนไหว การสร้างเฟรมสำหรับการรีเฟรชหน้าจอทุกครั้งเป็นสิ่งสำคัญ ภาพเคลื่อนไหวที่ซิงค์กับ Vsync ส่งผลดีอย่างมากต่อความรู้สึกของผู้ใช้ที่มีต่อแอป
  2. วิธีที่ดีที่สุดในการสร้างภาพเคลื่อนไหวที่ซิงค์กับ VSync ใน Chrome และเบราว์เซอร์ที่ทันสมัยอื่นๆ คือการใช้ภาพเคลื่อนไหว CSS เมื่อคุณต้องการความยืดหยุ่นมากกว่าที่ภาพเคลื่อนไหว CSS มีให้ เทคนิคที่ดีที่สุดคือภาพเคลื่อนไหวที่ใช้ requestAnimationFrame
  3. เพื่อให้ภาพเคลื่อนไหว rAF ทำงานได้อย่างราบรื่นและมีประสิทธิภาพ โปรดตรวจสอบว่าเครื่องจัดการเหตุการณ์อื่นๆ ไม่ได้ขัดขวางการทำงานของ Callback ของ rAF และทำให้ Callback ของ rAF สั้น (<15 มิลลิวินาที)

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

ขอให้สนุกกับการสร้างภาพเคลื่อนไหว

ข้อมูลอ้างอิง