הסבר על האינטראקציה עד השלב הבא (INP)

1. מבוא

הדגמה אינטראקטיבית ו-Codelab ללמידה על Interaction to Next Paint (INP).

דיאגרמה שמתארת אינטראקציה ב-thread הראשי. המשתמש מוסיף קלט בזמן שהוא חוסם משימות. הקלט מתעכב עד שהמשימות האלה מסתיימות. בסיום השלבים האלה מריצים את פונקציית ה-הגדלה, mouseup ו-click listener להרצה, ואז עבודות העיבוד והציור יתחילו לפעול עד שהפריים הבא יוצג.

דרישות מוקדמות

מה לומדים

  • איך האינטראקציות בין האינטראקציות של המשתמשים והטיפול באינטראקציות האלה משפיעות על הרספונסיביות של הדף.
  • איך לצמצם את העיכובים ולצמצם את העיכובים כדי לספק חוויית משתמש חלקה.

מה צריך

  • מחשב עם אפשרות לשכפל קוד מ-GitHub ולהריץ פקודות NPM.
  • כלי לעריכת טקסט.
  • גרסה עדכנית של Chrome כדי שכל המדידות של האינטראקציות יפעלו.

2. להגדרה

קבלת הקוד והפעלה שלו

הקוד נמצא במאגר 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: קצב הפריימים הראשי של ה-thread הראשי לשנייה בדף.
  • טיימר: אנימציית טיימר רצה שעוזרת להמחיש את המצב 'jank'.

הרשומות של ה-FPS והטיימר לא נחוצות בכלל למדידת אינטראקציות. הוספנו אותן רק כדי להקל על הצגת הרספונסיביות.

רוצה לנסות?

אפשר לנסות לבצע פעולות עם הלחצן עלייה ולצפות בעלייה בציון. האם הערכים INP ו-Interaction (אינטראקציה) משתנים בכל פעם?

מדד INP מודד את משך הזמן שעובר מהרגע שבו המשתמש מבצע אינטראקציה עד שהדף מציג למשתמש את העדכון שעבר עיבוד.

3. מדידת אינטראקציות עם כלי הפיתוח ל-Chrome

פותחים את כלי הפיתוח דרך כלים נוספים > תפריט הכלים למפתחים: לוחצים לחיצה ימנית על הדף ובוחרים באפשרות בדיקה או באמצעות מקש קיצור.

עוברים לחלונית ביצועים, שבה משתמשים כדי למדוד אינטראקציות.

צילום מסך של חלונית הביצועים של כלי הפיתוח לצד האפליקציה

בשלב הבא, מתעד את האינטראקציה בחלונית הביצועים.

  1. לוחצים על 'הקלטה'.
  2. מבצעים אינטראקציה עם הדף (לוחצים על הלחצן עלייה).
  3. מפסיקים את ההקלטה.

בציר הזמן שייווצר יופיע הטראק אינטראקציות. כדי להרחיב את הקטע, לוחצים על המשולש שבצד שמאל.

הדגמה של הקלטת אינטראקציה באמצעות חלונית הביצועים של כלי הפיתוח

יופיעו שתי אינטראקציות. כדי להגדיל את התצוגה של הסרטון השני, גוללים או לוחצים לחיצה ארוכה על המקש W.

צילום מסך של חלונית הביצועים של כלי הפיתוח, העברת הסמן מעל האינטראקציה בחלונית, והסבר קצר על התזמון הקצר של האינטראקציה

אם תעבירו את העכבר מעל האינטראקציה תוכלו לראות שהאינטראקציה הייתה מהירה, משך הזמן שחלף מהקליק להמרה משך העיבוד ומשך הזמן המינימלי הוא עיכוב בקלט ועיכוב בהצגת ההצגה. משך הזמן המדויק תלוי במהירות המכונה.

4. מאזינים ותיקים לאירועים

פותחים את הקובץ index.js ומבטלים את התגובה של הפונקציה blockFor בתוך האזנה לאירועים.

