چطوری کدهای js پروژه مون رو با استفاده از تب performance مرورگر کروم بهینه سازی کنیم؟
سلام.
مرورگر یه نخ(thread) داره که تمام کار(task)های لازم برای نمایش صفحه رو توش انجام میده. این نخ، نخ اصلی یا main thread نام داره. کارهایی مثل اجرای کدهای جاوااسکریپت ، رندرینگ، تجزیه ی html , css و کارهایی که شاید ما روی اون ها کنترلی نداشته باشیم. . اگر این نخ (توسط یک تسک) مسدود بشه؛ یعنی یه تسک مقدار زمان زیادی برای اجرا به خودش اختصاص بده(حداقل 50 میلی ثانیه)؛ توی این نخ، مرورگر تسک های مهم و بحرانی رو نمی تونه انجام بده. در نتیجه این موضوع باعث میشه که :
- سرعت load پایین بیاد.
- واکنش گرایی صفحه نسبت به اکشن ها و درخواست های کاربر پایین بیاد(یعنی کندی در اجرا) (unresponsive بودن).
- تجربه ی کاربری هم بد بشه.
مهم ترین عوامل مسدود شدن نخ اصلی در مرورگر، تسک های بزرگ یا Long Task ها هستن که معمولا کدهای جاوااسکریپت ما هستن.
برای حل این مساله، ما باید long task ها رو شناسایی کنیم و بعد اون ها رو به تسک های کوچکتر جاوااسکریپتی بشکنیم و خرد کنیم.
برای یافتن long task ها، از تب performance مرورگر میشه استفاده کرد. به این صورت که اول دکمه ی رکوردش رو می زنیم و بعد صفحه رو رفرش می کنیم. بعد قسمت Main اش رو باز می کنیم. می بینیم که یکسری نوار با هاشور قرمز وجود داره که نمایانگر تسک های طولانی هستن. یه نمونه تو تصویر زیر داریم که مربوط به سایت دیجیکالا هست.
شکستن تسک های طولانی به تسک های کوچکتر، باعث میشه که مرورگر فرصتی بیشتری برای پاسخ به کارهای با اولویت بالاتر (یعنی تعامل با کاربر و بروزرسانی سریعتر UI) داشته باشه. پس این کار مهمه. برای شکستن تسک های طولانی، سه تا استراتژی وجود داره:
1) به تعویق انداختن دستی کدها
2) استفاده از async/await برای ایجاد نقاط yield.
3) استفاده از yield تنها وقتی که نیاز است.
در ادامه، توضیح هرکدوم از روش ها رو باهم بررسی می کنیم:
1) تو این استراتژی، از تابع setTimeout جاوااسکریپت استفاده میشه که توابع و کدهایی که درون callback این تابع قرار میگیرن، به تسک های جداگانه و مجزا تبدیل میشن و به تعویق می افتن.
مثالش رو میشه تو کد زیر باهم ببینیم. تو این کد، می خوایم تسک طولانی saveSettings رو به تسک های کوچکتر خرد کنیم. برای اینکار، توابع saveToDatabase , saveAnalytics (که برای کاربر قابل مشاهده نیستن) با استفاده از setTimeout ،به تسک های جداگانه ای نسبت یه سایر توابع تبدیل میشن و از این تسک طولانی جدا میشن.
این استراتژی برای مواقعیه که ما یکسری تابع داریم که پشت سرهم اجرا میشن.
function saveSettings () { // Do critical work that is user-visible: validateForm(); showSpinner(); updateUI(); // Defer work that isn't user-visible to a separate task: setTimeout(() => { saveToDatabase(); sendAnalytics(); }, 0); }
2) معنی yield کردن یعنی ایجاد وقفه در انجام یک تسک(برای ایجاد تسک با اولویت بیشتر). مثلا وقتی تسکی وجود داره که درحال بروزرسانی UI برای کاربر هست. تسک فعلی باید به وقفه بیافته(yield بشه). وقفه انداختن یک تسک، باعث میشه که مرورگر که تسک ها رو با مکانیزم درونی خودش اولویت بندی میکنه، تسک های با اولویت بالاتر زودتر انجام بده. تو این استراتژی، بعد از هر تسک(تابع) یک وقفه ایجاد میشه(مانند مثال پایین)
برای ایجاد yield، از Promise, setTimeout استفاده میشه که مثالش رو تو کد زیر می تونیم ببینیم. کد زیر، همون تسک طولانی saveSettings رو با استفاده از این استراتژی به تسک های کوچکتر تبدیل کرده. ابتدا یه تابع yieldToMain نوشته که هر کجا از کد لازم بود با استفاده از این تابع تو تسک درحال اجرای فعلی وقفه ایجاد کنه:
function yieldToMain () { return new Promise(resolve => { setTimeout(resolve, 0); }); }
بعد تسک طولانی saveSettings رو که از چندین تابع تشکیل شده، به تسک های کوچکتر تبدیل میکنه. یعنی بعد از هر تسک، یه وقفه(yield) داخل نخ اصلی ایجاد می کنه.
3) اما اگر بخوایم برعکس استراتژی بالا، هرجا نیاز بود در اجرای تسک طولانی فعلی وقفه ایجاد کنیم و به این شکل تسک طولانی رو خرد کنیم چطور؟ مثلا وقتی کاربر توی صفحه اکشنی انجام میده، توی تسک طولانی فعلی که درحال اجراست وقفه ایجاد کنیم، بعد تسک های مربوط به انجام کارهای کاربر رو انجام بدیم(مثلا بروزرسانی ui)، سپس به انجام تسک های پس زمینه ادامه بدیم چی؟؟
این استراتژی باعث میشه که کاربر هیچگونه کندی ای مشاهده نکنه و ما فقط هرجا که لازمه وقفه تو اجرای Long task ایجاد کنیم نه همه جا(ممکنه در هرجایی، ایجاد وقفه درون تسک طولانی لازم نباشه). برای این کار از یک تابع به اسم isInputPending که تو شی scheduler در شی navigator مرورگر قرار داره استفاده میشه. وقتی کاربر با صفحه تعاملی انجام میده، این تابع true بر می گردونه. نحوه ی استفاده اش هم تو مثال پایین اومده:
async function saveSettings () { // A task queue of functions const tasks = [ validateForm, showSpinner, saveToDatabase, updateUI, sendAnalytics ]; while (tasks.length > 0) { // Yield to a pending user input: if (navigator.scheduling.isInputPending()) { // There's a pending user input. Yield here: await yieldToMain(); } else { // Shift the task out of the queue: const task = tasks.shift(); // Run the task: task(); } } }
ما می خوایم یه long task به اسم saveSettings رو به تسک های کوچکتر بشکنیم. اول چک می کنیم که کاربر با صفحه تعامل داره یا نه؟ اگر داشت با استفاده از تابع yieldToMain (که تو مثال قبل نوشتیم)، وقفه رو ایجاد می کنیم. اما اگر تعاملی وجود نداشت، تسک کوچک شده ی فعلی رو اجرا می کنیم.
تمام کارهایی که ما در قسمت های بالا انجام دادیم، پیاده سازی بصورت دستی هستن. اما مرورگر خودش یک scheduler داره که به ما این امکان رو میده که کارهای بالا رو بصورت خودکار انجام بدیم. این api مرورگر، یه تابع به اسم postTask داره به سه اولویت:
مثالش رو هم در ادامه می تونیم ببینیم:
function saveSettings () { // Validate the form at high priority scheduler.postTask(validateForm, {priority: 'user-blocking'}); // Show the spinner at high priority: scheduler.postTask(showSpinner, {priority: 'user-blocking'}); // Update the database in the background: scheduler.postTask(saveToDatabase, {priority: 'background'}); // Update the user interface at high priority: scheduler.postTask(updateUI, {priority: 'user-blocking'}); // Send analytics data in the background: scheduler.postTask(sendAnalytics, {priority: 'background'}); };
منبع:
https://web.dev/optimize-long-tasks/?utm_source=devtools