رفع لرزش برای عملکرد بهتر در پردازش

Tom Wiltzius
Tom Wiltzius

مقدمه

می‌خواهید برنامه وب شما هنگام انجام پویانمایی‌ها، انتقال‌ها، و دیگر جلوه‌های کوچک واسط کاربر، واکنش‌گرا و روان باشد. اطمینان از اینکه این جلوه‌ها بدون پرش باشند می‌تواند تفاوت بین یک حس «بومی» یا یک حس ناهنجار و ناصاف را ایجاد کند.

این اولین مقاله از مجموعه‌ای از مقاله‌ها است که بهینه‌سازی عملکرد پرداز زدن در مرورگر را پوشش می‌دهد. برای شروع، توضیح می‌دهیم که چرا پویانمایی روان دشوار است و برای دستیابی به آن چه چیزی لازم است، و همچنین چند روال مطلوب آسان را معرفی می‌کنیم. بسیاری از این ایده‌ها در ابتدا در «Jank Busters» ارائه شدند، گفتگویی که من و نات دوکا امسال در Google I/O داشتیم (ویدیو).

معرفی همگام‌سازی عمودی

بازیکنان بازی‌های رایانه‌ای ممکن است با این اصطلاح آشنا باشند، اما در وب رایج نیست: v-sync چیست؟

نمایشگر تلفنتان را درنظر بگیرید: در فواصل زمانی منظم، معمولاً (اما نه همیشه!) حدود ۶۰ بار در ثانیه، بازآوری می‌شود. همگام‌سازی عمودی (یا V-sync) به تولید فریم‌های جدید فقط بین بازآوری‌های صفحه اشاره دارد. می‌توانید این را مانند یک شرایط رقابتی بین فرایندی که داده‌ها را در میان‌گیر صفحه می‌نویسد و سیستم‌عاملی که آن داده‌ها را می‌خواند تا روی نمایشگر قرار دهد، تصور کنید. ما می‌خواهیم محتوای قاب میان‌گیرشده بین این بازآوری‌ها تغییر کند، نه درطول آن‌ها؛ درغیراین‌صورت، نمایشگر نیمی از یک قاب و نیمی از قاب دیگر را نمایش می‌دهد که منجر به «پارگی» می‌شود.

برای داشتن پویانمایی روان، باید هربار که صفحه بازآوری می‌شود، قاب جدیدی آماده باشد. این امر دو پیامد بزرگ دارد: زمان‌بندی فریم (یعنی زمانی که فریم باید آماده شود) و بودجه فریم (یعنی مدت زمانی که مرورگر برای تولید فریم دارد). برای تکمیل یک قاب فقط به‌اندازه فاصله زمانی بین بازآوری‌های صفحه وقت دارید (حدود ۱۶ میلی‌ثانیه در صفحه ۶۰ هرتز)، و می‌خواهید تولید قاب بعدی را به‌محض قرار گرفتن قاب قبلی روی صفحه شروع کنید.

زمان‌بندی همه‌چیز است: requestAnimationFrame

بسیاری از توسعه‌دهندگان وب هر ۱۶ میلی‌ثانیه از setInterval یا setTimeout برای ایجاد پویانمایی استفاده می‌کنند. این موضوع به دلایل مختلفی مشکل‌ساز است (و به‌زودی بیشتر درباره آن صحبت خواهیم کرد)، اما نگرانی‌های خاص عبارتند از:

  • وضوح زمان‌سنج از جاوا اسکریپت فقط در حد چند میلی‌ثانیه است
  • دستگاه‌های مختلف نرخ بازآوری متفاوتی دارند