להצגת הקוד המלא: click_block.html

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

שומרים את הקובץ. השרת יראה את השינוי וירענן את הדף עבורך.

מנסים שוב לבצע פעולה בדף. מעכשיו האינטראקציות יהיו איטיות יותר באופן ניכר.

מעקב ביצועים

אפשר לצלם עוד הקלטה בחלונית הביצועים כדי לראות איך זה נראה שם.

אינטראקציה של שנייה אחת בחלונית הביצועים

מה שהיה פעם אינטראקציה קצרה לוקח עכשיו שנייה שלמה.

כשמעבירים את העכבר מעל האינטראקציה, רואים שהזמן נוצל כמעט במלואו ב'משך העיבוד' – משך הזמן שלוקח לבצע את הקריאה החוזרת (callback) של ה-event listener. מכיוון שהקריאה ל-blockFor החוסמת היא לגמרי בתוך ה-event listener, כאן הזמן עובר.

5. ניסוי: משך העיבוד

כדאי לנסות דרכים לסידור מחדש של העבודה של מאזינים לאירועים כדי לראות את ההשפעה על INP.

קודם צריך לעדכן את ממשק המשתמש

מה קורה אם מחליפים את הסדר של קריאות js — קודם מעדכנים את ממשק המשתמש ואז חוסמים?

הצגת הקוד המלא: ui_first.html

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

האם הבחנת שממשק המשתמש הופיע מוקדם יותר? האם ההזמנה משפיעה על דירוגי ה-INP?

נסו לבצע מעקב ולבחון את האינטראקציה כדי לראות אם היו הבדלים.

הפרדת מאזינים

מה קורה אם מעבירים את העבודה ל-event listener נפרד? עדכון ממשק המשתמש ב-event listener אחד וחסימת הדף מ-listener נפרד.

לצפייה בקוד המלא: two_click.html

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

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

איך זה נראה בחלונית הביצועים עכשיו?

סוגים שונים של אירועים

רוב האינטראקציות יפעילו סוגים רבים של אירועים, למשל: מיקום הסמן או אירועים מרכזיים, כדי להעביר את העכבר מעל התמונה, להתמקד או לטשטש אירועים סינתטיים כמו beforechange ו-beforeinput.

לדפים אמיתיים רבים יש מאזינים לאירועים רבים ושונים.

מה קורה אם משנים את סוגי האירועים עבור מאזינים של אירועים? לדוגמה, להחליף את אחד מרכיבי event listener של click ב-pointerup או ב-mouseup?

להצגת הקוד המלא: diff_handlers.html

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

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

אין עדכונים בממשק המשתמש

מה קורה אם מסירים את הקריאה לעדכון ממשק המשתמש מ-event listener?

הצגת הקוד המלא: no_ui.html

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

6. משך העיבוד של תוצאות הניסוי

מעקב ביצועים: קודם צריך לעדכן את ממשק המשתמש

הצגת הקוד המלא: ui_first.html

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

בהקלטה של לחיצה על הלחצן בחלונית הביצועים אפשר לראות שהתוצאות לא השתנו. אמנם הופעל עדכון ממשק משתמש לפני קוד החסימה, אבל הדפדפן לא עדכן בפועל את מה שנצבע במסך עד לאחר ש-event listener הושלם. המשמעות היא שהאינטראקציה עדיין ארכה קצת יותר משנייה.

אינטראקציה שנמשכת שנייה אחת בחלונית הביצועים

מעקב ביצועים: מאזינים נפרדים

לצפייה בקוד המלא: two_click.html

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

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

שוב, אין הבדל מבחינה פונקציונלית. האינטראקציה עדיין נמשכת שנייה שלמה.

אם תתקרבו לאינטראקציה של הקליק, תוכלו לראות שאכן קיימות שתי פונקציות שונות כתוצאה מהאירוע click.

