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

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 ครั้งต่อวินาทีในหน้าจอ 60 Hz) แม้ว่าวิธีนี้จะลดอัตราเฟรมลงครึ่งหนึ่ง แต่ก็จะคงภาพเคลื่อนไหวให้สม่ำเสมอ และตามที่ได้กล่าวไว้ข้างต้น ดวงตาของเราปรับให้เข้ากับความแปรปรวนได้มากกว่าอัตราเฟรม อัตรา 30 Hz ที่ราบรื่นจะดูดีกว่า 60 Hz ที่ขาดเฟรม 2-3 เฟรมต่อวินาที

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

กำหนดงบประมาณ

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

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

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

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

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

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

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

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

แหล่งที่มาอื่นๆ ของปัญหา

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

คงไม่มีวิธีแก้ปัญหาที่ได้ผลแน่นอนในการหลีกเลี่ยงสถานการณ์เหล่านี้ แต่มีแนวทางปฏิบัติแนะนำด้านสถาปัตยกรรมบางอย่างที่จะช่วยให้คุณประสบความสำเร็จได้ ดังนี้

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

ดูบทแนะนำที่ยอดเยี่ยมเกี่ยวกับวิธีส่งการประมวลผลไปยังการเรียกกลับของ requestAnimationFrame แทนตัวแฮนเดิลอินพุตได้ที่บทความของ Paul Lewis เรื่องภาพเคลื่อนไหวที่เบาลง มีประสิทธิภาพมากขึ้น และเร็วขึ้นด้วย requestAnimationFrame

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

มีอะไรดีกว่า JS ที่มีน้ำหนักเบาในเหตุการณ์และการเรียกกลับ rAF ไหม ไม่มี JS

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

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

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

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

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