کارهای طولانی را بهینه کنید

به شما گفته شده "رشته اصلی را مسدود نکنید" و "کارهای طولانی خود را از بین ببرید"، اما انجام این کارها به چه معناست؟

توصیه های رایج برای سریع نگه داشتن برنامه های جاوا اسکریپت به توصیه های زیر خلاصه می شود:

  • "رشته اصلی را مسدود نکنید."
  • "کارهای طولانی خود را از بین ببرید."

این توصیه عالی است، اما چه کاری شامل می شود؟ ارسال جاوا اسکریپت کمتر خوب است، اما آیا این به طور خودکار با رابط های کاربری پاسخگوتر برابر است؟ شاید، اما شاید نه.

برای درک نحوه بهینه سازی وظایف در جاوا اسکریپت، ابتدا باید بدانید که وظایف چیست و مرورگر چگونه آنها را مدیریت می کند.

تکلیف چیست؟

وظیفه ، هر کار مجزایی است که مرورگر انجام می دهد. این کار شامل رندر کردن، تجزیه HTML و CSS، اجرای جاوا اسکریپت و انواع دیگری از کارهایی است که ممکن است کنترل مستقیمی روی آنها نداشته باشید. از بین همه اینها، جاوا اسکریپتی که می نویسید شاید بزرگترین منبع وظایف باشد.

تصویرسازی یک کار همانطور که در نمایه عملکرد DevTools Chrome نشان داده شده است. این وظیفه در بالای یک پشته قرار دارد، با یک کنترل کننده رویداد کلیک، یک فراخوانی تابع، و موارد بیشتر در زیر آن. این کار همچنین شامل چند کار رندر در سمت راست است.
کاری که توسط یک کنترل کننده رویداد click آغاز شده است، که در نمایه عملکرد Chrome DevTools نشان داده شده است.

وظایف مرتبط با جاوا اسکریپت از چند طریق بر عملکرد تأثیر می گذارد:

  • هنگامی که یک مرورگر یک فایل جاوا اسکریپت را در حین راه اندازی دانلود می کند، وظایف را برای تجزیه و کامپایل کردن آن جاوا اسکریپت در صف قرار می دهد تا بعداً اجرا شود.
  • در زمان‌های دیگر در طول عمر صفحه، وقتی جاوا اسکریپت کارهایی مانند ایجاد تعاملات از طریق کنترل‌کننده‌های رویداد، انیمیشن‌های مبتنی بر جاوا اسکریپت و فعالیت‌های پس‌زمینه مانند مجموعه تحلیلی انجام می‌دهد، در صف قرار می‌گیرند.

همه این موارد - به استثنای وب کارگران و API های مشابه - در موضوع اصلی اتفاق می افتد.

موضوع اصلی چیست؟

موضوع اصلی جایی است که اکثر وظایف در مرورگر اجرا می شوند و تقریباً تمام جاوا اسکریپتی که می نویسید در آن اجرا می شود.

رشته اصلی فقط می تواند یک کار را در یک زمان پردازش کند. هر کاری که بیش از 50 میلی ثانیه طول بکشد، کار طولانی است. برای کارهایی که بیش از 50 میلی ثانیه هستند، کل زمان کار منهای 50 میلی ثانیه به عنوان دوره مسدود کردن کار شناخته می شود.

مرورگر از وقوع تعاملات در حین اجرای یک کار با هر طولی جلوگیری می کند، اما تا زمانی که کارها برای مدت طولانی اجرا نشوند، این برای کاربر قابل درک نیست. با این حال، هنگامی که یک کاربر سعی می کند با یک صفحه تعامل داشته باشد، در حالی که وظایف طولانی مدت زیادی وجود دارد، رابط کاربری احساس می کند که پاسخگو نیست، و حتی اگر رشته اصلی برای مدت زمان بسیار طولانی مسدود شود، حتی ممکن است خراب شود.

یک کار طولانی در نمایه ساز عملکرد DevTools کروم. قسمت مسدود کننده کار (بیشتر از 50 میلی ثانیه) با الگویی از نوارهای مورب قرمز به تصویر کشیده شده است.
یک کار طولانی همانطور که در نمایه عملکرد کروم نشان داده شده است. کارهای طولانی با یک مثلث قرمز در گوشه کار نشان داده می شوند که قسمت مسدود کننده کار با الگویی از نوارهای قرمز مورب پر شده است.

