معرفی آزمایشی مبدا Scheduler.yield

ایجاد وب‌سایت‌هایی که به سرعت به ورودی‌های کاربر پاسخ می‌دهند، یکی از چالش‌برانگیزترین جنبه‌های عملکرد وب بوده است - موردی که تیم Chrome برای کمک به توسعه‌دهندگان وب سخت کار کرده است. همین امسال، اعلام شد که معیار Interaction to Next Paint (INP) از وضعیت آزمایشی به وضعیت معلق تبدیل می شود. اکنون در مارس 2024 به عنوان Core Web Vital جایگزین First Input Delay (FID) می شود.

در تلاش مستمر برای ارائه API های جدید که به توسعه دهندگان وب کمک می کند تا وب سایت های خود را تا آنجا که می توانند سریع تر کنند، تیم Chrome در حال حاضر در حال اجرای یک آزمایش اولیه برای scheduler.yield است که از نسخه 115 Chrome شروع می شود. scheduler.yield یک افزودنی جدید پیشنهادی به API زمانبندی است که به روشی آسانتر و بهتر برای بازگرداندن کنترل به رشته اصلی نسبت به روشهایی که به طور سنتی بر آنها تکیه می شد، اجازه می دهد.

در بازده

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

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

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

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

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

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

مشکل با استراتژی های بازده فعلی

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

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

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

  1. روی دکمه بالایی با برچسب اجرای دوره‌ای وظایف کلیک کنید، که وظایف مسدود کردن را برای اجرا هر چند وقت یکبار برنامه‌ریزی می‌کند. وقتی روی این دکمه کلیک می‌کنید، گزارش کار با پیام‌هایی پر می‌شود که عبارتند از «Ran blocking task» با setInterval .
  2. در مرحله بعد، روی دکمه با عنوان حلقه اجرا کلیک کنید و در هر تکرار با setTimeout نمایش داده می شود .

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

Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval

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

این یک مشکل رایج در وب را نشان می دهد: برای یک اسکریپت - به ویژه یک اسکریپت شخص ثالث - غیرعادی نیست که یک تابع تایمر را ثبت کند که کار را در برخی فاصله ها اجرا می کند. رفتار "انتهای صف کار" که با بازدهی setTimeout همراه است به این معنی است که کار از منابع دیگر وظیفه ممکن است قبل از کارهای باقی مانده ای که حلقه باید پس از بازده انجام دهد در صف قرار گیرد.

بسته به برنامه شما، این ممکن است یک نتیجه مطلوب باشد یا نباشد - اما در بسیاری از موارد، این رفتار باعث می شود توسعه دهندگان تمایلی به کنار گذاشتن کنترل رشته اصلی نداشته باشند. بازدهی خوب است زیرا تعاملات کاربر این فرصت را دارند که زودتر اجرا شوند، اما همچنین به سایر کارهای تعاملی غیر کاربر نیز اجازه می‌دهد تا در موضوع اصلی زمان داشته باشند. این یک مشکل واقعی است، اما scheduler.yield می تواند به حل آن کمک کند!

scheduler.yield وارد کنید

scheduler.yield از نسخه 115 کروم پشت پرچم به عنوان یک ویژگی آزمایشی پلتفرم وب در دسترس بوده است. یک سوالی که ممکن است داشته باشید این است که "چرا وقتی setTimeout از قبل این کار را انجام می دهد به یک تابع خاص نیاز دارم؟"

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

scheduler.yield تابعی است که به thread اصلی تسلیم می شود و هنگام فراخوانی یک Promise برمی گرداند. این بدان معنی است که می توانید آن را در یک تابع async await :

async function yieldy () {
  // Do some work...
  // ...

  // Yield!
  await scheduler.yield();

  // Do some more work...
  // ...
}

