درک تعامل با رنگ بعدی (INP)

۱. مقدمه

یک دموی تعاملی و آزمایشگاه کد برای یادگیری در مورد تعامل با Next Paint (INP) .

نموداری که یک تعامل در نخ اصلی را نشان می‌دهد. کاربر در حین مسدود کردن اجرای وظایف، ورودی را وارد می‌کند. ورودی تا زمان تکمیل آن وظایف به تأخیر می‌افتد، پس از آن شنونده‌های رویداد pointerup، mouseup و click اجرا می‌شوند، سپس کار رندر و نقاشی تا ارائه فریم بعدی آغاز می‌شود.

پیش‌نیازها

  • آشنایی با توسعه HTML و جاوا اسکریپت.
  • توصیه می‌شود: مستندات INP را مطالعه کنید.

آنچه یاد می‌گیرید

  • چگونگی تأثیر متقابل تعاملات کاربر و نحوه‌ی مدیریت شما بر این تعاملات، بر واکنش‌گرایی صفحه تأثیر می‌گذارد.
  • چگونه می‌توان برای یک تجربه کاربری روان، تأخیرها را کاهش داده و از بین برد.

آنچه شما نیاز دارید

  • رایانه‌ای با قابلیت کپی کردن کد از گیت‌هاب و اجرای دستورات npm.
  • یک ویرایشگر متن.
  • نسخه جدیدی از کروم برای کار کردن تمام معیارهای تعامل.

۲. آماده شوید

دریافت و اجرای کد

این کد در مخزن web-vitals-codelabs یافت می‌شود.

  1. مخزن را در ترمینال خود کپی کنید: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. به دایرکتوری کلون شده بروید: cd web-vitals-codelabs/understanding-inp
  3. نصب وابستگی‌ها: npm ci
  4. شروع سرور وب: npm run start
  5. در مرورگر خود به آدرس http://localhost:5173/understanding-inp/ مراجعه کنید.

نمای کلی برنامه

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

تصویری از برنامه آزمایشی این codelab

زیر دکمه چهار اندازه گیری وجود دارد:

  • INP: امتیاز INP فعلی، که معمولاً بدترین تعامل است.
  • تعامل: امتیاز جدیدترین تعامل.
  • FPS: تعداد فریم در ثانیه در رشته اصلی صفحه.
  • تایمر: یک انیمیشن تایمر در حال اجرا برای کمک به تجسم jank.

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

امتحانش کن.

سعی کنید با دکمه افزایش تعامل کنید و افزایش امتیاز را مشاهده کنید. آیا مقادیر INP و تعامل با هر افزایش تغییر می‌کنند؟

INP مدت زمانی را که از لحظه تعامل کاربر تا نمایش به‌روزرسانی رندر شده به کاربر طول می‌کشد، اندازه‌گیری می‌کند.

۳. اندازه‌گیری تعاملات با Chrome DevTools

DevTools را از منوی More Tools > Developer Tools ، با کلیک راست روی صفحه و انتخاب Inspect یا با استفاده از میانبر صفحه‌کلید ، باز کنید.

به پنل Performance بروید، که برای سنجش تعاملات از آن استفاده خواهید کرد.

تصویری از پنل عملکرد DevTools در کنار برنامه

در مرحله بعد، یک تعامل را در پنل Performance ضبط کنید.

  1. ضبط را فشار دهید.
  2. با صفحه تعامل داشته باشید (دکمه افزایش را فشار دهید).
  3. ضبط را متوقف کنید.

در جدول زمانی حاصل، یک مسیر Interactions پیدا خواهید کرد. با کلیک روی مثلث سمت چپ، آن را گسترش دهید.

نمایش انیمیشنی از ضبط یک تعامل با استفاده از پنل عملکرد DevTools

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

تصویری از پنل عملکرد DevTools، مکان‌نما روی تعامل در پنل معلق است، و یک راهنمای ابزار که زمان کوتاه تعامل را نشان می‌دهد

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

۴. شنوندگان رویداد طولانی مدت

فایل index.js را باز کنید و تابع blockFor را درون event listener از حالت کامنت خارج کنید.

کد کامل را ببینید: click_block.html

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();
});

