בואו לגלות מהו סורק הטעינה מראש של הדפדפן, איך הוא עוזר לביצועים ואיך לא לחרוג ממנו.
כשמדובר באופטימיזציה של מהירות דף, אחד מההיבטים שלא מפספסים הוא חשוב להכיר קצת את הממשק הפנימי של הדפדפן. דפדפנים מבצעים אופטימיזציות מסוימות לשיפור הביצועים בדרכים שאנחנו, כמפתחים, לא יכולים - אבל רק כל עוד האופטימיזציות האלה לא סותרות בטעות.
אחת משיטות האופטימיזציה הפנימיות של הדפדפן שצריך להבין היא סורק הטעינה מראש של הדפדפן. פוסט זה יסביר כיצד פועל סורק הטעינה מראש - וחשוב מכך, כיצד תוכל להימנע משיבושים.
מהו סורק טעינה מראש?
לכל דפדפן יש מנתח 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.
הפתרון הוא לשנות את סימון התמונה:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
זהו התבנית האופטימלית לתמונות שנמצאות באזור התצוגה במהלך ההפעלה, מאחר שסורק הטעינה מראש יגלה ויאחזר את משאב התמונה מהר יותר.
התוצאה בדוגמה הפשוטה הזו היא שיפור של 100 אלפיות השנייה ב-LCP בחיבור איטי. אולי זה לא נראה כמו שיפור משמעותי, אבל כאשר לוקחים את הפתרון הוא תיקון מהיר של סימון, ושרוב דפי האינטרנט מורכבים יותר מקבוצת הדוגמאות הזו. המשמעות היא שייתכן שמועמדי LCP יצטרכו להתמודד עם רוחב הפס בהרבה משאבים אחרים, לכן החשיבות של אופטימיזציות כאלה הולכת וגוברת.
תמונות רקע של CSS
חשוב לזכור שסורק הטעינה מראש של הדפדפן סורק תגי עיצוב. המערכת לא סורקת סוגי משאבים אחרים, כמו CSS, שעשויים לכלול אחזורים של תמונות שהנכס background-image
מפנה אליהן.
בדומה ל-HTML, דפדפנים מעבדים CSS לתוך מודל אובייקט משלו, שנקרא CSSOM. אם מתגלה משאבים חיצוניים כשה-CSSOM נבנה, המשאבים האלה נדרשים בזמן הגילוי ולא על ידי סורק הטעינה מראש.
נניח שהמועמד של LLCP בדף שלכם הוא רכיב עם מאפיין 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, אבל גם הצגת תוכן ראשוני (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 - היא שדפדפנים משדרים תגי עיצוב מהשרתים ומפצלים את העיבוד כך שלא יתבצע משימות ארוכות. מצד שני, תגי עיצוב שמעובדים על ידי הלקוח מטופלות כמשימה מונוליתית יחידה, שעשויה להשפיע על מדדי תגובה בדף, כמו זמן חסימה כולל (TBT) או עיכוב בקלט ראשון (FID), בנוסף ל-INP.
הפתרון לתרחיש הזה תלוי בתשובה לשאלה הזו: האם יש סיבה לכך שהשרת לא יכול לספק את תגי העיצוב של הדף במקום להציג אותו אצל הלקוח? אם התשובה היא "לא", יש לשקול את העיבוד בצד השרת (SSR) או תגי עיצוב שנוצרו באופן סטטי כאשר הדבר אפשרי, מכיוון שהם יעזרו לסורק הטעינה מראש לגלות משאבים חשובים ולאחזר אותם מראש באופן אוטומטי.
אם הדף שלכם כן צריך JavaScript כדי לצרף פונקציונליות לחלקים מסוימים מתגי העיצוב של הדף, עדיין תוכלו לעשות זאת באמצעות SSR, באמצעות JavaScript של וניל או הידרציה כדי להפיק את המיטב משני העולמות.
עזרה לסורק הטעינה מראש לעזור לך
סורק הטעינה מראש הוא אופטימיזציה יעילה במיוחד של הדפדפן שעוזרת לטעון דפים מהר יותר בזמן ההפעלה. הימנעות מדפוסים שפוגעים ביכולתה לגלות משאבים חשובים מראש, לא רק תקל על הפיתוח שלך אלא גם תיצור חוויות משתמש טובות יותר שיניבו תוצאות טובות יותר במדדים רבים, כולל חלק מתפקודי האתר.
לסיכום, הנה הדברים הבאים שכדאי להסיר מהפוסט הזה:
- סורק הטעינה מראש של הדפדפן הוא מנתח HTML משני שסורק לפני הסריקה הראשי אם הוא חסום כדי לאתר באופן אקראי משאבים שהוא יכול לאחזר מוקדם יותר.
- סורק הטעינה מראש לא יכול לגלות משאבים שלא נמצאים בתגי העיצוב שסופקו על ידי השרת בבקשת הניווט הראשונית. דרכים להבסת סורק הטעינה מראש עשויות לכלול, בין היתר:
- הזרקת משאבים ל-DOM באמצעות JavaScript, בין אם מדובר בסקריפטים, בתמונות, בגיליון סגנונות או בכל דבר אחר שעדיף להשתמש בו במטען הייעודי (payload) הראשוני של תגי העיצוב מהשרת.
- טעינה מדורגת של תמונות או iframes בחלק העליון והקבוע באמצעות פתרון JavaScript.
- רינדור תגי עיצוב בלקוח שעשויים להכיל הפניות למשאבי משנה של מסמכים באמצעות JavaScript.
- סורק הטעינה מראש סורק רק HTML. הוא לא בוחן את התוכן של משאבים אחרים — במיוחד CSS — שעשויים לכלול הפניות לנכסים חשובים, כולל מועמדי LCP.
אם מסיבה כלשהי לא ניתן להימנע מדפוס שמשפיע לרעה על היכולת של סורק הטעינה מראש להאיץ את ביצועי הטעינה, כדאי להשתמש ברמז למשאב rel=preload
. אם אתם משתמשים ב-rel=preload
, כדאי לבצע בדיקות בכלים לשיעור ה-Lab כדי לוודא שהוא מניב את האפקט הרצוי. לבסוף, אל תטענו מראש יותר מדי משאבים, כי כשאתם מתעדפים את הכול, שום דבר לא יקרה.
משאבים
- "סקריפטים אסינכרוניים" שהוחדרו לסקריפט נחשבים למזיקים
- איך הטוען מראש של הדפדפן גורם לטעינה מהירה יותר של דפים
- טעינה מראש של נכסים קריטיים לשיפור מהירות הטעינה
- יצירת חיבורי רשת בשלב מוקדם כדי לשפר את מהירות הדפים הנתפסת
- אופטימיזציה של Largest Contentful Paint (LCP)
תמונה ראשית (Hero) מתוך UnFlood, מאת מוחמד רחמני .