כצפוי, הגרסה הראשונה – עדכון ממשק המשתמש – פועלת מהר מאוד, והשנייה לוקחת שנייה שלמה. עם זאת, סך כל ההשפעות שלהם יוצר אותה אינטראקציה איטית עם משתמש הקצה.

תצוגה מוגדלת של האינטראקציה שנמשכת שנייה אחת בדוגמה הזו, שבה רואים שהקריאה להפעלת הפונקציה הראשונה נמשכת פחות מאלפית שנייה

מעקב ביצועים: סוגים שונים של אירועים

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

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

התוצאות האלה דומות מאוד. האינטראקציה עדיין נמשכת שנייה שלמה; ההבדל היחיד הוא שה-listener הקצר יותר של click לעדכון ממשק המשתמש בלבד פועל עכשיו אחרי ה-listener החוסם pointerup.

תצוגה מוגדלת של האינטראקציה שנמשכת שנייה אחת בדוגמה הזו, שמראה את ה-event listener של הקליק, שהשלמתו נמשכת פחות מאלפית שנייה, לאחר ההאזנה

מעקב ביצועים: אין עדכון בממשק המשתמש

הצגת הקוד המלא: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • הציון לא מתעדכן, אבל הדף עדיין מתעדכן.
  • אנימציות, אפקטים של CSS, פעולות ברירת מחדל של רכיבי אינטרנט (קלט טופס), הזנת טקסט, טקסט שמדגיש את כל העדכונים ימשיכו להתעדכן.

במקרה כזה, הלחצן עובר למצב 'פעיל' וחוזר כשלוחצים עליו. לשם כך, הדפדפן עובר למצב 'צבע', כך שעדיין יש INP.

מכיוון שמאזינים לאירוע חסמו את ה-thread הראשי למשך שנייה אחת ומונע צביעה של הדף, האינטראקציה עדיין נמשכת שנייה שלמה.

הקלטת חלונית הביצועים מראה את האינטראקציה כמעט זהה לזו שהתרחשה לפני כן.

אינטראקציה שנמשכת שנייה אחת בחלונית הביצועים

טייק אוויי

כל קוד שרץ ב-event listener כל יעכב את האינטראקציה.

  • זה כולל מאזינים שנרשמו מסקריפטים ומקוד של framework או ספרייה שונים שרץ ב-מאזינים, למשל עדכון מצב שמפעיל רינדור של רכיב.
  • לא רק הקוד שלך, אלא גם כל הסקריפטים של צד שלישי.

זוהי בעיה נפוצה!

לסיום: גם אם הקוד לא גורם להצגת קובץ צבע, זה לא אומר שהצבע לא ימתין שמקשיבים לאירועי אירוע איטיים יסתיימו.

7. ניסוי: השהיה לאחר קלט

מה לגבי קוד שרץ זמן רב מחוץ ל-event listener? לדוגמה:

  • אם אירוע <script> נטען מאוחר ונחסם באופן אקראי במהלך הטעינה.
  • קריאה ל-API, כמו setInterval, שחוסמת את הדף מדי פעם?

כדאי לנסות להסיר את blockFor מה-event listener ולהוסיף אותו ל-setInterval():

לצפייה בקוד המלא: input_delay.html

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


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

מה קורה

8. יש להזין השהיה של תוצאות הניסוי

לצפייה בקוד המלא: input_delay.html

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


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

תיעוד של קליק על לחצן שמתרחש בזמן שמשימת החסימה setInterval פעלה הובילה לאינטראקציה ממושכת, גם אם לא בוצעה פעולת חסימה באינטראקציה עצמה.

התקופות הממושכות האלה נקראות בדרך כלל משימות ארוכות.

אם תעבירו את העכבר מעל האינטראקציה בכלי הפיתוח, תוכלו לראות שזמן האינטראקציה משויך עכשיו בעיקר לעיכוב בהזנת הקלט, ולא למשך העיבוד.

