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

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

تاریخ انتشار: 30 سپتامبر 2022، آخرین به روز رسانی: 19 دسامبر 2024

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

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

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

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

وظیفه ، هر کار مجزایی است که مرورگر انجام می دهد. این کار شامل رندر کردن، تجزیه 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 میلی ثانیه یا بیشتر به طول کل کار کمک کند. در بدترین حالت، تعداد بیشتری از این وظایف می توانند بسیار طولانی تر اجرا شوند - به خصوص در دستگاه های دارای محدودیت منابع.

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

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

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

یکی از روش هایی که توسعه دهندگان برای تقسیم وظایف به کارهای کوچکتر استفاده کرده اند، شامل 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() تو در تو، مرورگر شروع به اعمال حداقل 5 میلی‌ثانیه تاخیر برای هر setTimeout() اضافی می‌کند.

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

یک API اختصاصی بازده: scheduler.yield()

Browser Support

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

Source

scheduler.yield() یک API است که به طور خاص برای تسلیم شدن به رشته اصلی در مرورگر طراحی شده است.

این نحو در سطح زبان یا یک ساختار خاص نیست. scheduler.yield() فقط تابعی است که یک Promise برمی گرداند که در یک کار آینده حل خواهد شد. هر کد زنجیره ای که پس از حل شدن آن Promise اجرا شود (چه در یک زنجیره .then() صریح یا پس از await برای آن در یک تابع async) سپس در آن کار آینده اجرا خواهد شد.

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

async function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Yield to the main thread:
  await scheduler.yield()

  // Work that isn't user-visible, continued in a separate task:
  saveToDatabase();
  sendAnalytics();
}
عملکرد saveSettings همانطور که در نمایه‌ساز عملکرد کروم نشان داده شده است، اکنون به دو کار تقسیم شده است. اولین کار دو تابع را فراخوانی می‌کند، سپس بازده می‌دهد و به کاربر اجازه می‌دهد تا کار چیدمان و رنگ انجام شود و پاسخی قابل مشاهده به کاربر بدهد. در نتیجه، رویداد کلیک در 64 میلی ثانیه بسیار سریعتر به پایان می رسد. وظیفه دوم سه تابع آخر را فراخوانی می کند.
اجرای تابع saveSettings() اکنون به دو وظیفه تقسیم شده است. در نتیجه، طرح‌بندی و رنگ‌آمیزی می‌توانند بین کارها اجرا شوند و پاسخ بصری سریع‌تری به کاربر می‌دهند، همانطور که با تعامل اشاره‌گر بسیار کوتاه‌تر اندازه‌گیری می‌شود.

با این حال، مزیت واقعی scheduler.yield() نسبت به سایر رویکردهای بازده این است که ادامه آن اولویت بندی شده است، به این معنی که اگر در وسط یک کار تسلیم شوید، ادامه کار فعلی قبل از انجام هر کار مشابه دیگری اجرا می شود. آغاز شد.

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

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

پشتیبانی از مرورگرهای متقابل

scheduler.yield() هنوز در همه مرورگرها پشتیبانی نمی‌شود، بنابراین نیاز به بازگشت مجدد است.

یک راه حل این است که scheduler-polyfill را در ساخت خود رها کنید و سپس scheduler.yield() می توان مستقیماً استفاده کرد. polyfill بازگشت به دیگر توابع زمانبندی کار را کنترل می کند، بنابراین در مرورگرها به طور مشابه کار می کند.

متناوباً، یک نسخه کمتر پیچیده را می‌توان در چند خط نوشت و فقط از setTimeout که در یک Promise پیچیده شده است، به‌عنوان بازگشتی استفاده می‌کند، در صورتی که scheduler.yield() در دسترس نباشد.

function yieldToMain () {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }

  // Fall back to yielding with setTimeout.
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

در حالی که مرورگرهای بدون پشتیبانی scheduler.yield() ادامه اولویت بندی شده را دریافت نمی کنند، اما همچنان برای مرورگر پاسخگو باقی می مانند.

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

این را می توان با شناسایی ویژگی و بازگشت به انتظار برای یک ریز وظیفه در یک خط یک خطی مفید انجام داد:

// Yield to the main thread if scheduler.yield() is available.
await globalThis.scheduler?.yield?.();

کار طولانی مدت را با scheduler.yield() جدا کنید

