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

Tom Wiltzius
Tom Wiltzius

เกริ่นนำ

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

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

ขอแนะนำ V-sync

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

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

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

เวลาคือทุกสิ่ง: requestAnimationFrame

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

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

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

จอแสดงผลแต่ละแบบจะมีอัตราการรีเฟรชที่แตกต่างกัน: ความถี่ 60Hz เป็นเรื่องปกติ แต่โทรศัพท์บางรุ่นจะเป็นแบบ 59Hz, แล็ปท็อปบางรุ่นจะมีอัตราการรีเฟรชที่ 50Hz และ) ขณะที่จอแสดงผลเดสก์ท็อปบางเครื่องจะเป็น 70Hz

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

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

requestAnimationFrame มีคุณสมบัติดีๆ อื่นๆ ด้วย:

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

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

งบประมาณของเฟรม

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

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

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

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

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

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

การสาธิตที่อัปเดตแล้วที่มีเลย์เอาต์ลดลงอย่างมาก
เดโมที่อัปเดตใหม่ซึ่งลดเลย์เอาต์ลงมาก

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

แหล่งอื่นๆ ของเจงค์

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

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

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

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

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

อะไรดีกว่า JS ขนาดเล็กในกิจกรรมและโค้ดเรียกกลับ rAF ไม่มี JS

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

มีข้อความโดยนัยในส่วนด้านบนเกี่ยวกับคำว่า Jank: เบราว์เซอร์สามารถดำเนินการได้เพียงครั้งละ 1 อย่างเท่านั้น ซึ่งไม่ได้เป็นเช่นนั้นจริง แต่ก็เป็นสมมติฐานที่ดีที่ต้องมี เช่น ให้เบราว์เซอร์สามารถเรียกใช้ JS, แสดงเลย์เอาต์ หรือลงสีได้ทีละ 1 รายการเท่านั้น ซึ่งยืนยันได้ในมุมมองไทม์ไลน์ของเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ ข้อยกเว้นอย่างหนึ่งของกฎนี้คือภาพเคลื่อนไหว 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'd ใน Chrome และเบราว์เซอร์รุ่นใหม่อื่นๆ คือ การใช้ภาพเคลื่อนไหว CSS หากต้องการความยืดหยุ่นที่มากกว่าภาพเคลื่อนไหว CSS ที่มีให้ เทคนิคที่ดีที่สุดคือภาพเคลื่อนไหวแบบ requestAnimationFrame
  3. โปรดตรวจสอบว่าตัวแฮนเดิลเหตุการณ์อื่นๆ ไม่ขัดขวางการเรียกกลับ rAF และช่วยให้ภาพเคลื่อนไหว rAF ทำงานอย่างถูกต้องและราบรื่น และใช้การติดต่อกลับ rAF ให้สั้น (น้อยกว่า 15 มิลลิวินาที)

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

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

รายการอ้างอิง