توصیههای رایج برای سریعتر کردن برنامههای جاوا اسکریپت اغلب شامل «رشته اصلی را مسدود نکنید» و «تکلیف طولانیتان را جدا کنید» است. این صفحه معنای این توصیه و اینکه چرا بهینه سازی وظایف در جاوا اسکریپت مهم است را توضیح می دهد.
تکلیف چیست؟
وظیفه ، هر کار مجزایی است که مرورگر انجام می دهد. این شامل رندر کردن، تجزیه HTML و CSS، اجرای کد جاوا اسکریپتی که می نویسید و موارد دیگری است که ممکن است کنترل مستقیمی روی آنها نداشته باشید. جاوا اسکریپت صفحات شما منبع اصلی وظایف مرورگر است.
وظایف به روش های مختلفی بر عملکرد تأثیر می گذارد. به عنوان مثال، هنگامی که مرورگر یک فایل جاوا اسکریپت را در حین راه اندازی دانلود می کند، وظایف را در صف می گذارد تا جاوا اسکریپت را تجزیه و کامپایل کند تا بتوان آن را اجرا کرد. بعداً در چرخه عمر صفحه، وقتی جاوا اسکریپت شما کار میکند، کارهای دیگر شروع میشوند، مانند ایجاد تعاملات از طریق کنترلکنندههای رویداد، انیمیشنهای مبتنی بر جاوا اسکریپت، و فعالیتهای پسزمینه مانند مجموعه تحلیلی. همه اینها، به استثنای وب کارگران و API های مشابه، در رشته اصلی اتفاق می افتد.
موضوع اصلی چیست؟
موضوع اصلی جایی است که اکثر وظایف در مرورگر اجرا می شوند و تقریباً تمام جاوا اسکریپتی که می نویسید در آن اجرا می شود.
رشته اصلی فقط می تواند یک کار را در یک زمان پردازش کند. هر کاری که بیش از 50 میلی ثانیه طول بکشد به عنوان یک کار طولانی محسوب می شود. اگر کاربر سعی کند در طول یک کار طولانی یا یک بهروزرسانی رندر با صفحه تعامل داشته باشد، مرورگر باید منتظر بماند تا آن تعامل را مدیریت کند و باعث تأخیر شود.
برای جلوگیری از این امر، هر کار طولانی را به کارهای کوچکتر تقسیم کنید که اجرای هر کدام زمان کمتری می برد. به این می گویند از بین بردن کارهای طولانی.
شکستن وظایف به مرورگر فرصت بیشتری برای پاسخگویی به کارهای با اولویت بالاتر، از جمله تعاملات کاربر، بین سایر وظایف می دهد. این باعث میشود تا تعاملات بسیار سریعتر اتفاق بیفتد، جایی که کاربر ممکن است در غیر این صورت متوجه تاخیر شود در حالی که مرورگر منتظر پایان کار طولانی است.
استراتژی های مدیریت وظیفه
جاوا اسکریپت هر تابع را به عنوان یک وظیفه واحد در نظر می گیرد، زیرا از یک مدل اجرا تا تکمیل برای اجرای کار استفاده می کند. این بدان معناست که تابعی که چندین تابع دیگر را فراخوانی میکند، مانند مثال زیر، باید تا زمانی که تمام توابع فراخوانی شده کامل شوند، اجرا شود، که سرعت مرورگر را کند میکند:
function saveSettings () { //This is a long task.
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
اگر کد شما حاوی توابعی است که چندین متد را فراخوانی می کند، آن را به چند تابع تقسیم کنید. این نه تنها به مرورگر فرصت های بیشتری برای پاسخگویی به تعامل می دهد، بلکه خواندن، نگهداری و نوشتن تست های کد شما را آسان تر می کند. در بخشهای زیر چند استراتژی برای تفکیک عملکردهای طولانی و اولویتبندی وظایفی که آنها را تشکیل میدهند، توضیح میدهند.
به تعویق انداختن دستی اجرای کد
می توانید اجرای برخی وظایف را با ارسال تابع مربوطه به setTimeout()
به تعویق بیندازید. این کار حتی اگر مهلت زمانی 0
را مشخص کنید کار می کند.
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);
}
این برای یک سری از توابع که باید به ترتیب اجرا شوند، بهترین کار را دارد. کدهایی که به طور متفاوتی سازماندهی شده اند به رویکرد متفاوتی نیاز دارند. مثال بعدی تابعی است که حجم زیادی از داده ها را با استفاده از یک حلقه پردازش می کند. هرچه مجموعه داده بزرگتر باشد، زمان بیشتری طول می کشد، و لزوماً جای خوبی در حلقه برای قرار دادن setTimeout()
وجود ندارد:
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
خوشبختانه، چند API دیگر وجود دارند که به شما امکان می دهند اجرای کد را به یک کار بعدی موکول کنید. توصیه میکنیم از postMessage()
برای زمانبندی سریعتر استفاده کنید .
همچنین میتوانید با استفاده از requestIdleCallback()
کار را جدا کنید، اما وظایف را با کمترین اولویت و فقط در زمان بیکاری مرورگر زمانبندی میکند، به این معنی که اگر رشته اصلی بهویژه شلوغ باشد، کارهای برنامهریزیشده با requestIdleCallback()
ممکن است هرگز اجرا نشوند.
از async
/ await
برای ایجاد نقاط بازده استفاده کنید
برای اطمینان از اینکه کارهای مهم پیش روی کاربر قبل از کارهای با اولویت پایینتر اتفاق میافتند، با قطع کوتاهی صف وظایف ، به موضوع اصلی تسلیم شوید تا به مرورگر فرصتهایی برای اجرای کارهای مهمتر بدهید.
واضح ترین راه برای انجام این کار شامل یک Promise
است که با فراخوانی به setTimeout()
حل می شود:
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
در تابع saveSettings()
، اگر بعد از هر فراخوانی تابع await
تابع yieldToMain()
باشید، می توانید پس از هر مرحله به رشته اصلی تسلیم شوید. این کار به طور موثر وظیفه طولانی شما را به چندین کار تقسیم می کند:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread:
await yieldToMain();
}
}
نکته کلیدی: لازم نیست پس از هر فراخوانی تابع تسلیم شوید. به عنوان مثال، اگر دو عملکرد را اجرا می کنید که منجر به به روز رسانی های مهم در رابط کاربری می شود، احتمالاً نمی خواهید بین آنها تسلیم شوید. اگر میتوانید، ابتدا اجازه دهید آن کار اجرا شود، سپس بین توابعی که پسزمینه انجام میدهند یا کارهای کمتر مهمی که کاربر نمیبیند، تسلیم شوید.
یک API زمانبندی اختصاصی
API های ذکر شده تا کنون می توانند به شما کمک کنند تا وظایف را تقسیم کنید، اما آنها یک نقطه ضعف قابل توجه دارند: وقتی با به تعویق انداختن کد برای اجرای کار بعدی، به رشته اصلی تسلیم می شوید، آن کد به انتهای صف کار اضافه می شود.
اگر تمام کدهای صفحه خود را کنترل می کنید، می توانید زمانبندی خود را برای اولویت بندی کارها ایجاد کنید. با این حال، اسکریپتهای شخص ثالث از زمانبندی شما استفاده نمیکنند، بنابراین نمیتوانید در این مورد واقعاً کار را اولویتبندی کنید . شما فقط می توانید آن را تجزیه کنید یا تسلیم تعاملات کاربر شوید.
API برنامهریزی تابع postTask()
را ارائه میکند که به برنامهریزی دقیقتر وظایف اجازه میدهد و میتواند به مرورگر کمک کند کار را اولویتبندی کند تا وظایف با اولویت پایین به رشته اصلی تسلیم شوند. postTask()
از وعدهها استفاده میکند و تنظیمات priority
میپذیرد.
API postTask()
دارای سه اولویت است:
-
'background'
برای وظایف با کمترین اولویت. -
'user-visible'
برای وظایف با اولویت متوسط. اگرpriority
تنظیم نشده باشد، این پیشفرض است. -
'user-blocking'
برای کارهای حیاتی که باید با اولویت بالا اجرا شوند.
کد مثال زیر از 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'});
};
در اینجا، اولویت وظایف برنامهریزی میشود تا وظایف اولویتبندی شده با مرورگر مانند تعاملات کاربر بتوانند به خوبی انجام شوند.
همچنین میتوانید اشیاء مختلف TaskController
را که اولویتها را بین وظایف به اشتراک میگذارند، از جمله توانایی تغییر اولویتها برای نمونههای مختلف TaskController
در صورت نیاز، نمونهسازی کنید.
بازده داخلی با استفاده از API scheduler.yield()
آینده
نکته کلیدی: برای توضیح دقیقتر scheduler.yield()
، در مورد آزمایش اولیه آن (از زمان به نتیجه رسیدن)، و همچنین توضیح دهنده آن را بخوانید.
یکی از افزودنیهای پیشنهادی به API زمانبند scheduler.yield()
است، یک API که به طور خاص برای تسلیم شدن به رشته اصلی در مرورگر طراحی شده است. استفاده از آن شبیه تابع yieldToMain()
است که قبلا در این صفحه نشان داده شده بود:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread with the scheduler
// API's own yielding mechanism:
await scheduler.yield();
}
}
این کد تا حد زیادی آشناست، اما به جای استفاده از yieldToMain()
از await scheduler.yield()
استفاده می کند.
مزیت scheduler.yield()
continuation است، به این معنی که اگر در وسط یک مجموعه از وظایف تسلیم شوید، سایر وظایف زمان بندی شده به همان ترتیب بعد از نقطه بازده ادامه می یابند. این امر مانع از آن می شود که اسکریپت های شخص ثالث کنترل ترتیبی را که کد شما در آن اجرا می کند، در دست بگیرند.
استفاده از scheduler.postTask()
با priority: 'user-blocking'
همچنین به دلیل اولویت بالای user-blocking
احتمال ادامه آن زیاد است، بنابراین می توانید از آن به عنوان جایگزین استفاده کنید تا زمانی که scheduler.yield()
به طور گسترده در دسترس قرار گیرد.
استفاده از setTimeout()
(یا scheduler.postTask()
با priority: 'user-visible'
or no explicit priority
) کار را در پشت صف زمان بندی می کند و به سایر وظایف معلق اجازه می دهد قبل از ادامه اجرا شوند.
بازده ورودی با isInputPending()
پشتیبانی مرورگر
- 87
- 87
- ایکس
- ایکس
API isInputPending()
راهی برای بررسی اینکه آیا کاربر سعی کرده است با یک صفحه تعامل داشته باشد یا خیر، ارائه می دهد و تنها در صورتی که ورودی معلق باشد، نتیجه می دهد.
این به جاوا اسکریپت اجازه میدهد در صورتی که هیچ ورودی معلقی وجود نداشته باشد، به جای تسلیم شدن و پایان در پشت صف کار، ادامه دهد. این می تواند منجر به بهبود عملکرد چشمگیر شود، همانطور که در Intent to Ship توضیح داده شده است، برای سایت هایی که در غیر این صورت ممکن است به موضوع اصلی باز نگردند.
با این حال، از زمان راه اندازی آن API، درک ما از بازده بهبود یافته است، به خصوص پس از معرفی INP. ما دیگر استفاده از این API را توصیه نمی کنیم ، و در عوض توصیه می کنیم صرف نظر از اینکه ورودی در حال تعلیق است یا خیر، تسلیم شوید. این تغییر در توصیه ها به چند دلیل است:
- API ممکن است در برخی موارد که کاربر تعامل داشته باشد، اشتباهاً
false
برگرداند. - ورودی تنها موردی نیست که وظایف باید نتیجه دهند. انیمیشنها و سایر بهروزرسانیهای معمولی رابط کاربری میتوانند به همان اندازه برای ارائه یک صفحه وب واکنشگرا مهم باشند.
- از آن زمان APIهای بازده جامع تری مانند
scheduler.postTask()
وscheduler.yield()
برای رفع نگرانی های بازده معرفی شده اند.
نتیجه
مدیریت وظایف چالش برانگیز است، اما انجام این کار به صفحه شما کمک می کند سریعتر به تعاملات کاربر پاسخ دهد. تکنیک های مختلفی برای مدیریت و اولویت بندی وظایف بسته به مورد استفاده شما وجود دارد. برای تکرار، اینها موارد اصلی هستند که باید هنگام مدیریت وظایف در نظر بگیرید:
- تسلیم موضوع اصلی برای کارهای حیاتی و مورد نظر کاربر شوید.
- آزمایش با
scheduler.yield()
را در نظر بگیرید. - با
postTask()
وظایف را اولویت بندی کنید. - در نهایت، تا حد امکان در عملکردهای خود کمتر کار کنید.
با استفاده از یک یا چند مورد از این ابزارها، باید بتوانید کار را در برنامه خود ساختار دهید تا نیازهای کاربر را در اولویت قرار دهد و در عین حال اطمینان حاصل کنید که کارهای مهم کمتر هنوز انجام می شود. این کار تجربه کاربر را با پاسخگویی بیشتر و استفاده لذت بخش تر، بهبود می بخشد.
تشکر ویژه از فیلیپ والتون برای بررسی فنی این سند.
تصویر بندانگشتی برگرفته از Unsplash توسط امیرعلی میرهاشمیان .