חלונית הביצועים של כלי הפיתוח שמציגה משימת חסימה של שנייה אחת, אינטראקציה שהתרחשה בחלקה במהלך המשימה ואינטראקציה של 642 אלפיות השנייה, שמשויכת בעיקר לעיכוב קלט

שימו לב, היכולת הזו לא תמיד משפיעה על האינטראקציות! אם לא תלחצו כשהמשימה פועלת, אולי יהיה לכם מזל. "אקראי" כזה עיטוש יכול להיות סיוט לניפוי באגים כאשר רק לפעמים הם גורמים לבעיות.

אחת הדרכים לעקוב אחרי אירועים כאלה היא באמצעות מדידה של משימות ארוכות (או מסגרות אנימציה ארוכות) וזמן החסימה הכולל.

9. הצגה איטית

עד עכשיו בדקנו את הביצועים של JavaScript באמצעות השהיית קלט או פונקציות event listener, אך מה עוד משפיע על עיבוד הצבע הבא?

ובכן, עדכון הדף באפקטים יקרים!

גם אם עדכון הדפים יתרחש במהירות, ייתכן שהדפדפן יצטרך להשקיע מאמצים רבים כדי לעבד אותם.

ב-thread הראשי:

  • frameworks של ממשק משתמש שצריכים לעבד עדכונים אחרי שינויים במצב
  • שינויי DOM או החלפת בוררים יקרים של שאילתות CSS עשויים להפעיל סוגים רבים של סגנון, פריסה וצבע.

בלי שהשרשור הראשי

  • שימוש ב-CSS להפעלת אפקטים של GPU
  • הוספת תמונות גדולות מאוד ברזולוציה גבוהה
  • שימוש ב-SVG/Canvas כדי לצייר סצנות מורכבות

שרטוט של הרכיבים השונים של הרינדור באינטרנט

RenderingNG

כמה דוגמאות נפוצות באינטרנט:

  • אתר SPA שבונה מחדש את ה-DOM כולו לאחר לחיצה על קישור, בלי להשהות אותו כדי לספק משוב חזותי ראשוני.
  • דף חיפוש שמציע מסנני חיפוש מורכבים עם ממשק משתמש דינמי, אבל לשם כך מריץ מאזינים יקרים.
  • מתג של מצב כהה שמפעיל את הסגנון/הפריסה בכל הדף

10. ניסוי: עיכוב בהצגת המצגת

requestAnimationFrame איטי

נניח שיש עיכוב ארוך בהצגת המודעות באמצעות API של requestAnimationFrame().

מעבירים את הקריאה blockFor לקריאה חוזרת (callback) של requestAnimationFrame כדי שהיא תפעל אחרי שה-event listener חוזר:

לצפייה בקוד המלא: present_delay.html

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

מה קורה

11. תוצאות הניסוי של השהיית ההצגה

לצפייה בקוד המלא: present_delay.html

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

האינטראקציה נותרה שנייה ארוכה, אז מה קרה?

התקבלה בקשה לקריאה חוזרת (callback) של requestAnimationFrame לפני הציור הבא. מכיוון ש-INP מודד את הזמן מהאינטראקציה ועד לצביעה הבאה, blockFor(1000) ב-requestAnimationFrame ממשיך לחסום את הצבע הבא למשך שנייה שלמה.

אינטראקציה שנמשכת שנייה אחת בחלונית הביצועים

עם זאת, חשוב לשים לב לשני דברים:

  • כשמעבירים את העכבר מעל האפשרות 'עיכוב הצגה', תוכלו לראות את כל זמן האינטראקציה שנוצל כי חסימת ה-thread הראשי מתרחשת אחרי שה-event listener חוזר.
  • הרמה הבסיסית (root) של הפעילות ב-thread הראשי היא כבר לא האירוע מסוג קליק, אלא 'הופעלה מסגרת האנימציה'.

12. אבחון אינטראקציות