مزیت استفاده از هر یک از این روش‌ها برای استفاده از scheduler.yield() این است که می‌توانید آن را در هر تابع async await .

به عنوان مثال، اگر مجموعه‌ای از کارها برای اجرا دارید که اغلب به یک کار طولانی اضافه می‌شوند، می‌توانید بازدهی را وارد کنید تا کار را تجزیه کنید.

async function runJobs(jobQueue) {
  for (const job of jobQueue) {
    // Run the job:
    job();

    // Yield to the main thread:
    await yieldToMain();
  }
}

ادامه runJobs() اولویت بندی خواهد شد، اما همچنان اجازه می دهد کارهایی با اولویت بالاتر مانند پاسخ بصری به ورودی کاربر اجرا شود، نه اینکه منتظر بمانید تا لیست بالقوه طولانی کارها تمام شود.

با این حال، این یک استفاده کارآمد از بازده نیست. scheduler.yield() سریع و کارآمد است، اما مقداری سربار دارد. اگر برخی از مشاغل در jobQueue بسیار کوتاه باشند، سربار می‌تواند به سرعت به زمان بیشتری نسبت به اجرای کار واقعی برای بازده و از سرگیری صرف شود.

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

async function runJobs(jobQueue, deadline=50) {
  let lastYield = performance.now();

  for (const job of jobQueue) {
    // Run the job:
    job();

    // If it's been longer than the deadline, yield to the main thread:
    if (performance.now() - lastYield > deadline) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}

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

مجموعه‌ای از عملکردهای شغلی که در پانل عملکرد Chrome DevTools نشان داده شده‌اند و اجرای آن‌ها در چندین کار شکسته شده است.
مشاغل در چندین کار دسته بندی شده اند.

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

Browser Support

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

Source

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

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

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

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

نتیجه گیری

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

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

برای کسب اطلاعات بیشتر در مورد scheduler.yield() ، زمانبندی کار صریح نسبی scheduler.postTask() و اولویت بندی کارها، به اسناد API زمانبندی وظایف اولویت دار مراجعه کنید.

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

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

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

،

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

تاریخ انتشار: 30 سپتامبر 2022، آخرین به روز رسانی: 19 دسامبر 2024

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

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

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

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

تکلیف چیست؟

وظیفه ، هر کار مجزایی است که مرورگر انجام می دهد. این کار شامل رندر کردن، تجزیه 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 میلی ثانیه یا بیشتر به طول کل کار کمک کند. در بدترین حالت، تعداد بیشتری از این وظایف می توانند بسیار طولانی تر اجرا شوند - به خصوص در دستگاه های دارای محدودیت منابع.

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

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

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

یکی از روش هایی که توسعه دهندگان برای تقسیم وظایف به کارهای کوچکتر استفاده کرده اند، شامل 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() تو در تو، مرورگر شروع به اعمال حداقل 5 میلی‌ثانیه تاخیر برای هر setTimeout() اضافی می‌کند.

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

یک API اختصاصی بازده: scheduler.yield()

Browser Support

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

Source

scheduler.yield() یک API است که به طور خاص برای تسلیم شدن به رشته اصلی در مرورگر طراحی شده است.

این نحو در سطح زبان یا یک ساختار خاص نیست. scheduler.yield() فقط تابعی است که یک Promise برمی گرداند که در یک کار آینده حل خواهد شد. هر کد زنجیره ای که پس از حل شدن آن Promise اجرا شود (چه در یک زنجیره .then() صریح یا پس از await برای آن در یک تابع async) سپس در آن کار آینده اجرا خواهد شد.

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

async function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Yield to the main thread:
  await scheduler.yield()

  // Work that isn't user-visible, continued in a separate task:
  saveToDatabase();
  sendAnalytics();
}
عملکرد saveSettings همانطور که در نمایه‌ساز عملکرد کروم نشان داده شده است، اکنون به دو کار تقسیم شده است. اولین کار دو تابع را فراخوانی می‌کند، سپس بازده می‌دهد و به کاربر اجازه می‌دهد تا کار چیدمان و رنگ انجام شود و پاسخی قابل مشاهده به کاربر بدهد. در نتیجه، رویداد کلیک در 64 میلی ثانیه بسیار سریعتر به پایان می رسد. وظیفه دوم سه تابع آخر را فراخوانی می کند.
اجرای تابع saveSettings() اکنون به دو وظیفه تقسیم شده است. در نتیجه، طرح‌بندی و رنگ‌آمیزی می‌توانند بین کارها اجرا شوند و پاسخ بصری سریع‌تری به کاربر می‌دهند، همانطور که با تعامل اشاره‌گر بسیار کوتاه‌تر اندازه‌گیری می‌شود.