مشکل زمان‌بندی فریم را که در بالا ذکر شد به‌خاطر بیاورید: شما به یک فریم پویانمایی کامل نیاز دارید که با هرگونه جاوا اسکریپت، دستکاری DOM، چیدمان، رنگ‌آمیزی و غیره تکمیل شده باشد تا قبل‌از وقوع بازآوری بعدی صفحه آماده باشد. وضوح پایین زمان‌سنج می‌تواند تکمیل فریم‌های پویانمایی را قبل‌از بازآوری بعدی صفحه‌نمایش دشوار کند، اما تغییر در نرخ بازآوری صفحه‌نمایش این کار را با زمان‌سنج ثابت غیرممکن می‌کند. مهم نیست که فاصله زمانی زمان‌سنج چقدر باشد، به‌آرامی از پنجره زمان‌بندی برای یک قاب خارج می‌شوید و درنهایت یکی را ازدست می‌دهید. این اتفاق حتی اگر زمان‌سنج با دقت میلی‌ثانیه فعال شود نیز رخ می‌دهد، که البته این‌طور نخواهد بود (همان‌طور که توسعه‌دهندگان کشف کرده‌اند) -- وضوح زمان‌سنج بسته به اینکه دستگاه از باتری استفاده می‌کند یا به برق وصل است متفاوت است، می‌تواند تحت تأثیر برگه‌های پس‌زمینه که منابع را اشغال می‌کنند قرار گیرد، و غیره. حتی اگر این اتفاق نادر باشد (مثلاً هر ۱۶ فریم چون یک میلی‌ثانیه عقب بودید) متوجه خواهید شد: چند فریم در ثانیه را ازدست خواهید داد. همچنین، برای تولید قاب‌هایی که هرگز نمایش داده نمی‌شوند، کار انجام خواهید داد که باعث هدر رفتن انرژی و زمان واحد پردازش مرکزی می‌شود که می‌توانستید برای انجام کارهای دیگر در برنامه‌تان صرف کنید.

نمایشگرهای مختلف نرخ بازآوری متفاوتی دارند: ۶۰ هرتز رایج است، اما برخی‌از تلفن‌ها ۵۹ هرتز هستند، برخی‌از رایانه‌های کیفی در حالت کم‌مصرف به ۵۰ هرتز کاهش می‌یابند، و برخی‌از نمایشگرهای رایانه رومیزی ۷۰ هرتز هستند.

هنگام بحث درباره عملکرد پردازش، معمولاً روی فریم در ثانیه (FPS) تمرکز می‌کنیم، اما واریانس می‌تواند مشکل بزرگ‌تری باشد. چشمان ما متوجه وقفه‌های کوچک و نامنظم در پویانمایی می‌شود که ممکن است ناشی از زمان‌بندی ضعیف پویانمایی باشد.

راه دریافت قاب‌های پویانمایی با زمان‌بندی صحیح، استفاده از requestAnimationFrame است. وقتی از این میانای برنامه‌سازی کاربردی استفاده می‌کنید، از مرورگر درخواست قاب پویانمایی می‌کنید. وقتی مرورگر به‌زودی قاب جدیدی تولید کند، فراخوانی برگشتی شما فراخوانده می‌شود. این اتفاق بدون توجه به نرخ بازآوری رخ می‌دهد.

requestAnimationFrame ویژگی‌های خوب دیگری هم دارد:

  • پویانمایی‌ها در برگه‌های پس‌زمینه موقتاً متوقف می‌شوند تا منابع سیستم و عمر باتری حفظ شود.
  • اگر سیستم نتواند رندر کردن را با نرخ بازآوری صفحه‌نمایش انجام دهد، می‌تواند پویانمایی‌ها را محدود کند و فراخوانی برگشتی را با دفعات کمتری تولید کند (مثلاً ۳۰ بار در ثانیه در صفحه‌نمایش ۶۰ هرتز). اگرچه این کار نرخ فریم را به نصف کاهش می‌دهد، اما پویانمایی را ثابت نگه می‌دارد -- و همان‌طور که در بالا گفته شد، چشمان ما نسبت‌به نرخ فریم، با تغییرات بسیار هماهنگ‌تر است. ۳۰ هرتز ثابت بهتر از ۶۰ هرتز است که چند فریم در ثانیه را ازدست می‌دهد.

requestAnimationFrame قبلاً در همه‌جا مورد بحث قرار گرفته است، بنابراین برای اطلاعات بیشتر درباره آن به مقالاتی مانند این مقاله از JS خلاق مراجعه کنید، اما این یک گام مهم اولیه برای پویانمایی روان است.

بودجه فریم