בדף הבדיקה הזה, הרספונסיביות היא סופר-ויזואלית, עם הציונים, הטיימרים וממשק המשתמש הנגדי...אבל כשבודקים את הדף הממוצע, הפעולה עדינה יותר.

כשהאינטראקציות נמשכות זמן רב, לא תמיד ברור מה הסיבה. האם זה:

  • השהיה לאחר קלט?
  • משך העיבוד של האירוע?
  • השהיה של הצגת תגובה?

אפשר להשתמש בכלי הפיתוח בכל דף כדי למדוד את הרספונסיביות. כדי להתרגל להרגל, אפשר לנסות את התהליך הבא:

  1. מנווטים באינטרנט כרגיל.
  2. אופציונלי: משאירים את מסוף כלי הפיתוח פתוח בזמן שהתוסף Web Vitals יתעד אינטראקציות.
  3. אם זיהית אינטראקציה שהביצועים שלה לא טובים, כדאי לנסות לחזור עליה:
  • אם אי אפשר לחזור על התהליך, אפשר להשתמש ביומני המסוף כדי לקבל תובנות.
  • אם אפשר לחזור עליו, כדאי להקליט אותו בחלונית הביצועים.

כל העיכובים

כדאי לנסות להוסיף לדף קצת מכל הבעיות הבאות:

לצפייה בקוד המלא: all_the_things.html

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

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

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

אחר כך משתמשים במסוף ובחלונית הביצועים כדי לאבחן את הבעיות.

13. ניסוי: עבודה אסינכרונית

מכיוון שאפשר להתחיל אפקטים לא-ויזואליים בתוך אינטראקציות, כמו שליחת בקשות רשת, הפעלת טיימרים או רק עדכון של המצב הגלובלי, מה יקרה כאשר אותם בסופו של דבר יעדכנו את הדף?

כל עוד הצבע הבא לאחר אינטראקציה מורשה לרנדר, גם אם הדפדפן מחליט שהוא לא באמת צריך עדכון רינדור חדש, מדידת האינטראקציות תיפסק.

כדי לנסות את זה, ממשיכים לעדכן את ממשק המשתמש מה-click listener, אבל מפעילים את מנגנון החסימה לאחר הזמן הקצוב לתפוגה.

להצגת הקוד המלא: Timeout_100.html

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

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

מה קורה עכשיו?

14. תוצאות של ניסוי עבודה אסינכרוני

להצגת הקוד המלא: Timeout_100.html

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

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

אינטראקציה של 27 אלפיות השנייה עם משימה באורך שנייה אחת שמתרחשת בשלב מאוחר יותר במעקב

עכשיו האינטראקציה קצרה כי ה-thread הראשי זמין מיד אחרי שממשק המשתמש מתעדכן. משימת החסימה הארוכה עדיין פועלת, אבל היא רק פועלת זמן מה אחרי המרת הקוד, כך שהמשתמש יקבל משוב מיידי מממשק המשתמש.

שיעור: אם אתם לא יכולים להסיר אותו, לפחות תעבירו אותו!

שיטות

האם אפשר לשפר את היעילות של setTimeout במקום קבוע של 100 אלפיות שנייה? סביר להניח שאנחנו עדיין רוצים שהקוד יפעל במהירות האפשרית, אחרת היינו צריכים פשוט להסיר אותו.

יעד:

  • האינטראקציה תריץ את incrementAndUpdateUI().
  • blockFor() יפעל בהקדם האפשרי, אבל לא יחסום את הצבע הבא.
  • התוצאה תהיה התנהגות צפויה ללא 'הזמן הקצוב לתפוגה של קסם'.

דרכים לעשות את זה הן:

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

'requestPostAnimationFrame'

בשונה מ-requestAnimationFrame בפני עצמו (שניסיון לפעול לפני הצבע הבא ובדרך כלל ימשיך לגרום לאינטראקציה איטית), requestAnimationFrame + setTimeout מאפשר לבצע מילוי פוליגונים פשוט ל-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);
});

15. אינטראקציות מרובות (וקליקים כעס)

