שיפור מהירות העמוד

איך לצמצם את זמן הביצוע של JavaScript ולפצל משימות ארוכות

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

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

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

מקליטים את הפעולה האיטית לפני שמשנים את החבילה

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

מסמנים את הזמן שבו הממשק אינו מגיב. בודקים את מסלול Main, את Bottom-up למציאת פונקציות בעלות זמן עצמי גבוה ואת Call tree להבנת הקוראים שלהן. זמן עצמי שייך לפונקציה; זמן כולל כולל קריאות פנימיות. מעטפת קצרה עם זמן כולל גבוה עשויה להוביל למקור הבעיה, בלי להיות הפונקציה שצריך לשכתב. מפת מקור מקשרת בין החבילה לבין המודול שבתחזוקה.

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

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

מפרידים בין הסרה, דחייה ותזמון

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

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

ממצא שינוי ראשון לבדיקה מה אינו מובטח
קוד לא נחוץ במסלול הראשוני הסרה או חבילה נפרדת שימוש ראשון מהיר יותר ביכולת
אתחול חוזר או מאזינים כפולים ביטול העבודה החוזרת פחות בתים ברשת
חישוב ארוך שאינו תלוי ב-DOM worker או אלגוריתם משופר פחות זמן מעבד כולל
רצף הכרחי חוסם משוב חזותי ויתור על זמן הריצה בין שלבים מוגבלים סיום מהיר יותר של כל הרצף
קוד גורם לחישובי סגנון או פריסה כבדים פחות שינויי DOM ותלויות פריסה פתרון באמצעות מאפייני script בלבד

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

שולחים רק את הקוד הדרוש למסך הנוכחי

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

Tree shaking מסיר ייצואים לא מנוצלים שאפשר לזהות סטטית, כאשר כלי הבנייה תומך בכך. מודולי ES והצהרות מדויקות על תופעות לוואי מסייעים. אין לסמן את כל החבילות כנטולות תופעות לוואי כדי להקטין פלט. התקנת polyfill, רישום רכיב או ייבוא CSS חיוני עשויים להידרש גם בלי שימוש בייצוא. בודקים את פלט הייצור, לא רק את משפט ה-import.

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

טוענים יכולת לפי דרישה עם import דינמי

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

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

html
<!-- index.html -->
<button id="preview" type="button">Preview export</button>
<p id="status" role="status"></p>
<pre id="output"></pre>
<script type="module" src="/main.js"></script>

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

js
// main.js
const button = document.querySelector('#preview');
const status = document.querySelector('#status');
const output = document.querySelector('#output');
button.addEventListener('click', async () => {
  button.disabled = true;
  status.textContent = 'Loading preview';
  try {
    const { renderPreview } = await import('./preview.js');
    renderPreview(output);
    status.textContent = 'Preview ready';
  } catch {
    status.textContent = 'Preview unavailable. Try again.';
  } finally {
    button.disabled = false;
  }
});

המודול הנפרד מספק את היכולת האופציונלית.

js
// preview.js
export function renderPreview(element) {
  element.textContent = JSON.stringify({ format: 'csv', ready: true }, null, 2);
}

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

נותנים לדפדפן הזדמנות ממשית להגיב

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

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

js
function yieldToBrowser() {
  if (globalThis.scheduler?.yield) return globalThis.scheduler.yield();
  return new Promise(resolve => setTimeout(resolve, 0));
}

export async function normalizeRows(rows, batchSize = 100) {
  if (!Number.isInteger(batchSize) || batchSize < 1) {
    throw new RangeError('batchSize must be a positive integer');
  }
  const normalized = [];
  for (let start = 0; start < rows.length; start += batchSize) {
    const end = Math.min(start + batchSize, rows.length);
    for (let index = start; index < end; index++) {
      normalized.push(String(rows[index]).trim());
    }
    if (end < rows.length) await yieldToBrowser();
  }
  return normalized;
}

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

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

מעבירים חישובים עצמאיים ל-worker

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

ה-worker הבא מדגים את הגבול; הנתונים קטנים בכוונה ואינם מצביעים על רווח ביצועים.

js
// totals.worker.js
self.addEventListener('message', event => {
  const total = event.data.reduce((sum, value) => sum + value, 0);
  self.postMessage(total);
});

הקוד הקורא מקבל את התוצאה בשרשור הראשי ומסיים את ה-worker לאחר מכן.

js
// totals.js, loaded as a module
const worker = new Worker(new URL('./totals.worker.js', import.meta.url), {
  type: 'module'
});
worker.addEventListener('message', event => {
  console.log('Total:', event.data);
  worker.terminate();
});
worker.addEventListener('error', () => worker.terminate());
worker.postMessage([2, 4, 6]);

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

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

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

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

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

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

עוקבים אחרי עבודה חוזרת וציור מיותר

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

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

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

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

מאמתים באותו מסלול שימוש

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

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

מדריכים קשורים