فایل را ذخیره کنید. سرور تغییر را مشاهده کرده و صفحه را برای شما رفرش می‌کند.

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

ردیابی عملکرد

یک ضبط دیگر در پنل Performance انجام دهید تا ببینید آنجا چه شکلی است.

یک تعامل یک ثانیه‌ای در پنل عملکرد

چیزی که زمانی یک تعامل کوتاه بود، حالا یک ثانیه کامل طول می‌کشد.

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

۵. آزمایش: مدت زمان پردازش

روش‌های تنظیم مجدد کار شنونده رویداد را امتحان کنید تا تأثیر آن را بر INP ببینید.

ابتدا رابط کاربری را به‌روزرسانی کنید

چه اتفاقی می‌افتد اگر ترتیب فراخوانی‌های js را عوض کنید - ابتدا رابط کاربری را به‌روزرسانی کنید، سپس بلوک را؟

کد کامل را ببینید: ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

آیا متوجه شدید که رابط کاربری زودتر ظاهر می‌شود؟ آیا ترتیب قرارگیری بر امتیاز INP تأثیر می‌گذارد؟

سعی کنید ردگیری کنید و تعامل را بررسی کنید تا ببینید آیا تفاوتی وجود دارد یا خیر.

شنوندگان جداگانه

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

کد کامل را ببینید: two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

الان توی پنل اجرا چه شکلیه؟

انواع مختلف رویداد

بیشتر تعاملات، انواع مختلفی از رویدادها را اجرا می‌کنند، از رویدادهای اشاره‌گر یا کلید گرفته تا رویدادهای hover، focus/blur و رویدادهای ترکیبی مانند beforechange و beforeinput.

بسیاری از صفحات واقعی، شنونده‌هایی برای رویدادهای مختلف دارند.

چه اتفاقی می‌افتد اگر نوع رویداد را برای شنونده‌های رویداد تغییر دهید؟ برای مثال، یکی از شنونده‌های رویداد click را با pointerup یا mouseup جایگزین کنید؟

کد کامل را ببینید: diff_handlers.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

بدون به‌روزرسانی رابط کاربری

اگر فراخوانی برای به‌روزرسانی رابط کاربری را از شنونده رویداد حذف کنید، چه اتفاقی می‌افتد؟

کد کامل را ببینید: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});

۶. نتایج آزمایش مدت زمان پردازش

ردیابی عملکرد: ابتدا رابط کاربری را به‌روزرسانی کنید

کد کامل را ببینید: ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

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

یک تعامل هنوز یک ثانیه‌ای در پنل عملکرد

ردیابی عملکرد: شنوندگان جداگانه

کد کامل را ببینید: two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

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

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

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

نگاهی زوم‌شده به تعامل یک ثانیه‌ای در این مثال، که نشان می‌دهد اولین فراخوانی تابع کمتر از یک میلی‌ثانیه طول کشیده است

ردیابی عملکرد: انواع مختلف رویداد

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

این نتایج بسیار مشابه هستند. تعامل هنوز یک ثانیه کامل است؛ تنها تفاوت این است که شنونده click کوتاه‌ترِ فقط به‌روزرسانی رابط کاربری، اکنون پس از شنونده مسدودکننده pointerup اجرا می‌شود.

نگاهی زوم‌شده به تعامل یک ثانیه‌ای در این مثال، که نشان می‌دهد شنونده‌ی رویداد کلیک، پس از شنونده‌ی اشاره‌گر بالا، کمتر از یک میلی‌ثانیه طول می‌کشد تا کامل شود.

ردیابی عملکرد: بدون به‌روزرسانی رابط کاربری

کد کامل را ببینید: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • امتیاز به‌روزرسانی نمی‌شود، اما صفحه هنوز به‌روزرسانی می‌شود!
  • انیمیشن‌ها، جلوه‌های CSS، اکشن‌های پیش‌فرض کامپوننت وب (ورودی فرم)، ورود متن، هایلایت کردن متن، همگی همچنان در حال به‌روزرسانی هستند.

