به شما گفته شده "رشته اصلی را مسدود نکنید" و "کارهای طولانی خود را از بین ببرید"، اما انجام این کارها به چه معناست؟
توصیه های رایج برای سریع نگه داشتن برنامه های جاوا اسکریپت به توصیه های زیر خلاصه می شود:
- "رشته اصلی را مسدود نکنید."
- "کارهای طولانی خود را از بین ببرید."
این توصیه عالی است، اما چه کاری شامل می شود؟ ارسال جاوا اسکریپت کمتر خوب است، اما آیا این به طور خودکار با رابط های کاربری پاسخگوتر برابر است؟ شاید، اما شاید نه.
برای درک نحوه بهینه سازی وظایف در جاوا اسکریپت، ابتدا باید بدانید که وظایف چیست و مرورگر چگونه آنها را مدیریت می کند.
تکلیف چیست؟
وظیفه ، هر کار مجزایی است که مرورگر انجام می دهد. این کار شامل رندر کردن، تجزیه HTML و CSS، اجرای جاوا اسکریپت و انواع دیگری از کارهایی است که ممکن است کنترل مستقیمی روی آنها نداشته باشید. از بین همه اینها، جاوا اسکریپتی که می نویسید شاید بزرگترین منبع وظایف باشد.
وظایف مرتبط با جاوا اسکریپت از چند طریق بر عملکرد تأثیر می گذارد:
- هنگامی که یک مرورگر یک فایل جاوا اسکریپت را در حین راه اندازی دانلود می کند، وظایف را برای تجزیه و کامپایل کردن آن جاوا اسکریپت در صف قرار می دهد تا بعداً اجرا شود.
- در زمانهای دیگر در طول عمر صفحه، وقتی جاوا اسکریپت کارهایی مانند ایجاد تعاملات از طریق کنترلکنندههای رویداد، انیمیشنهای مبتنی بر جاوا اسکریپت و فعالیتهای پسزمینه مانند مجموعه تحلیلی انجام میدهد، در صف قرار میگیرند.
همه این موارد - به استثنای وب کارگران و API های مشابه - در موضوع اصلی اتفاق می افتد.
موضوع اصلی چیست؟
موضوع اصلی جایی است که اکثر وظایف در مرورگر اجرا می شوند و تقریباً تمام جاوا اسکریپتی که می نویسید در آن اجرا می شود.
رشته اصلی فقط می تواند یک کار را در یک زمان پردازش کند. هر کاری که بیش از 50 میلی ثانیه طول بکشد، کار طولانی است. برای کارهایی که بیش از 50 میلی ثانیه هستند، کل زمان کار منهای 50 میلی ثانیه به عنوان دوره مسدود کردن کار شناخته می شود.
مرورگر از وقوع تعاملات در حین اجرای یک کار با هر طولی جلوگیری می کند، اما تا زمانی که کارها برای مدت طولانی اجرا نشوند، این برای کاربر قابل درک نیست. با این حال، هنگامی که یک کاربر سعی می کند با یک صفحه تعامل داشته باشد، در حالی که وظایف طولانی مدت زیادی وجود دارد، رابط کاربری احساس می کند که پاسخگو نیست، و حتی اگر رشته اصلی برای مدت زمان بسیار طولانی مسدود شود، حتی ممکن است خراب شود.
برای جلوگیری از مسدود شدن بیش از حد رشته اصلی، می توانید یک کار طولانی را به چندین کار کوچکتر تقسیم کنید.
این مهم است زیرا هنگامی که وظایف از هم جدا می شوند، مرورگر می تواند خیلی زودتر به کارهای با اولویت بالاتر - از جمله تعاملات کاربر - پاسخ دهد. پس از آن، کارهای باقیمانده تا پایان کامل می شوند، و اطمینان حاصل می شود که کارهایی که در ابتدا در صف قرار داده اید، انجام می شود.
در بالای شکل قبل، یک کنترل کننده رویداد که توسط یک تعامل کاربر در صف قرار می گیرد، باید قبل از شروع یک کار طولانی منتظر می ماند، این امر باعث به تاخیر افتادن تعامل می شود. در این سناریو، کاربر ممکن است متوجه تاخیر شده باشد. در پایین، کنترل کننده رویداد میتواند زودتر شروع به کار کند، و ممکن است تعامل آنی احساس شود.
اکنون که می دانید چرا جدا کردن وظایف مهم است، می توانید نحوه انجام این کار را در جاوا اسکریپت بیاموزید.
استراتژی های مدیریت وظیفه
یک توصیه رایج در معماری نرم افزار این است که کار خود را به عملکردهای کوچکتر تقسیم کنید:
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
در این مثال، تابعی به نام saveSettings()
وجود دارد که پنج تابع را برای اعتبارسنجی یک فرم، نشان دادن یک اسپینر، ارسال داده ها به پشتیبان برنامه، به روز رسانی رابط کاربری و ارسال تجزیه و تحلیل فراخوانی می کند.
از نظر مفهومی، saveSettings()
به خوبی طراحی شده است. اگر نیاز به اشکال زدایی یکی از این توابع دارید، می توانید درخت پروژه را طی کنید تا بفهمید هر تابع چه کاری انجام می دهد. شکستن کارهایی مانند این، هدایت و نگهداری پروژه ها را آسان تر می کند.
با این حال، یک مشکل بالقوه در اینجا این است که جاوا اسکریپت هر یک از این توابع را به عنوان وظایف جداگانه اجرا نمی کند زیرا آنها در تابع saveSettings()
اجرا می شوند. این بدان معنی است که هر پنج تابع به عنوان یک وظیفه اجرا می شوند.
در بهترین حالت، حتی تنها یکی از آن توابع می تواند 50 میلی ثانیه یا بیشتر به طول کل کار کمک کند. در بدترین حالت، تعداد بیشتری از این وظایف می توانند بسیار طولانی تر اجرا شوند - به خصوص در دستگاه های دارای محدودیت منابع.
به تعویق انداختن دستی اجرای کد
یکی از روش هایی که توسعه دهندگان برای تقسیم وظایف به کارهای کوچکتر استفاده کرده اند، شامل setTimeout()
است. با این تکنیک، تابع را به 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);
}
این به عنوان yielding شناخته می شود و برای یک سری از توابع که باید به صورت متوالی اجرا شوند بهترین کار را دارد.
با این حال، ممکن است کد شما همیشه به این شکل سازماندهی نشود. به عنوان مثال، شما می توانید حجم زیادی از داده ها را داشته باشید که باید در یک حلقه پردازش شوند، و اگر تکرارهای زیادی وجود داشته باشد، این کار ممکن است زمان بسیار زیادی طول بکشد.
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
استفاده از setTimeout()
در اینجا به دلیل ارگونومی توسعهدهنده مشکلساز است و کل آرایه دادهها ممکن است زمان بسیار زیادی طول بکشد تا پردازش شود، حتی اگر هر تکرار بهسرعت اجرا شود. همه چیز اضافه می شود و setTimeout()
ابزار مناسبی برای این کار نیست – حداقل در صورت استفاده از این روش.
از async
/ await
برای ایجاد نقاط بازده استفاده کنید
برای اطمینان از اینکه کارهای مهم پیش روی کاربر قبل از کارهای با اولویت پایینتر اتفاق میافتند، میتوانید با قطع مختصر صف کار، به موضوع اصلی تسلیم شوید تا به مرورگر فرصتهایی برای اجرای کارهای مهمتر بدهید.
همانطور که قبلا توضیح داده شد، setTimeout
می تواند برای تسلیم شدن به رشته اصلی استفاده شود. با این حال، برای راحتی و خوانایی بهتر، میتوانید setTimeout
در یک Promise
فراخوانی کنید و روش resolve
آن را بهعنوان پاسخ به تماس ارسال کنید.
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
مزیت تابع yieldToMain()
این است که می توانید در هر تابع async
await
آن باشید. با استفاده از مثال قبلی، میتوانید آرایهای از توابع برای اجرا ایجاد کنید و پس از اجرای هر کدام، به رشته اصلی تسلیم شوید:
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 زمانبندی اختصاصی
setTimeout
یک راه موثر برای جدا کردن وظایف است، اما می تواند یک اشکال داشته باشد: وقتی با به تعویق انداختن کد برای اجرای یک کار بعدی، به رشته اصلی تسلیم می شوید، آن کار به انتهای صف اضافه می شود.
اگر تمام کدهای صفحه خود را کنترل کنید، میتوانید زمانبندی خود را با قابلیت اولویتبندی وظایف ایجاد کنید - اما اسکریپتهای شخص ثالث از زمانبندی شما استفاده نمیکنند. در واقع، شما نمی توانید در چنین محیط هایی کار را اولویت بندی کنید . شما فقط می توانید آن را تکه تکه کنید یا به طور صریح به تعاملات کاربر تسلیم شوید.
API برنامهریزی تابع postTask()
را ارائه میکند که امکان زمانبندی دقیقتری از وظایف را فراهم میکند، و یکی از راههای کمک به مرورگر برای اولویتبندی کارها بهگونهای است که وظایف با اولویت پایین به رشته اصلی تسلیم شوند. postTask()
از وعده ها استفاده می کند و یکی از سه تنظیمات priority
را می پذیرد:
-
'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'});
};
در اینجا، اولویت وظایف به گونهای برنامهریزی میشود که وظایف اولویتبندی شده با مرورگر - مانند تعاملات کاربر - در صورت لزوم بتوانند در بین آنها کار کنند.
این یک مثال ساده از نحوه استفاده از postTask()
است. امکان نمونه سازی اشیاء مختلف TaskController
وجود دارد که می توانند اولویت ها را بین وظایف به اشتراک بگذارند، از جمله توانایی تغییر اولویت ها برای نمونه های مختلف TaskController
در صورت نیاز.
بازده داخلی با استفاده از API scheduler.yield()
ادامه دارد
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 است، به این معنی که اگر در وسط مجموعه ای از وظایف تسلیم شوید، سایر وظایف زمان بندی شده به همان ترتیب پس از نقطه بازده ادامه خواهند داشت. با این کار کدهای اسکریپت های شخص ثالث مانع از قطع شدن ترتیب اجرای کد شما می شوند.
از isInputPending()
استفاده نکنید
API isInputPending()
راهی برای بررسی اینکه آیا کاربر سعی کرده است با یک صفحه تعامل داشته باشد یا خیر، ارائه میکند و تنها در صورتی نتیجه میدهد که ورودی معلق باشد.
این به جاوا اسکریپت اجازه میدهد در صورتی که هیچ ورودی معلقی وجود نداشته باشد، به جای تسلیم شدن و پایان در پشت صف کار، ادامه دهد. این می تواند منجر به بهبود عملکرد چشمگیر شود، همانطور که در Intent to Ship توضیح داده شده است، برای سایت هایی که در غیر این صورت ممکن است به موضوع اصلی باز نگردند.
با این حال، از زمان راه اندازی آن API، درک ما از بازده افزایش یافته است، به ویژه با معرفی INP. ما دیگر استفاده از این API را توصیه نمیکنیم ، و در عوض توصیه میکنیم صرفنظر از اینکه ورودی معلق است یا خیر، به دلایلی تسلیم شوید:
-
isInputPending()
ممکن است علیرغم تعامل کاربر در برخی شرایط، به اشتباهfalse
را برگرداند. - ورودی تنها موردی نیست که وظایف باید نتیجه دهند. انیمیشنها و سایر بهروزرسانیهای معمولی رابط کاربری میتوانند به همان اندازه برای ارائه یک صفحه وب واکنشگرا مهم باشند.
- از آن زمان، APIهای بازده جامع تری معرفی شده اند که به نگرانی های بازده، مانند
scheduler.postTask()
وscheduler.yield()
رسیدگی می کنند.
نتیجه گیری
مدیریت وظایف چالش برانگیز است، اما انجام این کار تضمین می کند که صفحه شما سریعتر به تعاملات کاربر پاسخ می دهد. هیچ توصیه واحدی برای مدیریت و اولویت بندی وظایف وجود ندارد، بلکه تعدادی تکنیک مختلف وجود دارد. برای تکرار، اینها موارد اصلی هستند که باید هنگام مدیریت وظایف در نظر بگیرید:
- تسلیم موضوع اصلی برای کارهای حیاتی و مورد نظر کاربر شوید.
- با
postTask()
وظایف را اولویت بندی کنید. - آزمایش با
scheduler.yield()
را در نظر بگیرید. - در نهایت، تا حد امکان در عملکردهای خود کمتر کار کنید.
با استفاده از یک یا چند مورد از این ابزارها، باید بتوانید کار را در برنامه خود ساختار دهید تا نیازهای کاربر را در اولویت قرار دهد و در عین حال اطمینان حاصل کنید که کارهای مهم کمتری همچنان انجام می شود. این باعث ایجاد تجربه کاربری بهتری می شود که واکنش پذیرتر و استفاده لذت بخش تر است.
تشکر ویژه از فیلیپ والتون برای بررسی فنی این راهنما.
تصویر بندانگشتی برگرفته از Unsplash توسط امیرعلی میرهاشمیان .