برای جلوگیری از مسدود شدن بیش از حد رشته اصلی، می توانید یک کار طولانی را به چندین کار کوچکتر تقسیم کنید.

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

این مهم است زیرا هنگامی که وظایف از هم جدا می شوند، مرورگر می تواند خیلی زودتر به کارهای با اولویت بالاتر - از جمله تعاملات کاربر - پاسخ دهد. پس از آن، کارهای باقیمانده تا پایان کامل می شوند، و اطمینان حاصل می شود که کارهایی که در ابتدا در صف قرار داده اید، انجام می شود.

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

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

اکنون که می دانید چرا جدا کردن وظایف مهم است، می توانید نحوه انجام این کار را در جاوا اسکریپت بیاموزید.

استراتژی های مدیریت وظیفه

یک توصیه رایج در معماری نرم افزار این است که کار خود را به عملکردهای کوچکتر تقسیم کنید:

function saveSettings () {
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}

در این مثال، تابعی به نام saveSettings() وجود دارد که پنج تابع را برای اعتبارسنجی یک فرم، نشان دادن یک اسپینر، ارسال داده ها به پشتیبان برنامه، به روز رسانی رابط کاربری و ارسال تجزیه و تحلیل فراخوانی می کند.

از نظر مفهومی، saveSettings() به خوبی طراحی شده است. اگر نیاز به اشکال زدایی یکی از این توابع دارید، می توانید درخت پروژه را طی کنید تا بفهمید هر تابع چه کاری انجام می دهد. شکستن کارهایی مانند این، هدایت و نگهداری پروژه ها را آسان تر می کند.

با این حال، یک مشکل بالقوه در اینجا این است که جاوا اسکریپت هر یک از این توابع را به عنوان وظایف جداگانه اجرا نمی کند زیرا آنها در تابع saveSettings() اجرا می شوند. این بدان معنی است که هر پنج تابع به عنوان یک وظیفه اجرا می شوند.

عملکرد saveSettings همانطور که در نمایه‌ساز عملکرد Chrome نشان داده شده است. در حالی که تابع سطح بالا پنج تابع دیگر را فراخوانی می کند، تمام کارها در یک کار طولانی انجام می شود که رشته اصلی را مسدود می کند.
یک تابع 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();
  }
}

نتیجه این است که وظیفه زمانی یکپارچه اکنون به وظایف جداگانه تقسیم می شود.

همان تابع saveSettings که در نمایه‌ساز عملکرد Chrome نشان داده شده است، فقط با بازده. نتیجه این است که وظیفه زمانی یکپارچه اکنون به پنج وظیفه جداگانه تقسیم می شود - یکی برای هر تابع.
تابع saveSettings() اکنون توابع فرزند خود را به عنوان وظایف جداگانه اجرا می کند.

یک API زمانبندی اختصاصی

setTimeout یک راه موثر برای جدا کردن وظایف است، اما می تواند یک اشکال داشته باشد: وقتی با به تعویق انداختن کد برای اجرای یک کار بعدی، به رشته اصلی تسلیم می شوید، آن کار به انتهای صف اضافه می شود.

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

پشتیبانی مرورگر

  • کروم: 94.
  • لبه: 94.
  • فایرفاکس: پشت پرچم
  • سافاری: پشتیبانی نمی شود.

منبع

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'});
};

در اینجا، اولویت وظایف به گونه‌ای برنامه‌ریزی می‌شود که وظایف اولویت‌بندی شده با مرورگر - مانند تعاملات کاربر - در صورت لزوم بتوانند در بین آنها کار کنند.

عملکرد saveSettings همانطور که در نمایه‌ساز عملکرد کروم نشان داده شده است، اما از postTask استفاده می‌کند. postTask هر تابعی را که saveSettings اجرا می‌شود تقسیم می‌کند و آنها را به گونه‌ای اولویت‌بندی می‌کند که تعامل کاربر فرصتی برای اجرا بدون مسدود شدن داشته باشد.
هنگامی که saveSettings() اجرا می شود، تابع با استفاده از postTask() توابع جداگانه را زمان بندی می کند. کارهای حساس رو به روی کاربر با اولویت بالا برنامه ریزی شده است، در حالی که کارهایی که کاربر از آن اطلاعی ندارد در پس زمینه برنامه ریزی شده است. این اجازه می دهد تا تعاملات کاربر با سرعت بیشتری اجرا شود، زیرا کار هم شکسته شده و هم به طور مناسب اولویت بندی می شود.

