به شما گفته شده "رشته اصلی را مسدود نکنید" و "کارهای طولانی خود را از بین ببرید"، اما انجام این کارها به چه معناست؟
اگر مطالب زیادی در مورد عملکرد وب میخوانید، توصیههایی برای سریع نگه داشتن برنامههای جاوا اسکریپت شما شامل برخی از این نکات است:
- "رشته اصلی را مسدود نکنید."
- "کارهای طولانی خود را از بین ببرید."
هر کدام از اینها به چه معناست؟ ارسال جاوا اسکریپت کمتر خوب است، اما آیا این به طور خودکار با رابط های کاربری سریعتر در طول چرخه عمر صفحه برابر است؟ شاید، اما شاید نه.
برای اینکه بفهمید چرا بهینه سازی وظایف در جاوا اسکریپت مهم است، باید نقش وظایف و نحوه رسیدگی مرورگر به آنها را بدانید – و این با درک چیستی یک کار شروع می شود.
تکلیف چیست؟
وظیفه ، هر کار مجزایی است که مرورگر انجام می دهد. وظایف شامل کارهایی مانند رندر کردن، تجزیه HTML و CSS، اجرای کد جاوا اسکریپتی که می نویسید و موارد دیگری است که ممکن است کنترل مستقیمی روی آنها نداشته باشید. از بین همه اینها، جاوا اسکریپتی که می نویسید و در وب قرار می دهید منبع اصلی وظایف است.
وظایف از چند جهت بر عملکرد تأثیر می گذارد. به عنوان مثال، هنگامی که مرورگر یک فایل جاوا اسکریپت را در حین راه اندازی دانلود می کند، وظایف را در صف می گذارد تا جاوا اسکریپت را تجزیه و کامپایل کند تا بتوان آن را اجرا کرد. بعداً در چرخه عمر صفحه، هنگامی که جاوا اسکریپت شما کارهایی مانند ایجاد تعاملات از طریق کنترل کننده رویداد، انیمیشن های مبتنی بر جاوا اسکریپت، و فعالیت های پس زمینه مانند مجموعه تجزیه و تحلیل انجام می دهد، شروع به کار می کند. همه این موارد - به استثنای وب کارگران و API های مشابه - در موضوع اصلی اتفاق می افتد.
موضوع اصلی چیست؟
موضوع اصلی جایی است که اکثر وظایف در مرورگر اجرا می شوند. به یک دلیل به آن رشته اصلی می گویند: این رشته ای است که تقریباً تمام جاوا اسکریپتی که می نویسید کار خود را انجام می دهد.
رشته اصلی فقط می تواند یک کار را در یک زمان پردازش کند. هنگامی که وظایف فراتر از یک نقطه خاص - به طور دقیق 50 میلی ثانیه - به عنوان وظایف طولانی طبقه بندی می شوند. اگر کاربر در حین اجرای یک کار طولانی سعی در تعامل با صفحه داشته باشد - یا اگر یک بهروزرسانی مهم رندر باید اتفاق بیفتد، مرورگر در انجام آن کار به تأخیر میافتد. این منجر به تعامل یا تأخیر رندر می شود.
شما باید وظایف را از هم جدا کنید . این به این معنی است که یک کار طولانی را انجام دهید و آن را به کارهای کوچکتر تقسیم کنید که زمان کمتری برای اجرای جداگانه نیاز دارد.
این مهم است زیرا وقتی وظایف تقسیم می شوند، مرورگر فرصت های بیشتری برای پاسخگویی به کارهای با اولویت بالاتر دارد - و این شامل تعاملات کاربر نیز می شود.
در بالای شکل قبل، یک کنترل کننده رویداد که توسط یک تعامل کاربر در صف قرار می گیرد، باید برای یک کار طولانی منتظر بماند تا بتواند اجرا شود، این باعث می شود که تعامل انجام شود. در پایین، کنترل کننده رویداد این شانس را دارد که زودتر اجرا شود. از آنجا که کنترل کننده رویداد فرصتی برای اجرا در بین وظایف کوچکتر داشت، زودتر از زمانی که مجبور بود منتظر پایان کار طولانی باشد، اجرا می شود. در مثال بالا، کاربر ممکن است متوجه تاخیر شده باشد. در پایین، ممکن است تعامل آنی احساس شود.
با این حال، مشکل این است که توصیه "کارهای طولانی خود را از بین ببرید" و "رشته اصلی را مسدود نکنید" به اندازه کافی مشخص نیست، مگر اینکه از قبل بدانید که چگونه این کارها را انجام دهید. این چیزی است که این راهنما توضیح خواهد داد.
استراتژی های مدیریت وظیفه
یک توصیه رایج در معماری نرم افزار این است که کار خود را به عملکردهای کوچکتر تقسیم کنید. این به شما مزایای خوانایی بهتر کد و قابلیت نگهداری پروژه را می دهد. این همچنین نوشتن تست ها را آسان تر می کند.
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
در این مثال، تابعی به نام 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);
}
اگر یک سری توابع دارید که باید به صورت متوالی اجرا شوند، این کار به خوبی کار می کند، اما ممکن است کد شما همیشه به این شکل سازماندهی نشود. به عنوان مثال، میتوانید حجم زیادی از دادهها را داشته باشید که باید در یک حلقه پردازش شوند، و اگر میلیونها آیتم داشته باشید، این کار ممکن است زمان زیادی طول بکشد.
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
استفاده از setTimeout()
در اینجا مشکل ساز است، زیرا ارگونومی آن اجرای آن را دشوار می کند، و کل آرایه داده ممکن است زمان بسیار زیادی برای پردازش نیاز داشته باشد، حتی اگر هر آیتم را بتوان خیلی سریع پردازش کرد. همه چیز اضافه می شود و setTimeout()
ابزار مناسبی برای این کار نیست – حداقل در صورت استفاده از این روش.
علاوه بر setTimeout()
، چند API دیگر وجود دارد که به شما امکان می دهد اجرای کد را به یک کار بعدی موکول کنید. یکی شامل استفاده از postMessage()
برای زمانبندی سریعتر است . همچنین میتوانید با استفاده از requestIdleCallback()
کار را جدا کنید —اما مراقب باشید!— 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();
}
}
نتیجه این است که وظیفه زمانی یکپارچه اکنون به وظایف جداگانه تقسیم می شود.
مزیت استفاده از رویکرد مبتنی بر وعده برای بازده به جای استفاده دستی از setTimeout()
ارگونومی بهتر است. نقاط بازده بیانی می شوند و بنابراین نوشتن، خواندن و درک آن آسان تر است.
محصول فقط در صورت لزوم
اگر تعدادی کار دارید، اما تنها در صورتی که کاربر سعی کند با صفحه تعامل داشته باشد، میخواهید تسلیم شوید، چه؟ این همان چیزی است که isInputPending()
برای آن ساخته شده است.
isInputPending()
تابعی است که می توانید آن را در هر زمان اجرا کنید تا مشخص کنید آیا کاربر در حال تلاش برای تعامل با یک عنصر صفحه است یا خیر: فراخوانی به isInputPending()
true
را برمی گرداند. در غیر این صورت false
برمی گرداند.
فرض کنید یک صف از وظایفی دارید که باید اجرا کنید، اما نمیخواهید مانع هیچ ورودی شوید. این کد - که از هم isInputPending()
و هم تابع سفارشی yieldToMain()
ما استفاده می کند - تضمین می کند که در زمانی که کاربر سعی می کند با صفحه تعامل داشته باشد، ورودی به تأخیر نیفتد:
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();
}
}
}
در حالی که saveSettings()
اجرا می شود، روی وظایف موجود در صف حلقه می زند. اگر isInputPending()
در طول حلقه true
را برگرداند، saveSettings()
yieldToMain()
را فراخوانی می کند تا بتوان ورودی کاربر را مدیریت کرد. در غیر این صورت، کار بعدی را از جلوی صف جابجا می کند و آن را به طور مداوم اجرا می کند. این کار را تا زمانی انجام می دهد که هیچ کار دیگری باقی نماند.
استفاده از isInputPending()
در ترکیب با مکانیزم تسلیم، راهی عالی برای وادار کردن مرورگر به متوقف کردن کارهایی است که در حال پردازش است تا بتواند به تعاملات مهم با کاربر پاسخ دهد. این می تواند به بهبود توانایی صفحه شما در پاسخگویی به کاربر در بسیاری از موقعیت ها که وظایف زیادی در حال انجام است کمک کند.
راه دیگری برای استفاده از isInputPending()
- به خصوص اگر نگران ارائه یک بازگشت به عقب برای مرورگرهایی هستید که از آن پشتیبانی نمی کنند - استفاده از یک رویکرد مبتنی بر زمان در ارتباط با عملگر زنجیره ای اختیاری است:
async function saveSettings () {
// A task queue of functions
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
];
let deadline = performance.now() + 50;
while (tasks.length > 0) {
// Optional chaining operator used here helps to avoid
// errors in browsers that don't support `isInputPending`:
if (navigator.scheduling?.isInputPending() || performance.now() >= deadline) {
// There's a pending user input, or the
// deadline has been reached. Yield here:
await yieldToMain();
// Extend the deadline:
deadline = performance.now() + 50;
// Stop the execution of the current loop and
// move onto the next iteration:
continue;
}
// Shift the task out of the queue:
const task = tasks.shift();
// Run the task:
task();
}
}
با این رویکرد، با استفاده از یک رویکرد مبتنی بر زمان که از یک ضربالاجل استفاده میکند (و تنظیم میکند) برای مرورگرهایی که isInputPending()
پشتیبانی نمیکنند، یک بازگشت مجدد دریافت میکنید تا کار در صورت لزوم، چه با تسلیم شدن به ورودی کاربر، شکسته شود. یا در یک مقطع زمانی خاص
شکاف در APIهای فعلی
API های ذکر شده تا کنون می توانند به شما کمک کنند تا وظایف را تقسیم کنید، اما آنها یک جنبه منفی دارند: وقتی با به تعویق انداختن کد برای اجرای یک کار بعدی، به رشته اصلی تسلیم می شوید، آن کد به انتهای صف کار اضافه می شود.
اگر تمام کدهای صفحه خود را کنترل کنید، میتوانید زمانبندی خود را با قابلیت اولویتبندی وظایف ایجاد کنید، اما اسکریپتهای شخص ثالث از زمانبندی شما استفاده نمیکنند. در واقع، شما واقعا نمی توانید کار را در چنین محیط هایی اولویت بندی کنید . شما فقط می توانید آن را تکه تکه کنید یا به طور صریح به تعاملات کاربر تسلیم شوید.
خوشبختانه، یک API زمانبندی اختصاصی وجود دارد که در حال حاضر در حال توسعه است که این مشکلات را برطرف می کند.
یک API زمانبندی اختصاصی
API زمانبند در حال حاضر تابع postTask()
را ارائه می دهد که در زمان نگارش در مرورگرهای Chromium و در فایرفاکس پشت پرچم موجود است. 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'});
};
در اینجا، اولویت وظایف به گونهای برنامهریزی میشود که وظایف اولویتبندی شده مرورگر - مانند تعاملات کاربر - بتواند به خوبی انجام شود.
این یک مثال ساده از نحوه استفاده از postTask()
است. امکان نمونه سازی اشیاء مختلف TaskController
وجود دارد که می توانند اولویت ها را بین وظایف به اشتراک بگذارند، از جمله توانایی تغییر اولویت ها برای نمونه های مختلف TaskController
در صورت نیاز.
بازده داخلی با ادامه از طریق 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 است، به این معنی که اگر در وسط مجموعه ای از وظایف تسلیم شوید، سایر وظایف زمان بندی شده به همان ترتیب پس از نقطه بازده ادامه خواهند داشت. با این کار کدهای اسکریپت های شخص ثالث از غصب نظم اجرای کد شما جلوگیری می کند.
نتیجه
مدیریت وظایف چالش برانگیز است، اما انجام این کار به صفحه شما کمک می کند سریعتر به تعاملات کاربر پاسخ دهد. هیچ توصیه واحدی برای مدیریت و اولویت بندی وظایف وجود ندارد. بلکه تعدادی تکنیک مختلف است. برای تکرار، اینها موارد اصلی هستند که باید هنگام مدیریت وظایف در نظر بگیرید:
- تسلیم موضوع اصلی برای کارهای حیاتی و مورد نظر کاربر شوید.
- از
isInputPending()
برای تسلیم شدن به رشته اصلی زمانی که کاربر سعی در تعامل با صفحه دارد استفاده کنید. - با
postTask()
وظایف را اولویت بندی کنید. - در نهایت، تا حد امکان در عملکردهای خود کمتر کار کنید.
با استفاده از یک یا چند مورد از این ابزارها، باید بتوانید کار را در برنامه خود ساختار دهید تا نیازهای کاربر را در اولویت قرار دهد و در عین حال اطمینان حاصل کنید که کارهای مهم کمتری همچنان انجام می شود. این باعث ایجاد تجربه کاربری بهتری می شود که واکنش پذیرتر و استفاده لذت بخش تر است.
تشکر ویژه از فیلیپ والتون برای بررسی فنی این مقاله.
تصویر قهرمان برگرفته از سایت Unsplash توسط امیرعلی میرهاشمیان .