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

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

  • Chrome: 68.
  • קצה: 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

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

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

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

נמחקה

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

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

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

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

אירועים

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

שם פרטים
focus

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

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

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

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

blur

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

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

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

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

visibilitychange

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

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

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

freeze *

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

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

המצבים הנוכחיים האפשריים:
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

* מציין אירוע חדש שהוגדר על ידי ה-API של מחזור החיים של הדף

תכונות חדשות שנוספו בגרסה 68 של Chrome

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

ב-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';
};

לעומת זאת, אפשר לזהות את המצבים frozen ו-terminated רק ב-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

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

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

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

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

Frozen

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

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

במיוחד חשוב:

  • סוגרים את כל החיבורים הפתוחים של IndexedDB.
  • סוגרים את החיבורים הפתוחים של BroadcastChannel.
  • סגירת חיבורי WebRTC פעילים.
  • מפסיקים את הסקרים ברשת או סוגרים את כל החיבורים הפתוחים של Web Socket.
  • משחררים נעילת אינטרנט מוחזקת.

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

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

Terminated

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

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

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

Discarded

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

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

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

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

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

האירוע 'הסרת הנתונים שנטענו'

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

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

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

בכל הדפדפנים המתקדמים מומלץ תמיד להשתמש באירוע pagehide כדי לזהות העלאות אפשריות של דפים (שנקראות גם המצב הסתיים) ולא באירוע 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);
});

שאלות נפוצות

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

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

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

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

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

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

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

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

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

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

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

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

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

בסטטוסים 'מושהה' ו'הושעה', משימות שאפשר להשהות בתורנויות המשימות של הדף מושהות. המשמעות היא שאי אפשר להשתמש באופן מהימן בממשקי API אסינכררוניים ומבוססי קריאה חוזרת (callback), כמו 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 DELETEs

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

סיכום

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

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