این یک مثال ساده از نحوه استفاده از postTask() است. امکان نمونه سازی اشیاء مختلف TaskController وجود دارد که می توانند اولویت ها را بین وظایف به اشتراک بگذارند، از جمله توانایی تغییر اولویت ها برای نمونه های مختلف TaskController در صورت نیاز.

بازده داخلی با استفاده از API scheduler.yield() ادامه دارد

پشتیبانی مرورگر

  • کروم: 129.
  • لبه: 129.
  • فایرفاکس: پشتیبانی نمی شود.
  • سافاری: پشتیبانی نمی شود.

منبع

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() استفاده می کنید، اجرای کار از جایی که متوقف شد حتی پس از نقطه بازده ادامه می یابد.

مزیت scheduler.yield() continuation است، به این معنی که اگر در وسط مجموعه ای از وظایف تسلیم شوید، سایر وظایف زمان بندی شده به همان ترتیب پس از نقطه بازده ادامه خواهند داشت. با این کار کدهای اسکریپت های شخص ثالث مانع از قطع شدن ترتیب اجرای کد شما می شوند.

از isInputPending() استفاده نکنید

پشتیبانی مرورگر

  • کروم: 87.
  • لبه: 87.
  • فایرفاکس: پشتیبانی نمی شود.
  • سافاری: پشتیبانی نمی شود.

منبع

API isInputPending() راهی برای بررسی اینکه آیا کاربر سعی کرده است با یک صفحه تعامل داشته باشد یا خیر، ارائه می‌کند و تنها در صورتی نتیجه می‌دهد که ورودی معلق باشد.

این به جاوا اسکریپت اجازه می‌دهد در صورتی که هیچ ورودی معلقی وجود نداشته باشد، به جای تسلیم شدن و پایان در پشت صف کار، ادامه دهد. این می تواند منجر به بهبود عملکرد چشمگیر شود، همانطور که در Intent to Ship توضیح داده شده است، برای سایت هایی که در غیر این صورت ممکن است به موضوع اصلی باز نگردند.

با این حال، از زمان راه اندازی آن API، درک ما از بازده افزایش یافته است، به ویژه با معرفی INP. ما دیگر استفاده از این API را توصیه نمی‌کنیم ، و در عوض توصیه می‌کنیم صرفنظر از اینکه ورودی معلق است یا خیر، به دلایلی تسلیم شوید:

  • isInputPending() ممکن است علیرغم تعامل کاربر در برخی شرایط، به اشتباه false را برگرداند.
  • ورودی تنها موردی نیست که وظایف باید نتیجه دهند. انیمیشن‌ها و سایر به‌روزرسانی‌های معمولی رابط کاربری می‌توانند به همان اندازه برای ارائه یک صفحه وب واکنش‌گرا مهم باشند.
  • از آن زمان، APIهای بازده جامع تری معرفی شده اند که به نگرانی های بازده، مانند scheduler.postTask() و scheduler.yield() رسیدگی می کنند.

نتیجه گیری

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

  • تسلیم موضوع اصلی برای کارهای حیاتی و مورد نظر کاربر شوید.
  • با postTask() وظایف را اولویت بندی کنید.
  • آزمایش با scheduler.yield() را در نظر بگیرید.
  • در نهایت، تا حد امکان در عملکردهای خود کمتر کار کنید.

با استفاده از یک یا چند مورد از این ابزارها، باید بتوانید کار را در برنامه خود ساختار دهید تا نیازهای کاربر را در اولویت قرار دهد و در عین حال اطمینان حاصل کنید که کارهای مهم کمتری همچنان انجام می شود. این باعث ایجاد تجربه کاربری بهتری می شود که واکنش پذیرتر و استفاده لذت بخش تر است.

تشکر ویژه از فیلیپ والتون برای بررسی فنی این راهنما.

تصویر بندانگشتی برگرفته از Unsplash توسط امیرعلی میرهاشمیان .