برای مشاهده scheduler.yield در عمل، موارد زیر را انجام دهید:

  1. به chrome://flags بروید.
  2. آزمایش ویژگی‌های پلتفرم وب آزمایشی را فعال کنید. پس از انجام این کار ممکن است مجبور شوید کروم را مجددا راه اندازی کنید.
  3. به صفحه نمایشی بروید یا از نسخه تعبیه شده آن در زیر این لیست استفاده کنید.
  4. روی دکمه بالایی با عنوان Run tasks periodically کلیک کنید.
  5. در نهایت، روی دکمه با عنوان حلقه اجرا کلیک کنید و در هر تکرار با scheduler.yield نمایش داده شود .

خروجی در کادر پایین صفحه چیزی شبیه به این خواهد بود:

Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval

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

آن را امتحان کنید!

اگر scheduler.yield برای شما جالب به نظر می رسد و می خواهید آن را امتحان کنید، می توانید این کار را به دو روش از نسخه 115 Chrome انجام دهید:

  1. اگر می‌خواهید با scheduler.yield به صورت محلی آزمایش کنید، chrome://flags در نوار آدرس کروم تایپ کرده و وارد کنید و از منوی کشویی در بخش ویژگی‌های پلتفرم آزمایشی وب، Enable را انتخاب کنید. با این کار scheduler.yield (و هر ویژگی آزمایشی دیگر) فقط در نمونه Chrome شما در دسترس خواهد بود.
  2. اگر می‌خواهید scheduler.yield برای کاربران واقعی Chromium در یک مبدأ در دسترس عموم فعال کنید، باید برای آزمایشی مبدا scheduler.yield ثبت نام کنید. این به شما امکان می‌دهد تا با خیال راحت ویژگی‌های پیشنهادی را برای یک دوره زمانی معین آزمایش کنید، و به تیم Chrome اطلاعات ارزشمندی در مورد نحوه استفاده از آن ویژگی‌ها در میدان می‌دهد. برای اطلاعات بیشتر در مورد نحوه کارآزمایی‌های مبدا، این راهنما را بخوانید .

نحوه استفاده شما از scheduler.yield - در حالی که همچنان از مرورگرهایی که آن را اجرا نمی کنند پشتیبانی می کند - به اهداف شما بستگی دارد. می توانید از پلی فیل رسمی استفاده کنید. پلی‌فیل در صورتی مفید است که موارد زیر در مورد وضعیت شما صدق کند:

  1. شما در حال حاضر از scheduler.postTask در برنامه خود برای زمان بندی کارها استفاده می کنید.
  2. شما می خواهید بتوانید اولویت های کاری را تعیین کنید.
  3. شما می خواهید بتوانید وظایف را از طریق کلاس TaskController که API scheduler.postTask ارائه می دهد لغو یا اولویت بندی کنید.

اگر این وضعیت شما را توصیف نمی کند، ممکن است polyfill برای شما مناسب نباشد. در این صورت، می‌توانید از چند طریق بک آپ بگیرید. روش اول در صورت در دسترس بودن از scheduler.yield استفاده می کند، اما اگر در دسترس نباشد به setTimeout برمی گردد:

// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

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

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

این می تواند کار کند، اما همانطور که ممکن است حدس بزنید، مرورگرهایی که scheduler.yield پشتیبانی نمی کنند، بدون رفتار "در جلوی صف" عمل می کنند. اگر این بدان معناست که شما ترجیح می‌دهید به هیچ وجه بازدهی نداشته باشید، می‌توانید روش دیگری را امتحان کنید که در صورت موجود بودن از scheduler.yield استفاده می‌کند، اما اگر اینطور نباشد اصلا نتیجه نمی‌دهد:

// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to nothing:
  return;
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

scheduler.yield یک افزودنی هیجان انگیز به API زمانبندی است که امیدواریم بهبود پاسخگویی را برای توسعه دهندگان نسبت به استراتژی های بازده فعلی آسان تر کند. اگر scheduler.yield برای شما یک API مفید به نظر می رسد، لطفاً در تحقیقات ما شرکت کنید تا به بهبود آن کمک کنید و در مورد چگونگی بهبود بیشتر آن بازخورد ارائه دهید .

تصویر قهرمان از Unsplash اثر جاناتان آلیسون .