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

Tom Wiltzius
Tom Wiltzius

บทนำ

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

บทความนี้เป็นเรื่องแรกในชุดของบทความที่กล่าวถึงการเพิ่มประสิทธิภาพการแสดงผลในเบราว์เซอร์ ในการเริ่มต้น เราจะอธิบายว่าทำไมการสร้างภาพเคลื่อนไหวที่ราบรื่นจึงเป็นเรื่องยากและสิ่งที่ต้องทำเพื่อให้บรรลุเป้าหมายนั้น รวมถึงแนวทางปฏิบัติแนะนำง่ายๆ เพียงไม่กี่ข้อ ไอเดียเหล่านี้ส่วนใหญ่ถูกนำเสนอใน "Jank Busters" Nat Duca และฉันเคยพูดคุยในงาน Google I/O (วิดีโอ) ปีนี้

ขอแนะนำ V-sync

เกมเมอร์ที่ใช้ PC อาจคุ้นเคยกับคำนี้ แต่พบไม่บ่อยนักบนเว็บ นั่นก็คือ v-sync คืออะไร

พิจารณาจอแสดงผลของโทรศัพท์ โดยจะรีเฟรชเป็นช่วงเวลาที่สม่ำเสมอ โดยทั่วไป (แต่ไม่เสมอไป) ประมาณ 60 ครั้งต่อวินาที V-sync (หรือการซิงค์ในแนวตั้ง) หมายถึงการสร้างเฟรมใหม่ระหว่างการรีเฟรชหน้าจอเท่านั้น คุณอาจคิดว่ากรณีนี้เป็นเหมือนสภาวะการแข่งขันระหว่างกระบวนการที่เขียนข้อมูลลงในบัฟเฟอร์หน้าจอและระบบปฏิบัติการที่อ่านข้อมูลดังกล่าวเพื่อแสดงบนจอแสดงผล เราต้องการให้เนื้อหาในเฟรมที่บัฟเฟอร์เปลี่ยนไปในระหว่างการรีเฟรชเหล่านี้ ไม่ใช่ระหว่างการรีเฟรช ไม่เช่นนั้น จอภาพจะแสดงครึ่งหนึ่งของเฟรมหนึ่งและอีกครึ่งหนึ่งของเฟรมอื่นทำให้เกิด "การฉีกขาด"

หากต้องการให้ภาพเคลื่อนไหวลื่นไหล คุณต้องเตรียมเฟรมใหม่ให้พร้อมทุกครั้งที่มีการรีเฟรชหน้าจอ โดยมีนัยสำคัญ 2 ประการ ได้แก่ จังหวะเวลาของเฟรม (นั่นคือเมื่อเฟรมต้องพร้อมใช้งาน) และงบประมาณของเฟรม (ซึ่งก็คือระยะเวลาที่เบราว์เซอร์ใช้ในการผลิตเฟรม) คุณมีเวลาระหว่างการรีเฟรชหน้าจอให้เสร็จสิ้น 1 เฟรมเท่านั้น (ประมาณ 16 มิลลิวินาทีบนหน้าจอ 60 Hz) และคุณต้องการเริ่มสร้างเฟรมถัดไปทันทีที่เฟรมสุดท้ายปรากฏขึ้นบนหน้าจอ

จังหวะเวลาสำคัญที่สุด: requestAnimationFrame

นักพัฒนาเว็บจำนวนมากใช้ setInterval หรือ setTimeout ทุกๆ 16 มิลลิวินาทีเพื่อสร้างภาพเคลื่อนไหว ปัญหานี้เกิดจากหลายสาเหตุ (และเราจะกล่าวถึงเพิ่มเติมในอีกสักครู่) แต่ข้อกังวลประการหนึ่งมีดังนี้

  • ความละเอียดของตัวจับเวลาจาก JavaScript เป็นไปตามลำดับหลายมิลลิวินาทีเท่านั้น
  • อุปกรณ์ต่างๆ มีอัตราการรีเฟรชต่างกัน