در این حالت، دکمه به حالت فعال می‌رود و با کلیک دوباره برمی‌گردد، که نیاز به رنگ‌آمیزی توسط مرورگر دارد، به این معنی که هنوز یک INP وجود دارد.

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

ضبط یک پنل اجرا، تعاملی را نشان می‌دهد که عملاً مشابه تعاملات قبلی است.

یک تعامل هنوز یک ثانیه‌ای در پنل عملکرد

غذای بیرون‌بر

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

  • این شامل شنونده‌هایی می‌شود که از اسکریپت‌ها و کدهای فریم‌ورک یا کتابخانه‌های مختلف ثبت شده‌اند و در شنونده‌ها اجرا می‌شوند، مانند به‌روزرسانی وضعیت که رندر یک کامپوننت را آغاز می‌کند.
  • نه تنها کد خودتان، بلکه تمام اسکریپت‌های شخص ثالث را نیز.

مشکل رایجی است!

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

۷. آزمایش: تأخیر ورودی

در مورد کدهای طولانی مدت که خارج از شنونده‌های رویداد اجرا می‌شوند چطور؟ برای مثال:

  • اگر یک <script> دیر بارگذاری شده داشتید که به طور تصادفی صفحه را در حین بارگذاری مسدود می‌کرد.
  • یک فراخوانی API، مانند setInterval ، که به صورت دوره‌ای صفحه را مسدود می‌کند؟

سعی کنید blockFor را از شنونده رویداد حذف کرده و آن را به setInterval() اضافه کنید:

کد کامل را ببینید: input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

چه اتفاقی می‌افتد؟

۸. نتایج آزمایش تأخیر ورودی

کد کامل را ببینید: input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

ضبط کلیک دکمه‌ای که هنگام اجرای وظیفه مسدودسازی setInterval رخ می‌دهد، منجر به یک تعامل طولانی‌مدت می‌شود، حتی بدون اینکه هیچ کار مسدودسازی در خود تعامل انجام شود!

این دوره‌های طولانی مدت اغلب وظایف طولانی نامیده می‌شوند.

با نگه داشتن ماوس روی تعامل در DevTools، خواهید دید که زمان تعامل اکنون در درجه اول به تأخیر ورودی نسبت داده می‌شود، نه مدت زمان پردازش.

پنل عملکرد DevTools یک وظیفه مسدودکننده یک ثانیه‌ای، تعاملی که در نیمه راه آن وظیفه رخ می‌دهد و یک تعامل ۶۴۲ میلی‌ثانیه‌ای که عمدتاً به تأخیر ورودی نسبت داده می‌شود را نشان می‌دهد.

توجه داشته باشید که این موضوع همیشه روی تعاملات تأثیر نمی‌گذارد! اگر هنگام اجرای وظیفه کلیک نکنید، ممکن است خوش شانس باشید. چنین عطسه‌های «تصادفی» می‌توانند کابوسی برای اشکال‌زدایی باشند، زیرا فقط گاهی اوقات باعث ایجاد مشکل می‌شوند.

یک راه برای ردیابی این موارد، اندازه‌گیری وظایف طولانی (یا فریم‌های انیمیشن طولانی ) و زمان کل بلوک‌بندی است.

۹. ارائه آهسته

تاکنون، ما عملکرد جاوا اسکریپت را از طریق تأخیر ورودی یا شنونده‌های رویداد بررسی کرده‌ایم، اما چه چیزهای دیگری بر رندر کردن رنگ بعدی تأثیر می‌گذارند؟

خب، به‌روزرسانی صفحه با افکت‌های گران‌قیمت!

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

در تاپیک اصلی:

  • چارچوب‌های رابط کاربری که نیاز به رندر به‌روزرسانی‌ها پس از تغییرات وضعیت دارند
  • تغییرات DOM یا تغییر تعداد زیادی از انتخابگرهای پرس‌وجوی CSS پرهزینه می‌تواند باعث ایجاد خطاهای زیادی در Style، Layout و Paint شود.

خارج از بحث اصلی:

  • استفاده از CSS برای تقویت جلوه‌های گرافیکی (GPU)
  • افزودن تصاویر بسیار بزرگ با وضوح بالا
  • استفاده از SVG/Canvas برای ترسیم صحنه‌های پیچیده

