כאן מוסבר מהו הסורק של טעינת הנתונים מראש בדפדפן, איך הוא עוזר לשפר את הביצועים ואיך אפשר להימנע מהפרעה לפעילות שלו.
אחד ההיבטים של אופטימיזציית מהירות הדף שרבים מתעלמים ממנו הוא ידע בסיסי על הרכיבים הפנימיים של הדפדפן. הדפדפנים מבצעים אופטימיזציות מסוימות כדי לשפר את הביצועים בדרכים שאנחנו, כמפתחים, לא יכולים לבצע – אבל רק כל עוד האופטימיזציות האלה לא מושבשות בטעות.
אחת מהאפשרויות לאופטימיזציה של הדפדפן הפנימי היא סורק ההטענה מראש של הדפדפן. במאמר הזה נסביר איך סורק ההטענה מראש פועל, ומה חשוב יותר, איך אפשר להימנע מלהפריע לו.
מהו סורק טעינה מראש?
לכל דפדפן יש מנתח HTML ראשי שמחלק את התגים לסמלים (tokenizes) של תגי עיצוב גולמיים ומעבד אותם למודל אובייקטים. התהליך הזה נמשך עד שהמנתח מושהה כשהוא מוצא משאב חסימה, כמו גיליון סגנונות שנטען עם אלמנט <link>
או סקריפט שנטען עם אלמנט <script>
ללא מאפיין async
או defer
.
במקרה של קובצי CSS, המערכת חוסמת את העיבוד כדי למנוע הצגה קצרה של תוכן ללא עיצוב (FOUC). מצב כזה מתרחש כשאפשר לראות גרסה ללא עיצוב של דף למשך זמן קצר לפני החלת הסגנונות עליו.
הדפדפן חוסם גם את הניתוח והעיבוד של הדף כשהוא נתקל ברכיבי <script>
ללא מאפיין defer
או async
.
הסיבה לכך היא שהדפדפן לא יכול לדעת בוודאות אם סקריפט נתון ישנה את ה-DOM בזמן שמנתח ה-HTML הראשי עדיין מבצע את עבודתו. לכן, נהוג לטעון את קוד ה-JavaScript בסוף המסמך, כדי שההשפעות של ניתוח ועיבוד גרפי חסומים יהיו שולית.
אלה סיבות טובות לכך שהדפדפן צריך לחסום גם את הניתוח וגם את העיבוד. עם זאת, אסור לחסום אף אחד מהשלבים החשובים האלה, כי הם עלולים לעכב את הגילוי של משאבים חשובים אחרים. למרבה המזל, הדפדפנים עושים כמיטב יכולתם כדי לצמצם את הבעיות האלה באמצעות מנתח HTML משני שנקרא סורק טעינה מראש.
התפקיד של סורק טעינה מראש הוא ספקולטיבי, כלומר הוא בודק את התגים הגולמיים כדי למצוא משאבים לאחזור באופן הזדמנותי לפני שמנתח ה-HTML הראשי יגלה אותם.
איך יודעים מתי סורק ההטענה מראש פועל
הסורק של הטעינה מראש קיים כי יש חסימה של העיבוד והניתוח. אם שתי בעיות הביצועים האלה לא היו קיימות אף פעם, סורק ההטענה מראש לא היה שימושי במיוחד. המפתח להבנת היתרון של דף אינטרנט מסוים מסורק הטעינה מראש תלוי בתופעות החסימה האלה. כדי לעשות זאת, אפשר להוסיף עיכוב מלאכותי לבקשות כדי לבדוק איפה סורק ההטענה מראש פועל.
לדוגמה, הדף הזה מכיל טקסט ותמונות בסיסיים עם גיליון סגנונות. מאחר שקובצי CSS חוסמים גם את הרינדור וגם את הניתוח, הוספת עיכוב מלאכותי של שתי שניות לגיליון הסגנונות דרך שרת proxy. העיכוב הזה מאפשר לראות בקלות רבה יותר בתרשים המפל של הרשת איפה סורק ההטענה מראש פועל.
כפי שאפשר לראות בתרשים המפל, סורק ההטענה מראש מזהה את הרכיב <img>
גם כשעיבוד הנתונים והניתוח של המסמך חסומים. ללא האופטימיזציה הזו, הדפדפן לא יכול לאחזר דברים באופן הזדמנותי במהלך תקופת החסימה, ויהיו יותר בקשות למשאבים ברצף ולא בו-זמנית.
אחרי הדוגמה הזו, נבחן כמה דפוסים מהעולם האמיתי שבהם אפשר לעקוף את סורק ההטענה מראש – ונראה מה אפשר לעשות כדי לפתור אותם.
סקריפטים מושתלים של async
נניח שיש לכם קטע HTML ב-<head>
שכולל קצת JavaScript בקוד, כמו זה:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
סקריפטים מוזרקים הם async
כברירת מחדל, כך שכשהסקריפט הזה יוזרק, הוא יתנהג כאילו המאפיין async
הוחל עליו. המשמעות היא שהיא תפעל בהקדם האפשרי ולא תחסום את העיבוד. נשמע אופטימלי, נכון? עם זאת, אם נניח שה-<script>
הזה מוטמע בקוד אחרי רכיב <link>
שטעון קובץ CSS חיצוני, התוצאה תהיה לא אופטימלית:
בואו נסביר מה קרה:
- בשנייה 0, מתבצעת בקשה למסמך הראשי.
- אחרי 1.4 שניות, מגיע הבייט הראשון של בקשת הניווט.
- אחרי 2.0 שניות, מתבצעת בקשה ל-CSS ולתמונה.
- מאחר שהמנתח חסום לטעינת גיליון הסגנונות, וקוד ה-JavaScript המוטמע שמחדיר את הסקריפט
async
מגיע אחרי גיליון הסגנונות הזה, אחרי 2.6 שניות, הפונקציונליות שהסקריפט מספק לא זמינה בהקדם האפשרי.
זוהי דרך לא יעילה כי הבקשה לסקריפט מתבצעת רק אחרי שההורדה של גיליון הסגנונות מסתיימת. כך הסקריפט לא יפעל בהקדם האפשרי. לעומת זאת, מכיוון שאפשר לזהות את הרכיב <img>
בסימון שסופק על ידי השרת, הוא מזוהה על ידי סורק הטעינה מראש.
אז מה קורה אם משתמשים בתג <script>
רגיל עם המאפיין async
במקום להחדיר את הסקריפט ל-DOM?
<script src="/yall.min.js" async></script>
זו התוצאה:
יכול להיות שתהיה נטייה להציע שאפשר לפתור את הבעיות האלה באמצעות rel=preload
. זה אכן יפעל, אבל יכולות להיות לכך תופעות לוואי. אחרי הכל, למה להשתמש ב-rel=preload
כדי לפתור בעיה שאפשר למנוע על ידי לא להחדיר רכיב <script>
ל-DOM?
טעינת נתונים מראש 'פותרת' את הבעיה הזו, אבל היא יוצרת בעיה חדשה: הסקריפט async
בשני הדמואים הראשונים – למרות שהוא נטען ב-<head>
– נטען בעדיפות 'נמוכה', בעוד שגיליון הסגנונות נטען בעדיפות 'הגבוהה ביותר'. בדמו האחרון שבו הסקריפט async
נטען מראש, גיליון הסגנונות עדיין נטען בעדיפות 'הגבוהה ביותר', אבל העדיפות של הסקריפט שודרגה ל'גבוהה'.
כשהעדיפות של משאב מסוים עולה, הדפדפן מקצה לו יותר רוחב פס. כלומר, למרות שלגיליון הסגנונות יש את העדיפות הגבוהה ביותר, העדיפות הגבוהה של הסקריפט עשויה לגרום לסכסוך על רוחב הפס. זה יכול להיות גורם בחיבור איטי או במקרים שבהם המשאבים גדולים למדי.
התשובה היא פשוטה: אם צריך סקריפט במהלך ההפעלה, אל תכשילו את סורק ההטענה מראש על ידי הזרקה שלו ל-DOM. מומלץ להתנסות לפי הצורך במיקום הרכיב <script>
, וגם במאפיינים כמו defer
ו-async
.
טעינה מדורגת באמצעות JavaScript
טעינת נתונים באיטרציות היא שיטה מצוינת לחיסכון בנתונים, שנעשה בה שימוש לעיתים קרובות בתמונות. עם זאת, לפעמים טעינת הנתונים באיטרציות מופעלת באופן שגוי על תמונות שנמצאות "מעל למסך", אם אפשר לנסח זאת כך.
כתוצאה מכך, יכולות להיות בעיות פוטנציאליות בזיהוי המשאבים לגבי סורק ההטענה מראש, והן עלולות לגרום לעיכוב מיותר בזמן הנדרש כדי לזהות הפניה לתמונה, להוריד אותה, לפענח אותה ולהציג אותה. ניקח לדוגמה את תגי העיצוב הבאים של תמונה:
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
שימוש בתחילית data-
הוא דפוס נפוץ בתוכנות טעינה איטית שמבוססות על JavaScript. כשגוללים את התמונה לאזור התצוגה, ה-loader האטי מסיר את הקידומת data-
. כלומר, בדוגמה הקודמת, data-src
הופך ל-src
. העדכון הזה מעודד את הדפדפן לאחזר את המשאב.
התבנית הזו לא בעייתית עד שהיא חלה על תמונות שנמצאות בחלון התצוגה בזמן ההפעלה. מכיוון שהסורק של טעינת האפליקציה מראש לא קורא את המאפיין data-src
באותו אופן שבו הוא קורא את המאפיין src
(או srcset
), ההפניה לתמונה לא מתגלה מוקדם יותר. גרוע מכך, טעינת התמונה מתעכבת עד אחרי שהקוד של JavaScript של הטעינה הלא מיידית מוריד, מקמפל ומבצע.
בהתאם לגודל התמונה – שעשוי להיות תלוי בגודל אזור התצוגה – יכול להיות שהיא תהיה רכיב מתאים להצגת חלק התוכן הגדול ביותר (LCP). כשסורק הטעינה מראש לא יכול לאחזר באופן ספקולטיבי את משאב התמונה מראש – יכול להיות במהלך הנקודה שבה סגנונות העיצוב של הדף חוסמים את הרינדור – מדד ה-LCP נפגע.
הפתרון הוא לשנות את ה-Markup של התמונה:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
זהו התבנית האופטימלית לתמונות שנמצאות באזור התצוגה בזמן ההפעלה, כי סורק ההטענה מראש יזהה ויאתר את משאב התמונה מהר יותר.
התוצאה בדוגמה הפשוטה הזו היא שיפור של 100 אלפיות השנייה ב-LCP בחיבור איטי. אולי זה לא נראה כמו שיפור משמעותי, אבל הוא בהחלט כזה אם מביאים בחשבון שהפתרון הוא תיקון מהיר של הרכיב 'סימון', ורוב דפי האינטרנט מורכבים יותר מקבוצת הדוגמאות הזו. המשמעות היא שרכיבי LCP עשויים להתחרות על רוחב הפס עם משאבים רבים אחרים, ולכן אופטימיזציות כאלה הופכות להיות חשובות יותר ויותר.
תמונות רקע ב-CSS
חשוב לזכור שהסורק של טעינת האובייקטים מראש בדפדפן סורק סימון. הוא לא סורק סוגי משאבים אחרים, כמו CSS, שעשוי לכלול אחזור של תמונות שנכס background-image
מפנה אליהן.
בדומה ל-HTML, הדפדפנים מעבדים את ה-CSS לתוך מודל אובייקטים משלהם, שנקרא CSSOM. אם משאבים חיצוניים מתגלים במהלך היצירה של ה-CSSOM, המערכת שולחת בקשה למשאבים האלה בזמן הגילוי, ולא באמצעות הסורק של טעינת הנתונים מראש.
נניח שהמועמד ל-LCP של הדף הוא רכיב עם מאפיין CSS background-image
. במהלך טעינת המשאבים מתרחשים הדברים הבאים:
במקרה כזה, סורק הטעינה מראש לא מפסיק לפעול, אלא פשוט לא מעורב. עם זאת, אם אחד מהמועמדים ל-LCP בדף מגיע מנכס CSS מסוג background-image
, כדאי לטעון מראש את התמונה הזו:
<!-- Make sure this is in the <head> below any
stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">
ההצעה rel=preload
קטנה, אבל היא עוזרת לדפדפן לגלות את התמונה מוקדם יותר מאשר אחרת:
בעזרת ההצעה rel=preload
, המערכת תזהה את ה-LCP המועמד מוקדם יותר, וכך זמן ה-LCP יתקצק. ההצעה הזו עוזרת לפתור את הבעיה, אבל ייתכן שהאפשרות הטובה יותר היא לבדוק אם צריך לטעון את התמונה שנבחרה כ-LCP מ-CSS. באמצעות תג <img>
, תוכלו לשלוט טוב יותר בחירת התמונה שתתאימה למסך, תוך מתן אפשרות לסורק של טעינת האובייקטים מראש לזהות אותה.
הטמעת יותר מדי משאבים בקוד
הטמעה בקוד היא שיטה שבה משאב ממוקם בתוך ה-HTML. אפשר להוסיף גיליונות סגנונות בתוך שורות (inline) באלמנטים מסוג <style>
, סקריפטים באלמנטים מסוג <script>
ולכל משאב אחר כמעט באמצעות קידוד base64.
הטמעת משאבים בקוד יכולה להיות מהירה יותר מהורדה שלהם, כי לא נשלחת בקשה נפרדת עבור המשאב. הוא מופיע ישירות במסמך, וטוען באופן מיידי. עם זאת, יש לכך חסרונות משמעותיים:
- אם אתם לא שומרים את ה-HTML במטמון – ואתם לא יכולים לעשות זאת אם התגובה ב-HTML היא דינמית – המשאבים שנוספו לקוד אף פעם לא נשמרים במטמון. הדבר משפיע על הביצועים כי לא ניתן לעשות שימוש חוזר במשאבים שמוטמעים בקוד.
- גם אם אפשר לשמור HTML במטמון, משאבים שמוטמעים בקוד לא משותפים בין מסמכים. כך פוחתת היעילות של שמירת הקבצים במטמון בהשוואה לקבצים חיצוניים שאפשר לשמור במטמון ולהשתמש בהם מחדש במקור כולו.
- אם תוסיפו יותר מדי תוכן בקוד, הסריקה של טעינת הנתונים מראש תתעכב בזיהוי המשאבים בהמשך המסמך, כי הורדת התוכן הנוסף שמוטמע בקוד אורכת יותר זמן.
ניקח לדוגמה את הדף הזה. בתנאים מסוימים, התמונה בחלק העליון של הדף היא האפשרות ל-LCP, וקוד ה-CSS נמצא בקובץ נפרד שנטען על ידי רכיב <link>
. הדף כולל גם ארבע גופנים לאינטרנט, שמתבקשים כקבצים נפרדים ממשאב ה-CSS.
מה קורה אם ה-CSS וגם כל הגופנים מוטמעים כמשאבי base64?
בדוגמה הזו, ההשפעה של הטמעת קוד בתוך הטקסט גורמת לתוצאות שליליות לגבי LCP – וגם לביצועים באופן כללי. בגרסת הדף שבה לא מוטמעים רכיבים, תמונת ה-LCP נצבעת תוך כ-3.5 שניות. הדף שבו כל התוכן מוטמע לא מציג את התמונה של ה-LCP עד אחרי יותר מ-7 שניות.
יש כאן עוד גורמים מלבד הסורק של טעינה מראש. הטמעת גופנים בקוד היא לא אסטרטגיה מומלצת כי base64 הוא פורמט לא יעיל למשאבים בינאריים. גורם נוסף הוא שמשאבים חיצוניים של גופנים לא מורידים אלא אם המערכת קובעת שהם נחוצים ל-CSSOM. כשהגופנים האלה מוטמעים בקוד כ-base64, הם מורידים גם אם הם נחוצים לדף הנוכחי וגם אם לא.
האם טעינת נתונים מראש תעזור בעניין? ודאי. אפשר לטעון מראש את תמונת ה-LCP ולצמצם את זמן ה-LCP, אבל להוספת משאבים בקוד יכולות להיות השלכות שליליות אחרות על הביצועים, כי ייתכן שלא ניתן יהיה לשמור את ה-HTML במטמון. גם המדד הצגת תוכן ראשוני (FCP) מושפע מהדפוס הזה. בגרסת הדף שבה לא הוכנסו רכיבים בקוד, משך הזמן FCP הוא כ-2.7 שניות. בגרסה שבה הכל מוטמע, משך הזמן של FCP הוא כ-5.8 שניות.
חשוב מאוד להיזהר כשמזינים דברים בתוך HTML, במיוחד משאבים בקידוד base64. באופן כללי, לא מומלץ לעשות זאת, למעט במקרים של משאבים קטנים מאוד. מומלץ להשתמש בקוד מוטמע כמה שפחות, כי שימוש מוגזם בקוד מוטמע הוא משחק עם האש.
עיבוד תגים באמצעות JavaScript בצד הלקוח
אין ספק: JavaScript משפיע בהחלט על מהירות הדף. המפתחים מסתמכים עליו לא רק כדי לספק אינטראקטיביות, אלא גם כדי לספק את התוכן עצמו. כך, בחלק מהמקרים, חוויית המפתחים משתפרת, אבל היתרונות למפתחים לא תמיד מתורגמים ליתרונות למשתמשים.
דפוס אחד שיכול לעקוף את סורק העומס מראש הוא עיבוד תגים באמצעות JavaScript בצד הלקוח:
כשעומסי נתונים של רכיבי עיבוד נתונים (payload) נכללים ב-JavaScript בדפדפן ועוברים עיבוד על ידו, כל המשאבים ברכיבי העיבוד האלה לא גלויים לכלי לטעינה מראש. כתוצאה מכך, גילוי המשאבים החשובים מתעכב, וזה משפיע בהחלט על LCP. בדוגמאות האלה, הבקשה לתמונה של LCP מתעכבת באופן משמעותי בהשוואה לחוויה המקבילה שעבר עיבוד בשרת ולא נדרשת לה JavaScript כדי להופיע.
זהו נושא שקצת חורג מהנושא המרכזי של המאמר, אבל ההשפעות של עיבוד ה-Markup בצד הלקוח חורגות בהרבה מהיכולת לעקוף את סורק ההטענה מראש. ראשית, הוספת JavaScript כדי להפעיל חוויית משתמש שלא דורשת זאת מגדילה את זמן העיבוד הלא הכרחי, שעלול להשפיע על זמן האינטראקציה עד לציור הבא (INP). יש סיכוי גבוה יותר שעיבוד כמויות גדולות במיוחד של רכיבי Markup בצד הלקוח ייצור משימות ארוכות בהשוואה לאותה כמות של רכיבי Markup שנשלחים מהשרת. הסיבה לכך – מלבד העיבוד הנוסף שכרוך ב-JavaScript – היא שדפדפנים מעבירים סטרימינג של רכיבי קוד מהשרת ומחלקים את העיבוד כך שמשימות ארוכות מוגבלות. לעומת זאת, רכיבי קוד שעיבד הלקוח מטופלים כמשימה אחת מונוליתית, שעשויה להשפיע על ה-INP של הדף.
הפתרון לתרחיש הזה תלוי בתשובה לשאלה הבאה: האם יש סיבה לכך שהשרת לא יכול לספק את ה-Markup של הדף, במקום להפעיל אותו בלקוח? אם התשובה היא 'לא', כדאי לשקול להשתמש ברינדור בצד השרת (SSR) או בסימון שנוצר באופן סטטי, במידת האפשר. כך סורק הטעינה מראש יוכל לגלות משאבים חשובים ולשלוף אותם מראש באופן אופטימלי.
אם הדף כן צריך JavaScript כדי לצרף פונקציונליות לחלקים מסוימים של ה-Markup בדף, עדיין תוכלו לעשות זאת באמצעות SSR, באמצעות JavaScript רגיל או באמצעות הידרציה כדי ליהנות מהיתרונות של שני העולמות.
איך עוזרים לסורק של טעינה מראש לעזור לכם
סורק ההטענה מראש הוא אופטימיזציה יעילה מאוד לדפדפן שעוזרת לדפים להיטען מהר יותר במהלך ההפעלה. הימנעות מדפוסים שמקשים על היכולת של ה-CDN לגלות משאבים חשובים מראש לא רק מקלה עליכם את הפיתוח, אלא גם יוצרת חוויות משתמש טובות יותר שמניבות תוצאות טובות יותר במדדים רבים, כולל חלק ממדדי הליבה לבדיקת חוויית המשתמש באתר.
לסיכום, אלה הדברים שחשוב לזכור מהפוסט הזה:
- סורק הטעינה מראש של הדפדפן הוא מנתח HTML משני שסורק לפני המנתח הראשי אם הוא חסום, כדי לזהות באופן אופורטוניסטי משאבים שהוא יכול לאחזר מוקדם יותר.
- סורקי ההטענה מראש לא יכולים לזהות משאבים שלא נמצאים בסימני ה-Markup שסופקו על ידי השרת בבקשת הניווט הראשונית. דרכים שבהן אפשר לעקוף את סורק ההטענה מראש כוללות, בין היתר:
- הזרקת משאבים ל-DOM באמצעות JavaScript, בין אם מדובר בסקריפטים, בתמונות, בסגנונות גיליון או בכל דבר אחר שעדיף להוסיף לעומס התעבורה הראשוני של ה-Markup מהשרת.
- טעינת פריטים לאט (lazy loading) של תמונות או תגי iframe מעל למסך באמצעות פתרון JavaScript.
- עיבוד רכיבי קוד בלקוח שעשויים להכיל הפניות למשאבי משנה של מסמכים באמצעות JavaScript.
- סורק ההטענה מראש סורק רק HTML. הוא לא בודק את התוכן של משאבים אחרים, במיוחד CSS, שעשויים לכלול הפניות לנכסים חשובים, כולל נכסים שעומדים בקריטריונים ל-LCP.
אם מסיבה כלשהי אי אפשר להימנע מתבנית שמשפיעה לרעה על היכולת של סורק ההטענה מראש לזרז את ביצועי הטעינה, כדאי להשתמש בהצעה לגבי המשאב rel=preload
. אם כן משתמשים ב-rel=preload
, כדאי לבדוק את התוצאות בכלים של Lab כדי לוודא שהן תואמות לציפיות. לבסוף, אל תטעינו מראש יותר מדי משאבים, כי אם תעדיפו את כולם, אף אחד מהם לא יקבל עדיפות.
משאבים
- 'סקריפטים אסינכרונים' שהוחדרו באמצעות סקריפט נחשבים מזיקים
- איך הטעינה מראש בדפדפן גורמת לדפים להיטען מהר יותר
- טעינה מראש של נכסים קריטיים כדי לשפר את מהירות הטעינה
- יצירת חיבורי רשת בשלב מוקדם כדי לשפר את מהירות הדף שחושבים שהיא
- אופטימיזציה של Largest Contentful Paint
התמונה הראשית (Hero) מ-Unsplash, מאת Mohammad Rahmani .