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

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

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

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

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

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

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

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

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

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

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

כדאי לנסות לשנות את סדר הפעולות של event-listener כדי לראות את ההשפעה על 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.

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

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

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

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

טייק אוויי

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Methods

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

יעד:

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

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

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

‪"requestPostAnimationFrame"

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

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

מידע נוסף