ממשק API של מחזור החיים של דפים

תמיכה בדפדפנים

  • Chrome:‏ 68.
  • Edge:‏ 79.
  • Firefox: לא נתמך.
  • Safari: לא נתמך.

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

רקע

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

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

כבר זמן רב יש בפלטפורמת האינטרנט אירועים שקשורים למצבים במחזור החיים, כמו load,‏ unload ו-visibilitychange. עם זאת, האירועים האלה מאפשרים למפתחים להגיב רק לשינויים במצבים במחזור החיים שהמשתמשים יזמו. כדי שהאינטרנט יפעל בצורה מהימנה במכשירים עם הספק נמוך (וגם כדי לנצל את המשאבים בצורה יעילה יותר באופן כללי בכל הפלטפורמות), הדפדפנים צריכים דרך לממש מראש את המשאבים של המערכת ולתת להם הקצאה מחדש.

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

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

Page Lifecycle API מנסה לפתור את הבעיה הזו באמצעות:

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

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

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

סקירה כללית של מצבים ואירועים במחזור החיים של הדף

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

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

ייצוג חזותי של המצב ושל רצף האירועים שמתוארים במסמך הזה.
מצב וזרימה של אירועים ב-Page Lifecycle API.

מדינות

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

מדינה תיאור
פעיל

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

מצבים קודמים אפשריים:
פסיבי (דרך האירוע focus)
קפוא (דרך האירוע resume, ולאחר מכן האירוע pageshow)

המצבים הבאים האפשריים:
פסיבי (דרך האירוע blur)

פסיבי

דף נמצא בסטטוס פסיבי אם הוא גלוי ואין בו מוקד קלט.

מצבים קודמים אפשריים:
פעיל (דרך האירוע blur)
מוסתר (דרך האירוע visibilitychange)
קפוא (דרך האירוע resume ואז האירוע pageshow)

המצבים הבאים אפשריים:
פעיל (דרך האירוע focus)
מוסתר (דרך האירוע visibilitychange)

מוסתרת

הדף במצב מוסתר אם הוא לא גלוי (ולא הוקפא, נמחק או נסגר).

מצבים קודמים אפשריים:
פסיבי (דרך האירוע visibilitychange)
קפוא (דרך האירוע resume ואז האירוע pageshow)

המצבים הבאים אפשריים:
פסיבי (דרך האירוע visibilitychange)
קפוא (דרך האירוע freeze)
מושלך (לא מתרחשים אירועים)
הושבת (לא מתרחשים אירועים)

קפוא

במצב קפוא, הדפדפן משהה את הביצוע של משימות שניתנות להקפאה בתורנויות המשימות של הדף, עד שהדף ינוקה מהקיפאון. המשמעות היא שדברים כמו שעונים של JavaScript וקריאות חוזרות (callbacks) של אחזור לא פועלים. משימות שכבר פועלות יכולות להסתיים (החשובה שבהן היא קריאת החזרה (callback) של freeze), אבל יכול להיות שהן מוגבלות במה שהן יכולות לעשות ובמשך הזמן שבו הן יכולות לפעול.

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

מצבים קודמים אפשריים:
hidden (דרך האירוע freeze)

המצבים הבאים אפשריים:
פעיל (דרך האירוע resume ואז האירוע pageshow)
פסיבי (דרך האירוע resume ואז האירוע pageshow)
מוסתרת (דרך האירוע resume)
מושלכת (לא הופעלו אירועים)

Terminated

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

מצבים קודמים אפשריים:
מוסתר (באמצעות האירוע pagehide)

המצבים הבאים האפשריים:
NONE

Discarded

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

בסטטוס discarded, הכרטיסייה עצמה (כולל כותרת הכרטיסייה וסמל הדף) בדרך כלל גלויה למשתמש, גם אם הדף לא מופיע.

מצבים קודמים אפשריים:
מוסתרים (לא מופעלים אירועים)
קפואים (לא מופעלים אירועים)

המדינות הבאות האפשריות:
ללא

אירועים

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

שם פרטים
focus

הועבר מיקוד לרכיב DOM.

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

מצבים קודמים אפשריים:
פסיבי

המצבים הנוכחיים האפשריים:
פעיל

blur

רכיב DOM איבד את המיקוד.