העברה של פעולות חסימה ארוכות יכולה לעזור, אבל המשימות הארוכות האלה עדיין חוסמות את הדף, וזה ישפיע על אינטראקציות עתידיות וגם על הרבה אנימציות ועדכונים אחרים בדף.

מנסים שוב את גרסת העבודה החוסמת האסינכרונית של הדף (או את הגרסה שלכם, אם יש לכם וריאציה משלכם לגבי דחיית עבודה בשלב האחרון):

להצגת הקוד המלא: Timeout_100.html

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

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

מה קורה אם לוחצים כמה פעמים במהירות?

מעקב ביצועים

לכל קליק נדרשת משימה שנמשכת שנייה אחת, כדי להבטיח שה-thread הראשי ייחסם למשך פרק זמן משמעותי.

יש כמה משימות באורך שנייה ב-thread הראשי, שגורמות לאינטראקציות של עד 800 אלפיות השנייה

כשמשימות הארוכות האלה חופפות לקליקים חדשים שמתקבלים, הן גורמות לאינטראקציות איטיות, אף על פי שכלי ההאזנה עצמו חוזר באופן כמעט מיידי. יצרנו את אותו מצב כמו בניסוי הקודם עם עיכובים בקלט. רק הפעם, העיכוב בקלט לא מגיע מ-setInterval, אלא מהעבודה שהופעל על ידי מאזינים מוקדמים יותר לאירוע.

אסטרטגיות

במקרה אידיאלי, אנחנו רוצים להסיר לגמרי משימות ארוכות.

  • מסירים לגמרי קוד מיותר, במיוחד סקריפטים.
  • כדאי לבצע אופטימיזציה של הקוד כדי להימנע ממשימות ארוכות.
  • בטל עבודה לא פעילה כאשר אינטראקציות חדשות מגיעות.

16. אסטרטגיה 1: השהיה

אסטרטגיה קלאסית. בכל פעם שאינטראקציות מגיעות ברצף מהיר והאפקטים של העיבוד או הרשת יקרים, ההתחלה פועלת בכוונה כך שתוכלו לבטל ולהפעיל מחדש. הדפוס הזה שימושי לממשקי משתמש כמו שדות של השלמה אוטומטית.

  • ניתן להשתמש בפונקציה setTimeout כדי לדחות התחלה של עבודה יקרה עם טיימר בטווח של 500 עד 1,000 אלפיות השנייה.
  • לאחר מכן, שומרים את מזהה הטיימר.
  • אם מגיעה אינטראקציה חדשה, מבטלים את הטיימר הקודם באמצעות clearTimeout.

להצגת הקוד המלא: debounce.html

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

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

מעקב ביצועים

אינטראקציות מרובות אך ורק משימה אחת ארוכה כתוצאה מכולן

למרות מספר קליקים, רק משימה אחת מסוג blockFor תתחיל לפעול. היא ממתינה עד שלא יהיו קליקים במשך שנייה שלמה לפני ההפעלה. זוהי אסטרטגיה אידיאלית לשימוש כברירת מחדל באינטראקציות שמתרחשות ברצף – כמו הקלדה של קלט טקסט או יעדים של פריטים שצפוי לקבל הרבה קליקים מהירים.

17. אסטרטגיה 2: הפסקת עבודה ממושכת

עדיין יש סיכוי לא טוב שקליק נוסף יגיע זמן קצר אחרי שתקופת ההשהיה תסתיים, יגיע באמצע המשימה הארוכה הזו ויהיה אינטראקציה איטית מאוד בגלל עיכוב בקלט.

באופן אידיאלי, אם אינטראקציה מתרחשת במהלך המשימה שלנו, כדאי להשהות את העבודה העמוסה כדי שכל אינטראקציה חדשה תטופל מיד. איך עושים את זה?

יש כמה ממשקי 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);
  });
});

כך הדפדפן יכול לתזמן כל משימה בנפרד, והקלט יכול לקבל עדיפות גבוהה יותר.