ازآنجایی‌که می‌خواهیم در هر بازآوری صفحه یک قاب جدید آماده باشد، فقط زمان بین بازآوری‌ها برای انجام تمام کارهای ایجاد قاب جدید وجود دارد. در نمایشگر ۶۰ هرتز، این یعنی حدود ۱۶ میلی‌ثانیه فرصت داریم تا همه جاوا اسکریپت‌ها را اجرا کنیم، چیدمان را انجام دهیم، رنگ‌آمیزی کنیم، و هر کار دیگری را که مرورگر باید برای ارائه فریم انجام دهد انجام دهیم. یعنی اگر اجرای جاوا اسکریپت داخل فراخوانی برگشتی requestAnimationFrame شما بیشتر از ۱۶ میلی‌ثانیه طول بکشد، هیچ امیدی به تولید به‌موقع فریم برای همگام‌سازی عمودی ندارید!

۱۶ میلی‌ثانیه زمان زیادی نیست. خوشبختانه «ابزارهای توسعه‌دهنده» Chrome می‌تواند به شما کمک کند تا متوجه شوید که آیا درطول فراخوانی برگشتی requestAnimationFrame بودجه قاب خود را ازدست می‌دهید یا نه.

Opening up the Dev Tools timeline and taking a recording of this animation in action quickly shows that we're way over budget when animating. در «خط زمان»، به «قاب‌ها» بروید و نگاهی بیندازید:

نسخه نمایشی با چیدمان بسیار زیاد
نسخه نمایشی با چیدمان بسیار زیاد

آن فراخوانی‌های برگشتی requestAnimationFrame (rAF) بیشتر از ۲۰۰ میلی‌ثانیه طول می‌کشند. این مقدار خیلی طولانی است و نمی‌تواند هر ۱۶ میلی‌ثانیه یک قاب را پرداز کند! باز کردن یکی از آن فراخوان‌های طولانی rAF نشان می‌دهد که در داخل چه اتفاقی می‌افتد: در این مورد، چیدمان‌های زیادی وجود دارد.

ویدیو Paul جزئیات بیشتری درباره علت خاص بازچینی (خواندن scrollTop) و نحوه اجتناب از آن ارائه می‌دهد. اما نکته این است که می‌توانید به فراخوانی برگشتی بروید و بررسی کنید که چرا این‌قدر طول می‌کشد.

نسخه نمایشی به‌روزشده با چیدمان بسیار کاهش‌یافته
نمونه نمایشی به‌روزشده با چیدمان بسیار کاهش‌یافته

به زمان پردازش فریم ۱۶ میلی‌ثانیه‌ای توجه کنید. آن فضای خالی در قاب‌ها، فضای خالی‌ای است که برای انجام کارهای بیشتر دارید (یا به مرورگر اجازه می‌دهد کارهایی را که باید در پس‌زمینه انجام دهد، انجام دهد). آن فضای خالی چیز خوبی است.

منبع دیگر پرش تصویر

بزرگ‌ترین علت مشکل هنگام تلاش برای اجرای پویانمایی‌های مبتنی بر جاوا اسکریپت این است که چیزهای دیگر می‌توانند در مسیر فراخوانی rAF شما قرار بگیرند و حتی از اجرای آن جلوگیری کنند. از اجرای آن جلوگیری کنند. Even if your rAF callback is lean and runs in just a few milliseconds, other activities (like processing an XHR that just came in, running input event handlers, or running scheduled updates on a timer) can suddenly come in and run for any period of time without yielding. در دستگاه‌های همراه، گاهی پردازش این رویدادها می‌تواند صدها میلی‌ثانیه طول بکشد و در این مدت، پویانمایی شما کاملاً متوقف خواهد شد. به این وقفه‌های پویانمایی پرش می‌گوییم.

هیچ راه‌حل جادویی برای اجتناب از این موقعیت‌ها وجود ندارد، اما چند روال مطلوب معماری وجود دارد که شما را برای موفقیت آماده می‌کند:

  • Don't do a lot of processing in input handlers! انجام دادن مقدار زیادی جاوا اسکریپت یا تلاش برای بازآرایی کل صفحه درطول مثلاً یک کنترل‌کننده onscroll، علت بسیار رایج لرزش وحشتناک است.
  • تا جایی که ممکن است پردازش (بخوانید: هر چیزی که اجرای آن زمان زیادی می‌برد) را به callback rAF یا Web Workers منتقل کنید.
  • اگر کار را به فراخوان برگشتی rAF منتقل می‌کنید، سعی کنید آن را تکه‌تکه کنید تا در هر فریم فقط مقدار کمی را پردازش کنید یا آن را تا پس‌از پایان پویانمایی مهمی به‌تأخیر بیندازید -- با این روش می‌توانید به اجرای فراخوان‌های برگشتی rAF کوتاه ادامه دهید و پویانمایی را به‌طور روان اجرا کنید.

