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

1. מבוא

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

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

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

מה לומדים

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

מה צריך

  • מחשב עם אפשרות לשכפל קוד מ-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): הציון הנוכחי של מהירות התגובה לאינטראקציה באתר, שהוא בדרך כלל האינטראקציה הגרועה ביותר.
  • אינטראקציה: הציון של האינטראקציה האחרונה.
  • FPS: מספר הפריימים לשנייה של ה-thread הראשי בדף.
  • טיימר: אנימציה של טיימר רץ שעוזרת להמחיש את הבעיה.

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

רוצה לנסות?

מנסים ללחוץ על הלחצן הוספה ורואים את הניקוד עולה. האם הערכים של INP ושל Interaction משתנים עם כל עלייה?

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

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

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

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

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

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

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

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

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

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

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

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

4. פונקציות event listener שפועלות לאורך זמן

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

הקוד המלא: click_block.html

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

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

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

נתוני מעקב אחר ביצועים

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

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

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

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

5. Experiment: processing duration

כדאי לנסות לשנות את סדר הפעולות של event-listener כדי לראות את ההשפעה על INP.

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

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

הקוד המלא: ui_first.html

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

האם שמת לב שהממשק הופיע קודם? האם הסדר משפיע על ציוני INP?

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

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

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

הקוד המלא: two_click.html

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

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

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

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

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

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

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

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

יומן למעקב ביצועים: listeners נפרדים

הקוד המלא: 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 החסימה.

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

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

הקוד המלא: no_ui.html

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

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

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

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

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

טייק אוויי

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

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

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

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

7. Experiment: input delay

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

תקופות ארוכות כאלה נקראות לעיתים קרובות משימות ארוכות.

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

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

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

אחת הדרכים לאתר את הבעיות האלה היא למדוד משימות ארוכות (או Long Animation Frames) וTotal Blocking Time.

9. הצגה איטית

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

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

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

ב-thread הראשי:

  • מסגרות ממשק משתמש שצריכות לעבד עדכונים אחרי שינויים במצב
  • שינויים ב-DOM או החלפה בין הרבה סלקטורים יקרים של שאילתות CSS יכולים להפעיל הרבה פעולות של Style,‏ Layout ו-Paint.

מחוץ ל-thread הראשי:

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

סקיצה של הרכיבים השונים של עיבוד באינטרנט

RenderingNG

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

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

10. Experiment: presentation delay

requestAnimationFrame איטי

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

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

הקוד המלא: presentation_delay.html

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

מה קורה

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

הקוד המלא: presentation_delay.html

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

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

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

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

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

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

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

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

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

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

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

  1. גולשים באינטרנט כרגיל.
  2. חשוב לעקוב אחרי יומן האינטראקציות בתצוגת המדדים בזמן אמת בחלונית 'ביצועים' בכלי הפיתוח.
  3. אם אתם רואים אינטראקציה עם ביצועים נמוכים, נסו לחזור עליה:
  • אם לא הצלחתם לשחזר את הפעולה, תוכלו להשתמש ביומן האינטראקציות כדי לקבל תובנות.
  • אם אתם מצליחים לשחזר את הבעיה, הקליטו תיעוד בחלונית Performance (ביצועים).

כל העיכובים

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

הקוד המלא: all_the_things.html

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

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

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

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

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

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

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

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

הקוד המלא: 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 אלפיות השנייה עם משימה שנמשכת הרבה זמן באורך שנייה אחת שמתרחשת עכשיו מאוחר יותר ב-trace

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

לקח: אם אי אפשר להסיר את הפריט, לפחות כדאי להזיז אותו!

Methods

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

יעד:

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

כדי לעשות את זה, אפשר:

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

‪"requestPostAnimationFrame"

בניגוד ל-requestAnimationFrame לבד (שמנסה לפעול לפני הצביעה הבאה ובדרך כלל עדיין גורם לאינטראקציה איטית), requestAnimationFrame + setTimeout יוצר פוליפיל פשוט ל-requestPostAnimationFrame, ומריץ את הקריאה החוזרת אחרי הצביעה הבאה.

הקוד המלא: raf+task.html

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

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

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

כדי לשפר את הארגונומיה, אפשר אפילו להשתמש באובייקט promise:

הקוד המלא: 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 הראשי, שגורמות לאינטראקציות להיות איטיות עד כדי 800ms

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

לדוגמה:

הקוד המלא: 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()

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

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

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

כשמתקבלת קליק שני, הלולאה הראשונה מסומנת כבוטלה עם AbortController ומתחילה לולאה חדשה של blockInPiecesYieldyAborty. בפעם הבאה שהלולאה הראשונה מתוזמנת לפעול שוב, היא מבחינה בכך ש-signal.aborted הוא עכשיו true וחוזרת מיד בלי לבצע עבודה נוספת.

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

18. סיכום

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

חשוב לזכור

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

שיטות

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

מידע נוסף