כמה אינטראקציות, אבל כל העבודה המתוזמנת פוצלה להרבה משימות קטנות יותר

חזרנו לפעילות של חמש שניות מלאות בחמישה קליקים, אבל כל משימה של שנייה אחת לכל קליק חולקה לעשר משימות של 100 אלפיות שנייה. כך, גם כשיש כמה אינטראקציות שחופפות למשימות האלה, לאף אינטראקציה אין עיכוב בקלט במשך יותר מ-100 אלפיות השנייה! הדפדפן נותן עדיפות למאזינים נכנסים של אירועים על פני העבודה של setTimeout, והאינטראקציות נשארות רספונסיביות.

האסטרטגיה הזו עובדת טוב במיוחד כשמתזמנים נקודות כניסה נפרדות, למשל אם יש לכם כמה תכונות עצמאיות שצריך להפעיל בזמן הטעינה של האפליקציה. רק טעינת סקריפטים והרצת כל הקוד בזמן בדיקת הסקריפט עשויה להוביל להרצה של הכול במשימה ארוכה ועצומה כברירת מחדל.

עם זאת, השיטה הזו לא מתאימה גם לפיצול קוד בצימוד הדוק, כמו לולאה של for שמשתמשת במצב משותף.

עכשיו עם yield()

עם זאת, אנחנו יכולים למנף את async ו-await המודרניים כדי להוסיף בקלות "נקודות תפוקה" לכל פונקציית JavaScript.

לדוגמה:

לצפייה בקוד המלא: ויסותי.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);
});

כמו קודם, ה-thread הראשי נוצר אחרי קטע עבודה והדפדפן יכול להגיב לכל אינטראקציה נכנסת, אבל עכשיו כל מה שצריך הוא await schedulerDotYield() במקום פונקציות setTimeout נפרדות, ולכן הוא ארגונומי מספיק לשימוש גם באמצע לולאת for.

עכשיו עם AbortContoller()

זה עבד, אבל כל אינטראקציה מתזמנת יותר עבודה, גם אם אינטראקציות חדשות התקבלו וייתכן שינו את העבודה שצריך לבצע.

ביטלנו את הזמן הקצוב לתפוגה הקודם בכל אינטראקציה חדשה באמצעות אסטרטגיית ביטול ההפסקה. האם אפשר לעשות משהו דומה? דרך אחת לעשות זאת היא באמצעות 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 וחוזרת באופן מיידי ללא צורך בעבודה נוספת.

העבודה של ה-thread הראשי מורכבת עכשיו מהרבה חלקים קטנטנים, האינטראקציות קצרות, והעבודה נמשכת רק כל עוד צריך

18. סיכום

פירוט של כל המשימות הארוכות מאפשר לאתר להגיב לאינטראקציות חדשות. כך ניתן לספק משוב ראשוני במהירות ולקבל החלטות, כמו ביטול של עבודה שנמצאת בתהליך. לפעמים המשמעות היא תזמון נקודות כניסה כמשימות נפרדות. לפעמים המשמעות היא הוספת "תפוקה". במקומות המתאימים.

חשוב לזכור

  • INP מודד את כל האינטראקציות.
  • כל אינטראקציה נמדדת מהקלט ועד למקור הבא – האופן שבו המשתמש רואה את הרספונסיביות.
  • עיכוב בקלט, משך עיבוד האירוע ועיכוב ההצגה הכול משפיעים על תגובה לאינטראקציה.
  • אפשר למדוד בקלות את ה-INP והתפלגויות של האינטראקציות באמצעות כלי הפיתוח!

אסטרטגיות

  • אין בדפים קוד לטווח ארוך (משימות ארוכות).
  • צריך להעביר קוד מיותר מפונקציות ה-event listener עד לאחר ההעלאה הבאה.
  • צריך לוודא שעדכון הרינדור עצמו יעיל לדפדפן.

מידע נוסף