הערה: אירוע blur לא בהכרח מציין שינוי מצב. הוא מאותת על שינוי מצב רק אם הדף כבר לא מקבל קלט (כלומר, לא רק שהמיקוד בדף עבר מרכיב אחד לרכיב אחר).

מדינות קודמות אפשריות:
פעיל

המצבים הנוכחיים האפשריים:
פסיבי

visibilitychange

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

מצבים קודמים אפשריים:
פסיבי
מוסתר

המצבים הנוכחיים האפשריים:
פסיבי
מוסתר

freeze *

הדף הוקפא עכשיו. משימות שאפשר להקפיא אותן בתורי המשימות בדף לא יופעלו.

מצבים קודמים אפשריים:
מוסתר

המצבים הנוכחיים האפשריים:
frozen

resume *

הדפדפן המשיך בדף מקופא.

מצבים קודמים אפשריים:
קפוא

המצבים הקיימים האפשריים:
פעיל (אם ואחריו האירוע pageshow)
פסיבי (אם אחריו מופיע האירוע pageshow)
מוסתר

pageshow

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

הפעולה יכולה להיות טעינת דף חדש לגמרי או דף שנלקח מהמטמון לדף הקודם/הבא. אם הדף נלקח מהמטמון לדף הקודם/הבא, המאפיין persisted של האירוע הוא true, אחרת הוא false.

מצבים קודמים אפשריים:
קפוא (אירוע resume היה מופעל גם כן)

מצבים קיימים אפשריים:
פעיל
פסיבית
מוסתר

pagehide

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

אם המשתמש מנווט לדף אחר והדפדפן יכול להוסיף את הדף הנוכחי למטמון של דפים קודמים/באים כדי לעשות בו שימוש חוזר מאוחר יותר, הערך של המאפיין persisted של האירוע הוא true. כשהערך של true הוא 1, הדף עובר למצב frozen. אחרת, הוא עובר למצב terminated.

הסטטוסים הקודמים האפשריים:
hidden

מצבים אפשריים נוכחיים:
frozen (event.persisted הוא true, freeze האירוע הבא)
terminated (event.persisted הוא false, unload האירוע הבא)

beforeunload

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

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

הסטטוסים הקודמים האפשריים:
hidden

הסטטוסים האפשריים הנוכחיים:
terminated

unload

מתבצעת טעינה של הדף.

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

הסטטוסים הקודמים האפשריים:
hidden

הסטטוסים האפשריים הנוכחיים:
terminated

* סימן שמציין אירוע חדש שהוגדר על ידי Page Lifecycle API

תכונות חדשות שנוספו ב-Chrome 68

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

ב-Chrome 68, מפתחים יכולים עכשיו לראות מתי כרטיסייה מוסתרת קופאת ומתי היא מפשירה, על ידי האזנה לאירועים freeze ו-resume ב-document.

document.addEventListener('freeze', (event) => {
  // The page is now frozen.
});

document.addEventListener('resume', (event) => {
  // The page has been unfrozen.
});

החל מגרסה 68 של Chrome, האובייקט document כולל עכשיו מאפיין wasDiscarded ב-Chrome במחשב (בבעיה הזו מתבצע מעקב אחרי תמיכה ב-Android). כדי לקבוע אם דף הועבר ללא שימוש בזמן שהיה בכרטיסייה מוסתרת, אפשר לבדוק את הערך של המאפיין הזה בזמן טעינת הדף (הערה: צריך לטעון מחדש דפים שהועברו ללא שימוש כדי להשתמש בהם שוב).

if (document.wasDiscarded) {
  // Page was previously discarded by the browser while in a hidden tab.
}

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

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

איך לצפות במצבים של מחזור החיים של הדף בקוד

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

const getState = () => {
  if (document.visibilityState === 'hidden') {
    return 'hidden';
  }
  if (document.hasFocus()) {
    return 'active';
  }
  return 'passive';
};

לעומת זאת, אפשר לזהות את מצבי הקפאה וסופית רק ב-event event listener שלהם (freeze ו-pagehide) בהתאם לשינויים במצב.

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

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

// Stores the initial state using the `getState()` function (defined above).
let state = getState();