طرحی از عناصر مختلف رندرینگ در وب

رندرینگ ان جی

چند نمونه که معمولاً در وب یافت می‌شوند:

  • یک سایت SPA که پس از کلیک روی یک لینک، کل DOM را بدون مکث برای ارائه بازخورد بصری اولیه، بازسازی می‌کند.
  • یک صفحه جستجو که فیلترهای جستجوی پیچیده‌ای را با رابط کاربری پویا ارائه می‌دهد، اما برای این کار از شنونده‌های گران‌قیمت استفاده می‌کند.
  • یک دکمه‌ی حالت تاریک که استایل/طرح‌بندی کل صفحه را فعال می‌کند

۱۰. آزمایش: تأخیر در ارائه

requestAnimationFrame کندAnimationFrame

بیایید با استفاده از API مربوط به requestAnimationFrame() یک تأخیر طولانی در ارائه را شبیه‌سازی کنیم.

فراخوانی blockFor را به یک فراخوانی requestAnimationFrame منتقل کنید تا پس از بازگشت شنونده رویداد اجرا شود:

کد کامل را ببینید: presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

چه اتفاقی می‌افتد؟

۱۱. نتایج آزمایش تأخیر ارائه

کد کامل را ببینید: presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

این تعامل یک ثانیه طول کشید، پس چه اتفاقی افتاد؟

requestAnimationFrame قبل از رنگ‌آمیزی بعدی، درخواست فراخوانی مجدد می‌کند. از آنجایی که INP زمان بین تعامل تا رنگ‌آمیزی بعدی را اندازه‌گیری می‌کند، blockFor(1000) در requestAnimationFrame به مدت یک ثانیه کامل به مسدود کردن رنگ‌آمیزی بعدی ادامه می‌دهد.

یک تعامل هنوز یک ثانیه‌ای در پنل عملکرد

با این حال، به دو نکته توجه کنید:

  • با نگه داشتن ماوس روی صفحه، خواهید دید که تمام زمان تعامل اکنون در «تأخیر ارائه» صرف می‌شود، زیرا مسدود شدن نخ اصلی پس از بازگشت شنونده رویداد اتفاق می‌افتد.
  • ریشه فعالیت نخ اصلی دیگر رویداد کلیک نیست، بلکه "Animation Frame Fired" است.

۱۲. تشخیص تعاملات

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

وقتی تعاملات طولانی می‌شوند، همیشه مشخص نیست که مقصر چیست. آیا دلیلش این است:

  • تأخیر ورودی؟
  • مدت زمان پردازش رویداد؟
  • تأخیر در ارائه؟

در هر صفحه‌ای که می‌خواهید، می‌توانید از DevTools برای اندازه‌گیری میزان واکنش‌گرایی استفاده کنید. برای اینکه به این عادت عادت کنید، این روند را امتحان کنید:

  1. طبق معمول در وب گشت و گذار کنید.
  2. در نمای معیارهای زنده پنل عملکرد DevTools، گزارش تعاملات (Interactions log) را زیر نظر داشته باشید.
  3. اگر تعاملی با عملکرد ضعیف مشاهده کردید، سعی کنید آن را تکرار کنید:
  • اگر نمی‌توانید آن را تکرار کنید، از گزارش تعامل برای دریافت بینش استفاده کنید.
  • اگر می‌توانید آن را تکرار کنید، در پنل Performance یک ردپا ثبت کنید.

همه تاخیرها

سعی کنید کمی از همه این مشکلات را به صفحه اضافه کنید:

کد کامل را ببینید: all_the_things.html

setInterval(() => {
  blockFor(1000);
}, 3000);

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

سپس از کنسول و پنل عملکرد برای تشخیص مشکلات استفاده کنید!

۱۳. آزمایش: کار ناهمزمان

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

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

برای امتحان کردن این، به‌روزرسانی رابط کاربری را از شنونده‌ی کلیک ادامه دهید، اما کار مسدودسازی را از timeout اجرا کنید.

کد کامل را ببینید: timeout_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

حالا چه اتفاقی می‌افتد؟

۱۴. نتایج آزمایش کار ناهمگام

