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

توصیه‌های رایج برای سریع‌تر کردن برنامه‌های جاوا اسکریپت اغلب شامل «رشته اصلی را مسدود نکنید» و «تکلیف طولانی‌تان را جدا کنید» است. این صفحه معنای این توصیه و اینکه چرا بهینه سازی وظایف در جاوا اسکریپت مهم است را توضیح می دهد.

تکلیف چیست؟

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

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

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

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

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

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

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

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

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

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

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

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

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

function saveSettings () { //This is a long task.
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}
عملکرد saveSettings در نمایه‌ساز عملکرد Chrome نشان داده شده است. در حالی که تابع سطح بالا پنج تابع دیگر را فراخوانی می کند، تمام کارها در یک کار طولانی انجام می شود که رشته اصلی را مسدود می کند.
یک تابع saveSettings() که پنج تابع را فراخوانی می کند. این کار به عنوان بخشی از یک کار طولانی مدت یکپارچه اجرا می شود.

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

به تعویق انداختن دستی اجرای کد

می توانید اجرای برخی وظایف را با ارسال تابع مربوطه به 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();
  }
}

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

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

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

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

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

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

  • 94
  • 94
  • ایکس

منبع

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

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

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

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

مزیت 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 توسط امیرعلی میرهاشمیان .