เรียกคืนปัญหาการจับเวลาเฟรมที่กล่าวถึงข้างต้น คุณต้องมีเฟรมภาพเคลื่อนไหวที่เสร็จสมบูรณ์ โดยใช้ JavaScript จัดการ DOM, เลย์เอาต์, ลงสี และอื่นๆ เสร็จแล้ว เพื่อเตรียมพร้อมก่อนการรีเฟรชหน้าจอครั้งถัดไป ความละเอียดของตัวจับเวลาที่ต่ำอาจทำให้เฟรมภาพเคลื่อนไหวเสร็จสมบูรณ์ก่อนการรีเฟรชหน้าจอครั้งถัดไป แต่การเปลี่ยนแปลงอัตราการรีเฟรชหน้าจอทำให้ไม่สามารถใช้ตัวจับเวลาคงที่ ไม่ว่าช่วงเวลาของตัวจับเวลาจะเป็นเท่าใด คุณจะค่อยๆ หลุดออกจากกรอบเวลาของกรอบเวลาหนึ่งและจบลงด้วยการทิ้งเฟรมหนึ่งๆ เหตุการณ์นี้จะเกิดขึ้นแม้ว่าตัวจับเวลาจะทำงานด้วยความแม่นยำในมิลลิวินาทีซึ่งจะไม่เกิดขึ้น (ตามที่นักพัฒนาซอฟต์แวร์ค้นพบ) ความละเอียดของตัวจับเวลาจะแตกต่างกันไปขึ้นอยู่กับว่าเครื่องกำลังใช้แบตเตอรี่หรือเสียบปลั๊กอยู่ อาจได้รับผลกระทบจากแท็บที่ใช้พลังงานจากแท็บเบื้องหลัง ฯลฯ แม้จะทำได้ยาก (เช่น ทุกๆ 16 เฟรมเนื่องจากออกไปหนึ่งมิลลิวินาที) คุณก็จะเห็นว่าเฟรมของคุณลดลงหลายเฟรมหรือหลายเฟรม นอกจากนี้ คุณยังต้องสร้างเฟรมที่ไม่มีการแสดงผล ซึ่งจะสิ้นเปลืองพลังงานและเวลา CPU ไปกับการทำอย่างอื่นในแอปพลิเคชัน

จอแสดงผลแต่ละแบบมีอัตราการรีเฟรชต่างกัน กล่าวคือ 60 Hz เป็นเรื่องปกติ แต่โทรศัพท์บางรุ่นเป็นแบบ 59Hz แล็ปท็อปบางรุ่นจะลดเหลือ 50Hz ในโหมดใช้พลังงานต่ำ ส่วนจอภาพเดสก์ท็อปบางรุ่นเป็นแบบ 70Hz

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

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

requestAnimationFrame มีคุณสมบัติที่ดีอื่นๆ เช่นกัน:

  • ภาพเคลื่อนไหวในแท็บเบื้องหลังจะหยุดชั่วคราว เพื่อรักษาทรัพยากรของระบบและอายุการใช้งานแบตเตอรี่
  • หากระบบไม่สามารถจัดการการแสดงผลในอัตรารีเฟรชของหน้าจอ ระบบก็สามารถควบคุมภาพเคลื่อนไหวและสร้าง Callback ให้ถี่น้อยลง (เช่น 30 ครั้งต่อวินาทีบนหน้าจอ 60Hz) แม้ว่าภาพนี้จะลดอัตราเฟรมลงครึ่งหนึ่ง แต่จะทำให้ภาพเคลื่อนไหวมีความสม่ำเสมอ และอย่างที่ได้กล่าวไปแล้วข้างต้น เราปรับสายตาที่แปรผันมากกว่าอัตราเฟรมเป็นอย่างมาก ความถี่ 30 Hz ที่คงที่จะดีกว่า 60Hz ที่รับแสงไป 2-3 เฟรมต่อวินาที

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

จัดกรอบงบประมาณ

เนื่องจากเราต้องการเฟรมใหม่ที่พร้อมใช้งานทุกครั้งที่มีการรีเฟรชหน้าจอ จึงมีเวลาระหว่างการรีเฟรชทั้งหมดเพื่อสร้างเฟรมใหม่เท่านั้น บนจอแสดงผล 60Hz นั่นหมายความว่าเรามีเวลาประมาณ 16 มิลลิวินาทีในการเรียกใช้ JavaScript ทั้งหมด, ทำการเลย์เอาต์, ลงสี และอะไรก็ตามที่เบราว์เซอร์ต้องทำเพื่อเคลียร์เฟรม ซึ่งหมายความว่าหาก JavaScript ภายใน Callback ของ requestAnimationFrame ใช้เวลานานกว่า 16 มิลลิวินาทีในการทำงาน คุณก็ไม่มีทางหวังว่าจะสร้างเฟรมได้ทันเวลาสำหรับ v-sync เลย

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

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

วันที่ การสาธิตที่มีเลย์เอาต์มากเกินไป
การสาธิตที่มีเลย์เอาต์มากเกินไป

การติดต่อกลับ requestAnimationFrame (rAF) เหล่านั้นใช้เวลา >200 มิลลิวินาที ซึ่งเป็นลำดับของขนาดที่ยาวเกินกว่าจะทำเครื่องหมายในเฟรมทุกๆ 16 มิลลิวินาที! การเปิดหนึ่งใน Callback rAF ที่ยาวๆ พวกนั้นจะเผยให้เห็นสิ่งที่เกิดขึ้นภายใน ในกรณีนี้คือเลย์เอาต์จำนวนมาก

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

วันที่ การสาธิตที่อัปเดตพร้อมการออกแบบที่เล็กลงมาก
การสาธิตที่อัปเดตพร้อมทั้งปรับเลย์เอาต์ที่ลดลงอย่างมาก

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