کد کامل را ببینید: timeout_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

یک تعامل ۲۷ میلی‌ثانیه‌ای با یک وظیفه یک ثانیه‌ای که اکنون در ادامه‌ی ردیابی رخ می‌دهد

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

درس: اگر نمی‌توانید آن را حذف کنید، حداقل آن را جابجا کنید!

روش‌ها

آیا می‌توانیم کاری بهتر از یک setTimeout ثابت ۱۰۰ میلی‌ثانیه‌ای انجام دهیم؟ احتمالاً ما هنوز می‌خواهیم کد در سریع‌ترین زمان ممکن اجرا شود، در غیر این صورت باید آن را حذف می‌کردیم!

هدف:

  • این تعامل، incrementAndUpdateUI() را اجرا خواهد کرد.
  • blockFor() در اسرع وقت اجرا می‌شود، اما مانع از اجرای مرحله‌ی بعدی نقاشی نمی‌شود.
  • این منجر به رفتار قابل پیش‌بینی بدون «وقفه‌های جادویی» می‌شود.

برخی از راه‌های انجام این کار عبارتند از:

  • setTimeout(0)
  • Promise.then()
  • requestAnimationFrame
  • requestIdleCallback
  • scheduler.postTask()

"درخواست قاب انیمیشن پست"

برخلاف requestAnimationFrame به تنهایی (که سعی می‌کند قبل از رنگ‌آمیزی بعدی اجرا شود و معمولاً همچنان باعث کندی تعامل می‌شود)، requestAnimationFrame + setTimeout یک polyfill ساده برای requestPostAnimationFrame ایجاد می‌کند و callback را پس از رنگ‌آمیزی بعدی اجرا می‌کند.

کد کامل را ببینید: raf+task.html

function afterNextPaint(callback) {
  requestAnimationFrame(() => {
    setTimeout(callback, 0);
  });
}

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  afterNextPaint(() => {
    blockFor(1000);
  });
});

برای ارگونومی، حتی می‌توانید آن را در یک قول بپیچید:

کد کامل را ببینید: raf+task2.html

async function nextPaint() {
  return new Promise(resolve => afterNextPaint(resolve));
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  await nextPaint();
  blockFor(1000);
});

۱۵. تعاملات چندگانه (و کلیک‌های خشم)

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

نسخهٔ کاریِ مسدودکنندهٔ ناهمگامِ صفحه را دوباره امتحان کنید (یا اگر در مرحلهٔ قبل نسخه‌ی خودتان را برای به تعویق انداختن کار ارائه دادید):

کد کامل را ببینید: timeout_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

اگر چندین بار سریع کلیک کنید چه اتفاقی می‌افتد؟

ردیابی عملکرد

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

چندین وظیفه‌ی طولانی در ترد اصلی که باعث کندی تعاملات تا ۸۰۰ میلی‌ثانیه می‌شوند

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

استراتژی‌ها

در حالت ایده‌آل، ما می‌خواهیم وظایف طولانی را به طور کامل حذف کنیم!

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

۱۶. استراتژی ۱: دفع (debounce)

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

  • از setTimeout برای به تأخیر انداختن شروع کارهای پرهزینه، با یک تایمر، شاید ۵۰۰ تا ۱۰۰۰ میلی‌ثانیه، استفاده کنید.
  • هنگام انجام این کار، شناسه تایمر را ذخیره کنید.
  • اگر یک تعامل جدید از راه رسید، تایمر قبلی را با استفاده از clearTimeout لغو کنید.

کد کامل را ببینید: debounce.html

let timer;
button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  if (timer) {
    clearTimeout(timer);
  }
  timer = setTimeout(() => {
    blockFor(1000);
  }, 1000);
});

ردیابی عملکرد

تعاملات متعدد، اما در نتیجه‌ی همه آنها فقط یک کار طولانی و واحد انجام می‌شود

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

۱۷. استراتژی ۲: کار طولانی مدت را متوقف کنید

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

در حالت ایده‌آل، اگر در حین انجام وظیفه‌مان با یک تعامل مواجه شویم، می‌خواهیم کار فشرده‌مان را متوقف کنیم تا هرگونه تعامل جدید فوراً مدیریت شود. چگونه می‌توانیم این کار را انجام دهیم؟

