بهینه‌سازی مرحله‌ی Scripting

معمولاً کدهای جاوااسکریپت تغییراتی در ظاهر صفحه ایجاد می‌کنند که از این پس به این تغییرات «تغییرات ظاهری» می‌گوییم. گاهی اوقات این کار مستقیماً از طریق دست‌کاری استایل‌ها انجام می‌شود و بعضی وقت‌ها این تغییرات ظاهری نتیجه‌ی محاسبات و پردازش‌هایی خاص است، مثل جستجو یا مرتّب‌سازی اطّلاعات. معمولاً مشکل اصلی بهینه نبودن این مرحله به خاطر طولانی شدن اجرای کد جاوااسکریپت یا زمان‌بندی‌های نادرست است.

خلاصه

 

  • برای انجام تغییرات ظاهری از setInterval و setTimeout استفاده نکنید؛ همیشه از requestAnimationFrame به جای آن استفاده کنید.
  • کدهایی که اجرای آن‌ها طول می‌کشد را از thread اصلی بیرون ببرید و در یک Web Worker قرار دهید.
  • تغییرات DOM را در طیّ چند فریم، ریز ریز انجام دهید.
  • برای فهمیدن اینکه کد جاوااسکریپت شما چه قدر در نرم و روان بودن صفحه تأثیر دارد، از ابزار Performance در DevTools مرورگر کروم استفاده کنید.

برای انجام تغییرات ظاهری از requestAnimationFrame استفاده کنید

وقتی کد شما باعث تغییرات ظاهری می‌شود باید کاری کنید که این کد در زمانی اجرا شود که برای مرورگر مناسب‌تر است و این زمان، زمانِ آغاز فریم است. تنها راهی که در آن می‌توان مطمئن بود که کد شما در آغاز یک فریم اجرا می‌شود استفاده از requestAnimationFrame است.

// اگر این تابع به عنوان ورودی requestAnimationFrame استفاده شود
// دستورات داخل آن در آغاز فریم بعدی اجرا خواهند شد
function updateScreen(time) {
  // تغییرات ظاهری را در اینجا انجام دهید
}

requestAnimationFrame(updateScreen);

در بعضی فریم‌ورک‌ها یا نمونه‌کدهایی که می‌بینید ممکن است از setTimeout یا setInterval برای تغییرات ظاهری مثل انیمیشن، استفاده کرده‌باشند. مشکل این روش در آن است که دستورات مورد نظر معلوم نیست در چه نقطه‌ای از فریم اجرا بشوند، شاید درست در انتهای آن اجرا شوند، و در بیشتر مواقع این کار موجب از دست رفتن یک فریم و در نتیجه ایجاد jank می‌شود.

نمودار زمانی مشکل timeout در pixel pipeline

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

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

پیچیدگی کد را کاهش دهید یا از Web Worker استفاده کنید

کدهای جاوااسکریپت در thread اصلی مرورگر در کنار مرحله‌های محاسبه‌ی استایل، صفحه‌آرایی، و در خیلی از موارد درکنار مرحله‌ی ترسیم اجرا می‌شود. یعنی در حین اجرای کدهای جاوااسکریپت، مرورگر هیچ کار دیگری انجام نمی‌دهد. اگر اجرای کد جاوااسکریپت زیاد طول بکشد کارهای دیگر بلاک می‌شوند و این باعث از دست رفتن فریم‌ها و در نتیجه jank می‌شود.

در مورد اینکه کد جاوااسکریپت در چه موقع و چه مدّت‌زمانی می‌تواند بدون مشکل اجرا شود باید تاکتیک داشت. مثلاً فرض کنید انیمیشنی مثل اسکرول صفحه دارد اجرا می‌شود، در این حین اجرای کد جاوااسکریپت شما نباید بیشتر از ۳ یا ۴ میلی‌ثانیه طول بکشد. هر چه قدر بیشتر از این طول بکشد ریسک jank را زیاد می‌کند. ولی اگر در یک بازه‌ی زمانی کار خاصّی مثل انیمیشن انجام نشود زمان بیشتری دارید و در این موقعیّت اگر اجرای کد جاوااسکریپت شما زیادتر طول بکشد عیبی ندارد.

در اکثر مواقع می‌توان کارهای محاسباتی خالص را به Web Workerها منتقل کرد. به شرط این‌که به چیزهایی مثل دسترسی به DOM نیاز نداشته باشد. معمولاً کارهای مربوط به دست‌کاری یا برآورد اطّلاعات، مثل مرتّب‌سازی یا جستجو برای این مدل مناسب هستند.

var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);

// حالا thread اصلی سرش خلوت شد و می‌تواند به کارهایش برسد...

dataSortWorker.addEventListener('message', function(evt) {
   var sortedData = evt.data;
   // به روزرسانی اطّلاعات صفحه...
});