// Accepts a next state and, if there's been a state change, logs the
// change to the console. It also updates the `state` value defined above.
const logStateChange = (nextState) => {
  const prevState = state;
  if (nextState !== prevState) {
    console.log(`State change: ${prevState} >>> ${nextState}`);
    state = nextState;
  }
};

// Options used for all event listeners.
const opts = {capture: true};

// These lifecycle events can all use the same listener to observe state
// changes (they call the `getState()` function to determine the next state).
['pageshow', 'focus', 'blur', 'visibilitychange', 'resume'].forEach((type) => {
  window.addEventListener(type, () => logStateChange(getState()), opts);
});

// The next two listeners, on the other hand, can determine the next
// state from the event itself.
window.addEventListener('freeze', () => {
  // In the freeze event, the next state is always frozen.
  logStateChange('frozen');
}, opts);

window.addEventListener('pagehide', (event) => {
  // If the event's persisted property is `true` the page is about
  // to enter the back/forward cache, which is also in the frozen state.
  // If the event's persisted property is not `true` the page is
  // about to be unloaded.
  logStateChange(event.persisted ? 'frozen' : 'terminated');
}, opts);

הקוד הזה מבצע שלוש פעולות:

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

חשוב לזכור לגבי הקוד שכל פונקציות ה-event listener מתווספות ל-window, וכולן מעבירות את הערך {capture: true}. יכולות להיות לכך כמה סיבות:

  • לא לכל האירועים במחזור החיים של הדף יש את אותו יעד. pagehide, ו-pageshow מופעלים ב-window; visibilitychange, freeze ו-resume מופעלים ב-document, ו-focus ו-blur מופעלים ברכיבי ה-DOM המתאימים.
  • רוב האירועים האלה לא עוברים דרך כל ההורים, כלומר אי אפשר להוסיף רכיבי מעקב אחרי אירועים שלא מתעדים אותם לאלמנט אב משותף ולעקוב אחרי כולם.
  • שלב הלכידה מתרחש לפני שלבי היעד או הבועה, לכן הוספת מאזינים לשם עוזרת להבטיח שהם יפעלו לפני שקוד אחר יוכל לבטל אותם.

המלצות למפתחים לכל מצב

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

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

מדינה המלצות למפתחים
Active

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

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

Passive

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

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

Hidden

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

המעבר למצב מוסתר הוא גם שינוי המצב האחרון שהמפתחים יכולים לראות באופן מהימן (הדבר נכון במיוחד בנייד, כי משתמשים יכולים לסגור כרטיסיות או את אפליקציית הדפדפן עצמה, והאירועים beforeunload, pagehide ו-unload לא מופעלים במקרים האלה).

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

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

Frozen

במצב frozen, משימות שאפשר להקפיא בתורנויות המשימות מושהות עד שההקפאה של הדף תוסר – ויכול להיות שההקפאה לא תוסר אף פעם (למשל, אם הדף נמחק).

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

במיוחד חשוב:

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

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

Terminated

בדרך כלל אין צורך לבצע פעולה כלשהי כשדף עובר למצב סגור.

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

בנוסף (כפי שצוין בהמלצות למצב מוסתר), חשוב מאוד שהמפתחים יבינו שלא ניתן לזהות באופן מהימן את המעבר למצב הסיום במקרים רבים (במיוחד בנייד), ולכן מפתחים שתלויים באירועי סיום (למשל beforeunload,‏ pagehide ו-unload) צפויים לאבד נתונים.

Discarded

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

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

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

ממשקי API מדור קודם למחזור חיים שיש להימנע מהם

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

אירוע ההסרה

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

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

בנוסף, קיומו של הגורם המטפל באירועים רשום של unload (באמצעות onunload או addEventListener()) עלול למנוע מהדפדפנים להוסיף דפים למטמון לדף הקודם/הבא כדי לאפשר טעינה מהירה יותר אחורה וקדימה.

בכל הדפדפנים המודרניים, מומלץ תמיד להשתמש באירוע pagehide כדי לזהות אפשרויות של ביטול טעינת דפים (המכונה גם המצב terminated) במקום באירוע unload. אם צריך תמיכה ב-Internet Explorer בגרסה 10 ומטה, כדאי לזהות את האירוע pagehide ולהשתמש ב-unload רק אם הדפדפן לא תומך ב-pagehide:

const terminationEvent = 'onpagehide' in self ? 'pagehide' : 'unload';