برخی APIها مانند isInputPending وجود دارند، اما به‌طورکلی بهتر است وظایف طولانی را به بخش‌های کوچک‌تر تقسیم کنید .

تعداد زیادی setTimeout

اولین تلاش: یک کار ساده انجام دهید.

کد کامل را ببینید: small_tasks.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
  });
});

این قابلیت به مرورگر اجازه می‌دهد تا هر وظیفه را به صورت جداگانه زمان‌بندی کند و ورودی می‌تواند اولویت بالاتری داشته باشد!

تعاملات متعدد، اما تمام کارهای برنامه‌ریزی‌شده به وظایف کوچک‌تر زیادی تقسیم شده‌اند

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

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

با این حال، این استراتژی برای تجزیه کدهایی که به شدت به هم وابسته هستند، مانند حلقه for که از حالت مشترک استفاده می‌کند، به خوبی کار نمی‌کند.

اکنون با yield()

با این حال، می‌توانیم از async و await مدرن استفاده کنیم تا به راحتی «نقاط بازده» را به هر تابع جاوا اسکریپت اضافه کنیم.

برای مثال:

کد کامل را ببینید: yieldy.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldy(ms) {
  const ms_per_part = 10;
  const parts = ms / ms_per_part;
  for (let i = 0; i < parts; i++) {
    await schedulerDotYield();

    blockFor(ms_per_part);
  }
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();
  await blockInPiecesYieldy(1000);
});

مانند قبل، نخ اصلی پس از انجام یک بخش از کار، اجرا می‌شود و مرورگر قادر به پاسخگویی به هرگونه تعامل ورودی است، اما اکنون تنها چیزی که مورد نیاز است، یک await schedulerDotYield() به جای setTimeout های جداگانه است که آن را به اندازه کافی ارگونومیک می‌کند تا حتی در وسط یک حلقه for نیز قابل استفاده باشد.

اکنون با AbortContoller()

این روش جواب داد، اما هر تعامل، کار بیشتری را برنامه‌ریزی می‌کند، حتی اگر تعاملات جدیدی ایجاد شده باشد و ممکن است کاری را که باید انجام شود تغییر داده باشد.

با استراتژی debouncing، ما timeout قبلی را با هر تعامل جدید لغو کردیم. آیا می‌توانیم اینجا هم کار مشابهی انجام دهیم؟ یک راه برای انجام این کار استفاده از AbortController() است:

کد کامل را ببینید: aborty.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldyAborty(ms, signal) {
  const parts = ms / 10;
  for (let i = 0; i < parts; i++) {
    // If AbortController has been asked to stop, abandon the current loop.
    if (signal.aborted) return;

    await schedulerDotYield();

    blockFor(10);
  }
}

let abortController = new AbortController();

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  abortController.abort();
  abortController = new AbortController();

  await blockInPiecesYieldyAborty(1000, abortController.signal);
});

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

وقتی کلیک دوم زده می‌شود، حلقه اول با AbortController به عنوان لغو شده علامت‌گذاری می‌شود و یک حلقه blockInPiecesYieldyAborty جدید شروع می‌شود - دفعه بعد که حلقه اول برای اجرای مجدد برنامه‌ریزی می‌شود، متوجه می‌شود که signal.aborted اکنون true است و بلافاصله بدون انجام کار بیشتر، برمی‌گردد.

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

۱۸. نتیجه‌گیری

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

به یاد داشته باشید

  • INP تمام تعاملات را اندازه‌گیری می‌کند.
  • هر تعامل از ورودی تا رنگ‌آمیزی بعدی اندازه‌گیری می‌شود - روشی که کاربر واکنش‌گرایی را می‌بیند .
  • تأخیر ورودی، مدت زمان پردازش رویداد و تأخیر ارائه ، همگی بر پاسخگویی تعامل تأثیر می‌گذارند.
  • شما می‌توانید به راحتی INP و تجزیه و تحلیل تعاملات را با DevTools اندازه‌گیری کنید!

استراتژی‌ها

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

بیشتر بدانید