แหล่งที่มาอื่นๆ ของ Jank

สาเหตุหลักของปัญหาเมื่อพยายามเรียกใช้ภาพเคลื่อนไหวที่ทำงานด้วยระบบ JavaScript รายการอื่นๆ อาจเข้ามาขัดขวางการเรียกกลับ rAF ของคุณ และแม้กระทั่งป้องกัน ไม่ทำงานเลย แม้ว่าการเรียกกลับ rAF ของคุณจะเป็นแบบ Lean และทำงานได้เพียงไม่กี่ครั้ง มิลลิวินาที กิจกรรมอื่นๆ (เช่น การประมวลผล XHR ที่เพิ่งเข้ามา ใช้งานเครื่องจัดการเหตุการณ์อินพุต หรือเรียกใช้การอัปเดตที่กำหนดเวลาไว้บนตัวจับเวลา) ได้ เข้ามาและวิ่งในช่วงเวลาใดก็ได้โดยไม่ยอมแพ้ บนอุปกรณ์เคลื่อนที่ ในบางครั้ง การประมวลผลเหตุการณ์เหล่านี้ อาจใช้เวลาเป็นร้อยมิลลิวินาที ระหว่างนั้นภาพเคลื่อนไหวของคุณจะหยุดทำงาน ซึ่งเราเรียกว่า ภาพเคลื่อนไหวค้างการกระตุก

ไม่มีวิธีพิเศษใดในการหลีกเลี่ยงสถานการณ์เหล่านี้ แต่มีแนวทางปฏิบัติแนะนำด้านสถาปัตยกรรม 2-3 ข้อในการเตรียมตัวคุณให้พร้อมสำหรับความสำเร็จ

  • ไม่ต้องประมวลผลมากในเครื่องจัดการอินพุต มี JS จำนวนมากหรือพยายามจัดเรียงหน้าใหม่ทั้งหมดในระหว่างนั้น เช่น ตัวแฮนเดิล onscroll เป็นสาเหตุที่พบบ่อยที่สุดของความไม่สบายที่แย่มาก
  • พุชการประมวลผลให้มากที่สุด (อ่าน: ทุกสิ่งที่ใช้เวลานานในการทำงาน) ไปยัง Callback rAF หรือ Web Workers ของคุณให้ได้มากที่สุด
  • หากคุณพุชงานเข้าไปใน Callback rAF ให้พยายามแยกงานออกจากกันเพื่อให้ประมวลผลทีละเฟรม หรือเลื่อนเวลาออกไปจนกว่าภาพเคลื่อนไหวที่สำคัญจะจบลง วิธีนี้จะช่วยให้คุณสามารถเรียกใช้ Callback rAF ระยะสั้นๆ และทำให้เคลื่อนไหวได้อย่างราบรื่นต่อไป

หากต้องการดูบทแนะนำที่ยอดเยี่ยมซึ่งครอบคลุมวิธีพุชการประมวลผลไปยัง requestAnimationFrame Callback แทนตัวแฮนเดิลอินพุต โปรดดูบทความ Leaner, Meaner, Retry Animations with requestAnimationFrame ของ Paul Lewis

ภาพเคลื่อนไหว CSS

อะไรจะดีไปกว่า JS ขนาดเล็กในกิจกรรมของคุณและ Callback rAF ไม่มี JS

ก่อนหน้านี้เราบอกว่าไม่มีวิธีที่มีประสิทธิภาพในการหลีกเลี่ยงการรบกวนการเรียกกลับ rAF แต่คุณใช้ภาพเคลื่อนไหว CSS เพื่อหลีกเลี่ยงการรบกวนการเรียกกลับของ rAF ได้ โดยเฉพาะใน Chrome สำหรับ Android (และเบราว์เซอร์อื่นๆ ที่นำเสนอคุณลักษณะที่คล้ายคลึงกัน) ภาพเคลื่อนไหว CSS มีคุณสมบัติที่เหมาะสมอย่างยิ่งซึ่งเบราว์เซอร์มักจะสามารถเรียกใช้ได้แม้ว่า JavaScript จะทำงานอยู่ก็ตาม

มีข้อความโดยนัยในส่วนด้านบนเกี่ยวกับ Jank ว่าเบราว์เซอร์ทำงานได้ครั้งละ 1 อย่างเท่านั้น ซึ่งไม่เป็นความจริงแต่อย่างใด แต่เป็นข้อสันนิษฐานที่ดีที่ว่าในช่วงเวลาหนึ่งๆ เบราว์เซอร์จะเรียกใช้ 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 และแม้กระทั่งการเลื่อนบนหน้าแบบคงที่ ในบทความถัดไปของชุดนี้ เราจะเจาะลึกลงไปในประสิทธิภาพการเลื่อนโดยคำนึงถึงแนวคิดเหล่านี้

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

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