برای آموزش گام‌به‌گام عالی درباره نحوه انتقال پردازش به callbackهای requestAnimationFrame به‌جای کنترل‌کننده‌های ورودی، مقاله Paul Lewis با عنوان Leaner, Meaner, Faster Animations with requestAnimationFrame را ببینید.

پویانمایی CSS

چه چیزی بهتر از JS سبک در رویداد شما و فراخوانی‌های rAF؟ جاوااسکریپت وجود ندارد.

پیش‌تر گفتیم که هیچ راه حل جادویی‌ای برای جلوگیری از قطع شدن callbackهای rAF وجود ندارد، اما می‌توانید از پویانمایی CSS استفاده کنید تا نیاز به آن‌ها را به‌طور کامل ازبین ببرید. به‌ویژه در Chrome برای Android (و مرورگرهای دیگر که روی ویژگی‌های مشابه کار می‌کنند)، پویانمایی‌های CSS ویژگی بسیار مطلوبی دارند که مرورگر اغلب می‌تواند آن‌ها را اجرا کند، حتی اگر جاوا اسکریپت درحال اجرا باشد.

در بخش بالا درباره پرش، یک عبارت ضمنی وجود دارد: مرورگرها فقط می‌توانند یک کار را در یک زمان انجام دهند. این کاملاً درست نیست، اما فرض کاری خوبی است: در هر زمان معین، مرورگر می‌تواند JS را اجرا کند، چیدمان را انجام دهد، یا نقاشی کند، اما فقط یکی در یک زمان. این موضوع را می‌توان در نمای «خط زمان» در Dev Tools درستی‌سنجی کرد. یکی از استثناهای این قانون، پویانمایی‌های CSS در Chrome برای Android (و به‌زودی Chrome برای رایانه، هرچند هنوز نه) است.

درصورت امکان، استفاده از پویانمایی CSS هم برنامه شما را ساده می‌کند و هم باعث می‌شود پویانمایی‌ها به‌آرامی اجرا شوند، حتی زمانی که جاوا اسکریپت اجرا می‌شود.

  // 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);

اگر روی دکمه کلیک کنید، جاوا اسکریپت به‌مدت ۱۸۰ میلی‌ثانیه اجرا می‌شود و باعث پرش می‌شود. اما اگر به‌جای آن، پویانمایی را با پویانمایی‌های 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. هنگام پویانمایی، تولید قاب برای هر بازآوری صفحه‌نمایش اهمیت دارد. پویانمایی همگام‌سازی عمودی تأثیر مثبت بسیار زیادی بر حس برنامه می‌گذارد.
  2. بهترین راه برای دریافت پویانمایی همگام‌سازی‌شده با نرخ بازآوری در Chrome و دیگر مرورگرهای مدرن، استفاده از پویانمایی CSS است. وقتی به انعطاف‌پذیری بیشتری نسبت به پویانمایی CSS نیاز دارید، بهترین تکنیک پویانمایی مبتنی بر requestAnimationFrame است.
  3. برای اینکه پویانمایی‌های rAF سالم و شاداب بمانند، مطمئن شوید که سایر گرداننده‌های رویداد مانع اجرای فراخوانی rAF شما نمی‌شوند و فراخوانی‌های rAF را کوتاه نگه دارید (کمتر از ۱۵ میلی‌ثانیه).

درنهایت، پویانمایی همگام‌سازی عمودی فقط برای پویانمایی‌های ساده میانای کاربر اعمال نمی‌شود، بلکه برای پویانمایی Canvas2D، پویانمایی WebGL، و حتی پیمایش در صفحه‌های ایستا نیز اعمال می‌شود. در مقاله بعدی این مجموعه، با درنظر گرفتن این مفاهیم، به عملکرد پیمایش خواهیم پرداخت.

از پویانمایی لذت ببرید!

مرجع‌ها