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

ساخت وب‌سایت‌هایی که به سرعت به ورودی کاربر پاسخ دهند، یکی از چالش‌برانگیزترین جنبه‌های عملکرد وب بوده است - جنبه‌ای که تیم کروم سخت تلاش کرده است تا به توسعه‌دهندگان وب در برآورده کردن آن کمک کند. همین امسال اعلام شد که معیار تعامل با رنگ بعدی (INP) از حالت آزمایشی به حالت در حال بررسی ارتقا می‌یابد. اکنون قرار است در مارس 2024 به عنوان یک معیار حیاتی اصلی وب، جایگزین اولین تأخیر ورودی (FID) شود.

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

در حال تسلیم

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

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

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

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

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

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

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

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

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

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

  1. روی دکمه‌ی بالایی با عنوان Run tasks periodic کلیک کنید، که وظایف مسدودکننده را برای اجرا در فواصل زمانی منظم برنامه‌ریزی می‌کند. وقتی روی این دکمه کلیک می‌کنید، گزارش وظایف با چندین پیام که Ran blocking task را با setInterval نشان می‌دهند، پر می‌شود.
  2. سپس، روی دکمه‌ای با عنوان Run loop کلیک کنید که در هر تکرار 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

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

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

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

scheduler.yield را وارد کنید.

scheduler.yield از نسخه ۱۱۵ کروم به عنوان یک ویژگی آزمایشی پلتفرم وب، پشت یک پرچم (flag) در دسترس بوده است. سوالی که ممکن است برایتان پیش بیاید این است که «چرا وقتی setTimeout از قبل این کار را انجام می‌دهد، به یک تابع ویژه برای yield نیاز دارم؟»

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

scheduler.yield تابعی است که به نخ اصلی (main thread) تسلیم می‌شود و هنگام فراخوانی، یک Promise برمی‌گرداند. این بدان معناست که می‌توانید آن را در یک تابع async در حالت await (avail) قرار دهید:

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

  // Yield!
  await scheduler.yield();

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

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

  1. به chrome://flags بروید.
  2. فعال کردن ویژگی‌های آزمایشی پلتفرم وب آزمایشی . ممکن است لازم باشد پس از انجام این کار، کروم را مجدداً راه‌اندازی کنید.
  3. به صفحه دمو بروید یا از نسخه جاسازی‌شده زیر بعد از این لیست استفاده کنید.
  4. روی دکمه‌ی بالایی با عنوان « اجرای وظایف به صورت دوره‌ای» کلیک کنید.
  5. در نهایت، روی دکمه‌ای با عنوان Run loop کلیک کنید، که در هر تکرار با 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 خروجی می‌دهد، می‌توانید ببینید که حلقه - حتی با اینکه بعد از هر تکرار خروجی می‌دهد - کار باقیمانده را به انتهای صف ارسال نمی‌کند، بلکه به ابتدای آن می‌فرستد. این به شما بهترین مزایای هر دو دنیا را می‌دهد: می‌توانید با yield کردن، پاسخگویی ورودی را در وب‌سایت خود بهبود بخشید، اما همچنین مطمئن شوید که کاری که می‌خواستید پس از yield کردن به پایان برسانید، به تأخیر نمی‌افتد.

امتحانش کن!

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

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

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

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

اگر این وضعیت شما را توصیف نمی‌کند، ممکن است polyfill برای شما مناسب نباشد. در این صورت، می‌توانید از چند روش، fallback خودتان را ایجاد کنید. رویکرد اول در صورت موجود بودن 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 پشتیبانی نمی‌کنند، بدون رفتار "جلوی صف" yield می‌کنند. اگر این بدان معناست که ترجیح می‌دهید اصلاً yield نکنید، می‌توانید رویکرد دیگری را امتحان کنید که در صورت موجود بودن scheduler.yield استفاده می‌کند، اما در صورت موجود نبودن، اصلاً 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 زمانبند است - افزونه‌ای که امیدواریم بهبود پاسخگویی را برای توسعه‌دهندگان نسبت به استراتژی‌های yielding فعلی آسان‌تر کند. اگر scheduler.yield برای شما یک API مفید به نظر می‌رسد، لطفاً در تحقیقات ما شرکت کنید تا به بهبود آن کمک کنید و در مورد چگونگی بهبود بیشتر آن بازخورد ارائه دهید .

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