با این حال، مزیت واقعی scheduler.yield() نسبت به سایر رویکردهای بازده این است که ادامه آن اولویت بندی شده است، به این معنی که اگر در وسط یک کار تسلیم شوید، ادامه کار فعلی قبل از انجام هر کار مشابه دیگری اجرا می شود. آغاز شد.

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

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

پشتیبانی از مرورگرهای متقابل

scheduler.yield() هنوز در همه مرورگرها پشتیبانی نمی‌شود، بنابراین نیاز به بازگشت مجدد است.

یک راه حل این است که scheduler-polyfill را در ساخت خود رها کنید و سپس scheduler.yield() می توان مستقیماً استفاده کرد. polyfill بازگشت به دیگر توابع زمانبندی کار را کنترل می کند، بنابراین در مرورگرها به طور مشابه کار می کند.

متناوباً، یک نسخه کمتر پیچیده را می‌توان در چند خط نوشت و فقط از setTimeout که در یک Promise پیچیده شده است، به‌عنوان بازگشتی استفاده می‌کند، در صورتی که scheduler.yield() در دسترس نباشد.

function yieldToMain () {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }

  // Fall back to yielding with setTimeout.
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

در حالی که مرورگرهای بدون پشتیبانی scheduler.yield() ادامه اولویت بندی شده را دریافت نمی کنند، اما همچنان برای مرورگر پاسخگو باقی می مانند.

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

این را می توان با شناسایی ویژگی و بازگشت به انتظار برای یک ریز وظیفه در یک خط یک خطی مفید انجام داد:

// Yield to the main thread if scheduler.yield() is available.
await globalThis.scheduler?.yield?.();

کار طولانی مدت را با scheduler.yield() جدا کنید

مزیت استفاده از هر یک از این روش‌ها برای استفاده از scheduler.yield() این است که می‌توانید آن را در هر تابع async await .

به عنوان مثال، اگر مجموعه‌ای از کارها برای اجرا دارید که اغلب به یک کار طولانی اضافه می‌شوند، می‌توانید بازدهی را وارد کنید تا کار را تجزیه کنید.

async function runJobs(jobQueue) {
  for (const job of jobQueue) {
    // Run the job:
    job();

    // Yield to the main thread:
    await yieldToMain();
  }
}

ادامه runJobs() اولویت بندی خواهد شد، اما همچنان اجازه می دهد کارهایی با اولویت بالاتر مانند پاسخ بصری به ورودی کاربر اجرا شود، نه اینکه منتظر بمانید تا لیست بالقوه طولانی کارها تمام شود.

با این حال، این یک استفاده کارآمد از بازده نیست. scheduler.yield() سریع و کارآمد است، اما مقداری سربار دارد. اگر برخی از مشاغل در jobQueue بسیار کوتاه باشند، سربار می‌تواند به سرعت به زمان بیشتری نسبت به اجرای کار واقعی برای بازده و از سرگیری صرف شود.

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

async function runJobs(jobQueue, deadline=50) {
  let lastYield = performance.now();

  for (const job of jobQueue) {
    // Run the job:
    job();

    // If it's been longer than the deadline, yield to the main thread:
    if (performance.now() - lastYield > deadline) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}

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

مجموعه‌ای از عملکردهای شغلی که در پانل عملکرد Chrome DevTools نشان داده شده‌اند و اجرای آن‌ها در چندین کار شکسته شده است.
مشاغل در چندین کار دسته بندی شده اند.

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

Browser Support

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

Source

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

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

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

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

نتیجه گیری

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

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

برای کسب اطلاعات بیشتر در مورد scheduler.yield() ، scheduler.postTask() و اولویت بندی کار ، به اسناد API برنامه ریزی وظیفه اولویت بندی شده مراجعه کنید.

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

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

تصویر تصویربرداری که از Unsplash گرفته شده است ، حسن نیت ارائه می دهد از امیرالی میراشمی .