window.addEventListener(terminationEvent, (event) => {
  // Note: if the browser is able to cache the page, `event.persisted`
  // is `true`, and the state is frozen rather than terminated.
});

האירוע beforeunload

לאירוע beforeunload יש בעיה דומה לבעיה של האירוע unload, מאחר שבעבר נוכחות של אירוע beforeunload יכלה למנוע מדפים לעמוד בדרישות לשמירה במטמון לדף הקודם/הבא. בדפדפנים מודרניים אין הגבלה כזו. עם זאת, דפדפנים מסוימים לא יפעילו את האירוע beforeunload כצעד זהירות כשמנסים להעביר דף למטמון של 'הקודם'/'הבא', כלומר האירוע לא מהימן כאות לסיום הסשן. בנוסף, בדפדפנים מסוימים (כולל Chrome) נדרשת אינטראקציה של משתמש בדף לפני שהאירוע beforeunload יופעל, מה שמשפיע עוד יותר על האמינות שלו.

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

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

במילים אחרות, אל תעשו את זה (כי הפעולה הזו מוסיפה מאזין beforeunload ללא תנאי):

addEventListener('beforeunload', (event) => {
  // A function that returns `true` if the page has unsaved changes.
  if (pageHasUnsavedChanges()) {
    event.preventDefault();

    // Legacy support for older browsers.
    return (event.returnValue = true);
  }
});

במקום זאת, צריך לעשות את הפעולות הבאות (כי הקוד הזה מוסיף את המאזין beforeunload רק כשצריך, ומסיר אותו כשלא צריך):

const beforeUnloadListener = (event) => {
  event.preventDefault();
  
  // Legacy support for older browsers.
  return (event.returnValue = true);
};

// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() => {
  addEventListener('beforeunload', beforeUnloadListener);
});

// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() => {
  removeEventListener('beforeunload', beforeUnloadListener);
});

שאלות נפוצות

למה אין סטטוס 'טעינה'?

ב-Page Lifecycle API, המצבים מוגדרים כמצבים נפרדים ולא חופפים. מכיוון שדף יכול להיטען במצב פעיל, פסיבי או מוסתר, ויכול לשנות מצבים – או אפילו להסתיים – לפני שהוא מסיים את הטעינה, אין טעם להגדיר מצב טעינה נפרד בתוך הפרדיגמה הזו.

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

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

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

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

  • הפעלת אודיו
  • שימוש ב-WebRTC
  • עדכון כותרת הטבלה או הסמל של הטבלה
  • הצגת התראות
  • שליחת התראות

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

מהו המטמון לדף הקודם/הבא?

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

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

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

איך אפשר לשמור נתונים ב-IndexedDB אם אי אפשר להריץ ממשקי API אסינכררוניים במצבים 'קפוא' או 'הושעה'?

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

בעתיד נוסיף שיטה commit() לאובייקטים מסוג IDBTransaction, שתאפשר למפתחים לבצע טרנזקציות לכתיבה בלבד, ללא צורך בקריאות חזרה (callbacks). במילים אחרות, אם המפתח רק כותב נתונים ל-IndexedDB ולא מבצע עסקה מורכבת שמכילה קריאות וכתיבה, השיטה commit() תוכל להסתיים לפני שתורנויות המשימות יושהו (בהנחה שמסד הנתונים של IndexedDB כבר פתוח).

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

  • שימוש ב-Session Storage: Session Storage הוא סנכרוני ונשמר גם אחרי שמחרימים דפים.
  • משתמשים ב-IndexedDB מ-service worker: קובץ שירות (service worker) יכול לאחסן נתונים ב-IndexedDB אחרי שהדף נסגר או נמחק. אפשר לשלוח נתונים ל-service worker דרך postMessage() במעקב האירועים freeze או pagehide, וה-service worker יכול לטפל בשמירת הנתונים.

בדיקת האפליקציה במצב 'קפוא' או 'נמחק'

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

ממשק המשתמש של Chrome Discards
ממשק המשתמש של Chrome Discards

כך תוכלו לוודא שהדף מטפל בצורה נכונה באירועים freeze ו-resume, וגם בדגל document.wasDiscarded כשדפים נטענים מחדש אחרי שהם נמחקים.

סיכום

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

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