גלו מהו סורק הטעינה מראש של הדפדפן, איך הוא עוזר לביצועים ואיך תוכלו לא להפריע לו.
אחד מההיבט שאתם לא להתעלם ממנו בעת אופטימיזציה של מהירות דף הוא להכיר קצת את הרכיבים הפנימיים של הדפדפן. דפדפנים מבצעים אופטימיזציות מסוימות כדי לשפר את הביצועים בדרכים שאנחנו כמו המפתחים לא יכולים לעשות – אבל רק כל עוד ביצוע האופטימיזציות האלה לא אסור בכוונה.
אחת האופטימיזציה הפנימית של הדפדפן שכדאי להבין היא סורק הטעינה מראש של הדפדפן. במאמר הזה נסביר איך סורק ההטענה מראש פועל, ומה חשוב יותר, איך אפשר להימנע מלהפריע לו.
מהו סורק טעינה מראש?
לכל דפדפן יש מנתח HTML ראשי שמחלק את התגים לסמלים של תגי עיצוב גולמיים ומעבד אותם למודל אובייקטים. התהליך הזה נמשך עד שהמנתח מושהה כשהוא מוצא משאב חסימה, כמו גיליון סגנונות שנטען עם אלמנט <link>
או סקריפט שנטען עם אלמנט <script>
ללא מאפיין async
או defer
.
במקרה של קובצי CSS, העיבוד נחסם כדי למנוע הבהוב של תוכן ללא עיצוב (FOUC). כלומר, אפשר לראות לזמן קצר גרסה לא מעוצבת של דף לפני שמחילים עליו סגנונות.
הדפדפן גם חוסם ניתוח ורינדור של הדף כשהוא נתקל ברכיבי <script>
ללא המאפיין defer
או async
.
הסיבה לכך היא שהדפדפן לא יכול לדעת בוודאות אם סקריפט מסוים ישנה את ה-DOM בזמן שמנתח ה-HTML הראשי עדיין מבצע את פעולתו. זו הסיבה לכך שבדרך כלל מקובל לטעון את ה-JavaScript בסוף המסמך, כדי שההשפעות של חסימת הניתוח והעיבוד יהפכו לשוליים.
אלה סיבות טובות לכך שהדפדפן צריך לחסום גם את הניתוח וגם את העיבוד. עם זאת, חסימת כל אחד מהשלבים החשובים האלה היא לא רצויה, כי הם עלולים לעצור את ההצגה על ידי עיכוב בגילוי של משאבים חשובים אחרים. למרבה המזל, הדפדפנים עושים כמיטב יכולתם כדי לטפל בבעיות האלה באמצעות מנתח HTML משני שנקרא סורק טעינה מראש.
התפקיד של סורק טעינה מראש הוא ספקולטיבי. כלומר, הוא בודק תגי עיצוב גולמיים כדי למצוא משאבים לאחזור מדי פעם לפני שמנתח ה-HTML הראשי יגלה אותם בדרך אחרת.
איך יודעים מתי סורק ההטענה מראש פועל
הסורק של הטעינה מראש קיים כי יש חסימה של העיבוד והניתוח. אם שתי בעיות הביצועים האלה לא היו קיימות אף פעם, סורק ההטענה מראש לא היה שימושי במיוחד. המפתח לבירור אם סורק הטעינה מראש מועיל לדף אינטרנט מסוים תלוי בתופעות החסימה האלה. כדי לעשות זאת, אפשר ליצור עיכוב מלאכותי של בקשות כדי לגלות איפה פועל סורק הטעינה מראש.
כדוגמה, צלם את הדף הזה שמכיל טקסט ותמונות בסיסיים באמצעות גיליון סגנונות. מכיוון שקובצי CSS חוסמים גם את העיבוד וגם את הניתוח, יוצרת עיכוב מלאכותי של שתי שניות בגיליון הסגנון באמצעות שירות proxy. עם העיכוב הזה, קל יותר לראות ב-Waterfall של הרשת איפה פועל סורק הטעינה מראש.
כמו שאפשר לראות ב-Waterfall, סורק הטעינה מראש מגלה את הרכיב <img>
גם כשהעיבוד וניתוח המסמך חסומים. בלי האופטימיזציה הזו, הדפדפן לא יכול לאחזר דברים באופן זמני במהלך תקופת החסימה, ובקשות משאבים רבות יותר יהיו עוקבות ולא בו-זמניות.
בהמשך לדוגמה של הצעצוע, נבחן כמה דפוסים בעולם האמיתי שבהם אפשר להביס את סורק הטעינה מראש ומה אפשר לעשות כדי לתקן אותם.
סקריפטים מושתלים של async
נניח שיש ב-<head>
שלכם HTML שמכיל קוד 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. כשגוללים את התמונה לאזור התצוגה, הטוען העצלני מסיר את הקידומת 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. ניתן להטביע גיליונות סגנונות ברכיבי <style>
, בסקריפטים ברכיבי <script>
ובכל משאב אחר באמצעות קידוד base64.
הטמעת משאבים עשויה להיות מהירה יותר מאשר ההורדה שלהם, מפני שלא מונפקת בקשה נפרדת למשאב. הוא מופיע ממש במסמך ונטען מיד. עם זאת, יש חסרונות משמעותיים:
- אם אתם לא שומרים במטמון את קוד ה-HTML, ואתם לא יכולים לעשות זאת אם תגובת ה-HTML היא דינמית, המשאבים שבשורה לא נשמרים במטמון אף פעם. האפשרות הזו משפיעה על הביצועים כי אי אפשר לעשות שימוש חוזר במשאבים שבגוף ההודעה.
- גם אם אפשר לשמור HTML במטמון, משאבים בגוף ההודעה לא ישותפו בין מסמכים. כך פוחתת היעילות של שמירת הקבצים במטמון בהשוואה לקבצים חיצוניים שאפשר לשמור במטמון ולהשתמש בהם מחדש במקור כולו.
- אם תוסיפו יותר מדי תוכן בקוד, הסריקה של טעינת הנתונים מראש תתעכב בזיהוי המשאבים בהמשך המסמך, כי הורדת התוכן הנוסף שמוטמע בקוד אורכת יותר זמן.
ניקח לדוגמה את הדף הזה. בתנאים מסוימים, אלמנט ה-LCP הוא התמונה בחלק העליון של הדף, וה-CSS נמצא בקובץ נפרד שנטען על ידי רכיב <link>
. הדף משתמש גם בארבעה גופנים באינטרנט המבוקשים כקבצים נפרדים מהמשאב של שירות ה-CSS.
מה קורה אם ה-CSS וכל הגופנים מוגדרים כמשאבי base64?
ההשפעה של ההטבעה מובילה להשלכות שליליות על LCP בדוגמה הזו, ועל הביצועים באופן כללי. בגרסת הדף שבה לא מוטמעים פריטים, תמונת ה-LCP מוצגת תוך כ-3.5 שניות. בדף שמטמיע את כל הטקסט, לא מתבצעת צביעה של תמונת ה-LCP עד שהיא נמשכת רק יותר מ-7 שניות.
יש כאן עוד יתרונות מלבד סורק הטעינה מראש. הטמעת גופנים בקוד היא לא אסטרטגיה מומלצת, כי base64 הוא פורמט לא יעיל למשאבים בינאריים. גורם נוסף חשוב הוא שלא מתבצעת הורדה של משאבי גופנים חיצוניים, אלא אם נקבע שהם נחוצים על ידי ה-CSSOM. כשהגופנים האלה מסומנים כ-base64, תתבצע הורדה שלהם גם אם הם נחוצים לדף הנוכחי וגם אם לא.
האם טעינה מראש יכולה לשפר את המצב? ודאי. אתם יכולים לטעון מראש את תמונת ה-LCP ולצמצם את זמן ה-LCP, אבל להגדלת נפח ה-HTML שעשוי להיות בלתי ניתן לשמירה באמצעות משאבים מוטבעים יש השלכות שליליות אחרות על הביצועים. הדפוס הזה משפיע גם על First Contentful Paint (FCP). בגרסת הדף שבה שום דבר לא משולב, אורך ה-FCP הוא בערך 2.7 שניות. בגרסה שבה כל המידע הוא בכתב, ערך FCP הוא בערך 5.8 שניות.
חשוב מאוד להיזהר כשמזינים דברים בתוך HTML, במיוחד משאבים בקידוד base64. באופן כללי, לא מומלץ לעשות זאת, למעט במקרים של משאבים קטנים מאוד. רצוי כמה שפחות מדי כניסות, כי הטמעה ארוכה מדי עלולה לגרום לדליקה.
עיבוד תגי עיצוב באמצעות JavaScript בצד הלקוח
אין ספק: JavaScript בהחלט משפיע על מהירות הדף. לא רק שהמפתחים מסתמכים על היכולות האלה כדי לספק אינטראקטיביות, אלא גם יש אנשים נוטים להסתמך על התכונה הזו כדי להציג את התוכן עצמו. כך אפשר לשפר את חוויית המפתח בדרכים מסוימות. אבל היתרונות למפתחים לא תמיד מובילים ליתרונות למשתמשים.
דפוס אחד שיכול להביס את סורק הטעינה מראש הוא רינדור תגי עיצוב באמצעות JavaScript בצד הלקוח:
כאשר מטענים ייעודיים (payloads) של תגי עיצוב נמצאים בתוך JavaScript ומעובדים באופן מלא על ידי JavaScript, המשאבים בתגי העיצוב האלו אינם גלויים בפועל לסורק הטעינה מראש. הפעולה הזו מעכבת את הגילוי של משאבים חשובים, וזה בהחלט משפיע על LCP. במקרה של הדוגמאות האלה, הבקשה לתמונה מסוג LCP מתעכבת משמעותית בהשוואה לחוויה המקבילה שמעובדת על ידי שרת שבה לא נדרש JavaScript כדי להופיע.
זה סותר קצת את הנושא של מאמר זה, אבל ההשפעות של עיבוד תגי העיצוב על הלקוח הרבה מעבר להבסת סורק הטעינה מראש. קודם כול, שימוש ב-JavaScript כדי ליהנות מחוויה שלא מחייבת זמן עיבוד מיותר, עלול להשפיע על האינטראקציה (Interaction to Next Paint) (INP). אם מעבדים כמויות גדולות מאוד של תגי עיצוב בלקוח, יש סיכוי גבוה יותר שייווצר משימות ארוכות בהשוואה לאותה כמות של תגי עיצוב שנשלחת על ידי השרת. הסיבה לכך – מלבד העיבוד הנוסף שכרוך ב-JavaScript – היא שדפדפנים מעבירים סטרימינג של רכיבי קוד מהשרת ומחלקים את העיבוד כך שמשימות ארוכות מוגבלות. לעומת זאת, תגי עיצוב שמעובדים על ידי לקוח מטופלים כמשימה מונוליתית אחת, שעלולה להשפיע על ה-INP של דף.
הפתרון לתרחיש הזה תלוי בתשובה לשאלה הזו: האם יש סיבה לכך שהשרת לא יכול לספק את תגי העיצוב של הדף, ולא להיות מוצג אצל הלקוח? אם התשובה היא 'לא', יש להביא בחשבון את העיבוד בצד השרת (SSR) או את הסימון שנוצר באופן סטטי ככל האפשר, מכיוון שהדבר יעזור לסורק הטעינה מראש לגלות ולאחזר משאבים חשובים מראש מדי פעם.
אם הדף כן צריך JavaScript כדי לצרף פונקציונליות לחלקים מסוימים של ה-Markup בדף, עדיין תוכלו לעשות זאת באמצעות SSR, באמצעות JavaScript רגיל או באמצעות הידרציה כדי ליהנות מהיתרונות של שני העולמות.
עוזרים לסורק הטעינה מראש לעזור
סורק הטעינה מראש הוא אופטימיזציה יעילה במיוחד של הדפדפן, שעוזרת לדפים להיטען מהר יותר במהלך ההפעלה. הימנעות מדפוסים שעלולים לבטל את היכולת של המערכת לגלות משאבים חשובים מראש, לא רק הופכים את תהליך הפיתוח לפשוט יותר, אלא גם משפרים את חוויות המשתמש שיניבו תוצאות טובות יותר במדדים רבים, כולל חלק מתפקוד האפליקציה.
לסיכום, הנה כמה דברים שכדאי להסיר מהפוסט הזה:
- סורק הטעינה מראש של הדפדפן הוא מנתח HTML משני שסורק לפני הסורק הראשי אם הוא חסום כדי לגלות מדי פעם משאבים שניתן לאחזר מוקדם יותר.
- סורקי ההטענה מראש לא יכולים לזהות משאבים שלא נמצאים בסימני ה-Markup שסופקו על ידי השרת בבקשת הניווט הראשונית. דרכים שבהן ניתן להביס את סורק הטעינה מראש כוללות (בין היתר):
- החדרת משאבים אל ה-DOM באמצעות JavaScript, בין אם מדובר בסקריפטים, בתמונות, בגיליונות סגנונות או בכל דבר אחר שמתאים יותר למטען הייעודי (payload) הראשוני של תגי העיצוב מהשרת.
- טעינה מדורגת של תמונות או iframes בחלק העליון והקבוע, באמצעות פתרון של JavaScript.
- עיבוד תגי עיצוב בלקוח שעשויים להכיל הפניות למשאבי משנה של מסמכים באמצעות JavaScript.
- סורק הטעינה מראש סורק רק HTML. הוא לא בודק את התוכן של משאבים אחרים, במיוחד CSS, שעשויים לכלול הפניות לנכסים חשובים, כולל נכסים שעומדים בקריטריונים ל-LCP.
אם מסיבה כלשהי אי אפשר להימנע מדפוס שמשפיע לרעה על היכולת של סורק הטעינה מראש לזרז את ביצועי הטעינה, כדאי להשתמש ברמז למשאב rel=preload
. אם אתם כן משתמשים ב-rel=preload
, כדאי לבדוק בכלים לשיעור ה-Lab כדי לוודא שהוא מניב את האפקט הרצוי. לבסוף, אל תטעינו מראש יותר מדי משאבים, כי אם תתנו עדיפות לכל דבר, לא תהיה עדיפות לאף דבר.
משאבים
- "סקריפטים אסינכרוניים" שהוחדרו לסקריפט נחשב למזיק
- איך הטוען מראש של הדפדפן גורם לטעינה מהירה יותר של דפים
- טעינה מראש של נכסים קריטיים כדי לשפר את מהירות הטעינה
- יצירת חיבורי רשת בשלב מוקדם כדי לשפר את מהירות הדף שחושבים שהיא
- אופטימיזציה של Largest Contentful Paint (LCP)
תמונה ראשית (Hero) מ-Unbounce, מאת Mohammad Rahmani