انجام هر جور کاری با این مدل ممکن نیست: Web Workerها به DOM دسترسی ندارند. در مواقعی که ناچاراً کدتان باید در thread اصلی اجرا شود از روش دسته‌بندی استفاده کنید. یعنی کاری که می‌خواهید انجام شود را به تعدادی کار کوچک بشکنید، که هر کدام از این کارهای کوچک فقط چند میلی‌ثانیه طول می‌کشند. حالا در هر فریم فقط یکی از این کارها را با استفاده از requestAnimationFrame انجام می‌دهیم.

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);

function processTaskList(taskStartTime) {
  var taskFinishTime;

  do {
    // فرض گرفتیم کارهایی که میخواهد انجام شود درون یک آرایه هستند
    var nextTask = taskList.pop();

    // کار کوچک مورد نظر انجام می‌شود
    processTask(nextTask);

    // اگر زمان کافی برای انجام کار کوچک بعدی داشتیم آن را هم انجام می‌دهیم
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);

  if (taskList.length > 0)
    requestAnimationFrame(processTaskList);

}

اگر از این روش استفاده می‌کنید باید حواستان به UI و UX هم باشد. مثلاً مادامی که هنوز همه‌ی این کارهای کوچک تمام نشده‌اند به کاربر Loading نشان دهید. در هر صورت این روش سر thread اصلی را خلوت می‌کند و باعث می‌شود که در حین اجرای این دستورات، به کارهایی که کاربر انجام می‌دهد پاسخگو باشد.

هزینه‌ای که کد جاوااسکریپت برای هر فریم صرف می‌کند را بدانید

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

پنل Performance در DevTools مرورگر کروم بهترین راه برای فهمیدن هزینه‌ی کدهای جاوااسکریپت شماست. با کمک این ابزار می‌توانید در یک بازه‌ی زمانی شروع به ضبط صفحه کنید و بعد از کار با صفحه ضبط را متوقّف کنید و سپس نتایج اتّفاقاتی که از لحاظ Performance در مدّت ضبط صفحه اتّفاق افتاده را ببینید. معمولاً نتایج ضبط صفحه به شکل زیر است:

پنل performance

در قسمت Main، نمودار کدهای جاوااسکریپت اجرا شده را می‌بینید که از طریق آن می‌توانید بررسی کنید که کدام تابع‌ها اجرا شده‌اند و چه قدر طول کشیده‌اند.

با فهمیدن این اطَلاعات متوجّه می‌شوید که کدهای جاوااسکریپت چه تأثیری در عملکرد برنامه دارد، و می‌توانید نقاط مهمّی را که اجرایشان خیلی طول می‌کشد را پیدا و درست کنید. همان طور که قبلاً گفتیم برای درست کردن آن می‌توانید کد جاوااسکریپتِ طولانی‌مدّت را حذف کنید، یا اگر امکانش نیست، آن را به Web Worker منتقل کنید تا thread اصلی سرش خلوت شود و به کارهای دیگرش برسد.

برای یادگیری نحوه‌ی کار با پنل Performance، آموزش Get Started With Analyzing Runtime Performance را ببینید.

روی کد جاوااسکریپت خود بهینه‌سازی جزئی انجام ندهید

خوب است بدانید که مرورگر ممکن است یک کار را با روشی متفاوت ۱۰۰ برابر سریع‌تر انجام دهد. مثلاً گرفتن offsetTop یک عنصر سریع‌تر از getBoundingClientRect() است. ولی معمولاً از این توابع در هر فریم خیلی کم استفاده می‌شود. بنابراین کار بیهوده‌ایست که روی این نوع بهینه‌سازی تمرکز کنید. با این کار کمتر از یک‌هزارم ثانیه در زمان صرفه‌جویی می‌کنید.

اگر دارید یک بازی تحت وب، یا اپلیکیشنی که محاسبات سنگین انجام می‌دهد می‌سازید، استثناء است.  چون معمولاً در یک بازی در هر فریم محاسبات بسیار زیادی انجام می‌گیرد و در این طور موارد، بهینه‌سازی هرچیزی می‌تواند کمک‌تان کند. ولی در وب‌اپلیکیشن‌های معمولی که می‌سازیم واقعاً این قدر جزئی وارد شدن و بهینه کردن هیچ تأثیری ندارد.

2 نظر در مورد “بهینه‌سازی مرحله‌ی Scripting

  1. محسن سلیمانی

    سلام لطفا بگید چطور یه اسلایدر رو با این ترفند فعال کنم؟ یعنی بجای Setinterval از requestAnimationFrame استفاده کنم؟

    1. مجتبی قاسم زاده تهرانی

      سلام علیکم.
      نه، بلکه به صورت ترکیبی ازشون استفاده کنید. مثلاً
      setInterval(() => {
      requestAnimationFrame(() => {
      // کد شما
      })
      }, 1000)

      این کار باعث میشه مشکل مطرح شده به وجود نیاد.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *