יישומי האינטרנט של היום יכולים להיות די גדולים, במיוחד החלק של ה-JavaScript שלהן. החל מאמצע שנת 2018, גודל החציון של ההעברה של JavaScript במכשירים ניידים עומד על כ-350KB. וזהו גודל ההעברה לרוב, JavaScript נדחס כשהוא נשלח דרך הרשת. כלומר, הכמות בפועל של JavaScript הרבה יותר גדולה אחרי שהדפדפן מבטל אותו. חשוב לציין את זה, כי הדחיסה לא רלוונטית מבחינת עיבוד המשאבים. 900KB של JavaScript מפוקח עדיין צריכים להיות 900KB למנתח ולמהדר, למרות שהוא עשוי להיות בערך 300KB כשהוא דחוס.
עיבוד JavaScript הוא משאב יקר. בניגוד לתמונות שצוברות זמן פענוח טריוויאלי יחסית רק לאחר ההורדה, יש לנתח, להדר ולבסוף לבצע את ה-JavaScript. בייט לבייט, דבר שגורם ל-JavaScript להיות יקר יותר מסוגי משאבים אחרים.
אנחנו ממשיכים לבצע שיפורים כדי לשפר את היעילות של מנועי JavaScript, אבל שיפור הביצועים של JavaScript הוא כמו תמיד משימה למפתחים.
לשם כך יש שיטות לשיפור הביצועים של JavaScript. פיצול קוד היא שיטה כזו שמשפרת את הביצועים על ידי חלוקה של ה-JavaScript של האפליקציה למקטעים, והצגת המקטעים האלה רק לנתיבים של אפליקציה שזקוקים להם.
השיטה הזו פועלת, אבל היא לא מטפלת בבעיה נפוצה של אפליקציות שמגוונות מאוד ב-JavaScript, כלומר הוספה של קוד שמעולם לא נעשה בו שימוש. ניעור עצים מנסה לפתור את הבעיה.
מהי רעידת עצים?
ניעור עצים הוא סוג של ביטול קוד מת. המונח הפך לפופולרי על ידי סיכום, אבל המושג 'מחיקת קודים מתים' קיים כבר לא מעט זמן. המושג הזה מצא גם אפשרות רכישה בחבילת אינטרנט, ומודגמת במאמר הזה באמצעות אפליקציה לדוגמה.
המונח 'ניעור עצים' מגיע מהמודל המנטלי של האפליקציה שלכם ומיחסי התלות שלה כמבנה דמוי עץ. כל צומת בעץ מייצג תלות שמספקת פונקציונליות ייחודית לאפליקציה שלכם. באפליקציות מודרניות, יחסי התלות האלה מוגשים באמצעות הצהרות import
סטטיות, כך:
// Import all the array utilities!
import arrayUtils from "array-utils";
כאשר אפליקציה היא צעירה - אם רוצים, ייתכן שיהיו לה מעט יחסי תלות. הוא גם משתמש ברוב יחסי התלות שהוספת, אם לא בכולן. עם זאת, ככל שהאפליקציה תתבגר, תגדל יחסי תלות נוספים. כדי להרכיב עניינים, יחסי תלות ישנים יוצאים משימוש, אבל יכול להיות שהם לא ייקטעו מ-codebase. התוצאה הסופית היא שכמות גדולה של JavaScript שאינה בשימוש של אפליקציה מסוימת תגיע ליעדה. כדי למנוע מצב כזה, אפשר לנצל את האופן שבו הצהרות import
סטטיות מושכות חלקים מסוימים במודולים של ES6:
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
ההבדל בין הדוגמה הזו ב-import
לבין הדוגמה הקודמת הוא שבמקום לייבא כל דבר מהמודול "array-utils"
(שיכול להיות הרבה קוד) - דוגמה זו מייבאת רק חלקים ספציפיים ממנו. בגרסאות build של פיתוח, זה לא ישנה דבר, מאחר שהייבוא כולו של המודול מתבצע ללא קשר. בגרסאות build של סביבת ייצור, ניתן להגדיר את Webpack "לנער" ייצוא ממודולים של ES6 שלא יובאו באופן מפורש, וכך להקטין את גרסאות ה-build האלה של סביבת הייצור. במדריך הזה תלמדו איך לעשות את זה!
מציאת הזדמנויות לניעור עץ
להמחשה, יש אפליקציה לדוגמה באורך דף אחד שמדגימה איך פועל רעידת העצים. אפשר לשכפל אותו ולעקוב אחריו אם רוצים, אבל במדריך הזה נעבור על כל שלב בתהליך, כך שאין צורך לבצע שכפול (אלא אם אתם לומדים באופן מעשי).
האפליקציה לדוגמה היא מסד נתונים שניתן לבצע בו חיפוש של פדלים לאפקטים לגיטרה. כשמזינים שאילתה, תופיע רשימה של פדלים.
ההתנהגות שמובילה לאפליקציה הזו מופרדת לספק (כלומר, Preact ו-Emotion (רגש) וחבילות קודים ספציפיות לאפליקציה (או 'chunks', כפי שקראנו אותן ב-Webpack):
חבילות ה-JavaScript שמוצגות בתרשים שלמעלה הן גרסאות build לייצור, כלומר הן עוברות אופטימיזציה באמצעות רפליקציה. 21.1 KB לחבילה ספציפית לאפליקציה אינו רע, אבל חשוב לזכור שלא מתרחשת רעידת עצים בכלל. בואו נסתכל על קוד האפליקציה ונראה איך אפשר לפתור את הבעיה.
בכל אפליקציה, איתור הזדמנויות לניעור עצים יהיה כרוך בחיפוש הצהרות import
סטטיות. סמוך לחלק העליון של קובץ הרכיב הראשי, תופיע שורה כזו:
import * as utils from "../../utils/utils";
אפשר לייבא מודולים של ES6 במגוון דרכים, אבל מודולים כאלה אמורים למשוך את תשומת הלב של המשתמשים. בשורה הספציפית הזו כתוב "import
כל דבר מהמודול utils
, ושמים אותו במרחב שמות שנקרא utils
." השאלה הגדולה שצריך לשאול כאן היא "כמה דברים יש במודול הזה?"
אם תסתכלו בקוד המקור של המודול utils
, תראו שיש כ-1,300 שורות קוד.
האם אתם צריכים את כל הדברים האלה? כדי לבדוק זאת, מחפשים את קובץ הרכיב הראשי שמייבא את המודול utils
כדי לראות כמה מופעים של מרחב השמות הזה מופיעים.
כפי שהתברר, מרחב השמות של utils
מופיע בשלושה מקומות בלבד ביישום שלנו - אך לאילו פונקציות? אם תסתכלו שוב בקובץ הרכיב הראשי, נראה שמדובר בפונקציה אחת בלבד, utils.simpleSort
, שמשמשת למיון רשימת תוצאות החיפוש לפי מספר קריטריונים בזמן שינוי התפריטים הנפתחים למיון:
if (this.state.sortBy === "model") {
// `simpleSort` gets used here...
json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
// ..and here...
json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
// ..and here.
json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}
מתוך קובץ של 1,300 שורות שמכיל הרבה פעולות ייצוא, נעשה שימוש רק באחת מהן. כתוצאה מכך, יש הרבה קובצי JavaScript שאינם בשימוש.
האפליקציה לדוגמה הזו קצת תמציתית, אבל היא לא משנה את העובדה שתרחיש מהסוג הסינתטי הזה דומה להזדמנויות אופטימיזציה אמיתיות שאתם עשויים להיתקל בהן באפליקציית אינטרנט לייצור. עכשיו, אחרי שזיהיתם הזדמנות לרעידת עצים כמועילה, איך זה נעשה בפועל?
מניעת היכולת של Babel לשנות מודולים של ES6 למודולים של CommonJS
Babel הוא כלי חיוני, אבל הוא עלול להקשות על הצפייה בהשפעות של רעידת עצים. אם אתם משתמשים ב-@babel/preset-env
, Babel עשוי לשנות מודולים של ES6 למודולים של CommonJS עם תאימות רחבה יותר – כלומר, מודולים שאתם require
במקום import
.
מכיוון שקשה יותר לבצע ניעור עצים במודולים של CommonJS, ל-webpack לא תהיה אפשרות לדעת מה צריך לקצץ מחבילות אם תחליטו להשתמש בהן. הפתרון הוא להגדיר את @babel/preset-env
כך שישאיר באופן מפורש את המודולים של ES6 ללא שינוי. בכל מקום שבו מגדירים את Babel, בין אם זה ב-babel.config.js
או ב-package.json
, צריך להוסיף עוד משהו קטן:
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
ציון modules: false
בהגדרה של @babel/preset-env
יגרום ל-Babel לפעול באופן הרצוי, וכך לאפשר ל-Webpack לנתח את עץ התלות ולהימנע מיחסי תלות שאינם בשימוש.
חשוב לזכור את תופעות הלוואי
היבט נוסף שצריך לקחת בחשבון כשמנסים לנער את יחסי התלות מהאפליקציה הוא לבדוק אם למודולים של הפרויקט יש תופעות לוואי. דוגמה לתופעת לוואי היא כאשר פונקציה משנה משהו מחוץ להיקף שלה, וזה תופעת לוואי של הביצוע שלה:
let fruits = ["apple", "orange", "pear"];
console.log(fruits); // (3) ["apple", "orange", "pear"]
const addFruit = function(fruit) {
fruits.push(fruit);
};
addFruit("kiwi");
console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]
בדוגמה הזו, הפונקציה addFruit
יוצרת תופעת לוואי כאשר היא משנה את המערך fruits
, שנמצא מחוץ להיקף שלו.
תופעות לוואי חלות גם על מודולים של ES6, וזה חשוב בהקשר של רעידת עצים. מודולים שמקבלים קלט צפוי ומפיקים פלטים צפויים באותה מידה בלי לשנות שום דבר מחוץ להיקף שלהם הם יחסי תלות שניתן לשחרר באופן בטוח אם לא משתמשים בהם. אלו קטעי קוד מודולריים שעומדים בפני עצמם. כלומר, 'מודולים'.
כשמדובר ב-Webpack, אפשר להשתמש ברמז כדי לציין שחבילה ויחסי התלות שלה ללא תופעות לוואי על ידי ציון "sideEffects": false
בקובץ package.json
של הפרויקט:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": false
}
לחלופין, אפשר לציין ל-webpack אילו קבצים ספציפיים אינם ללא תופעות לוואי:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
בדוגמה האחרונה, כל קובץ שלא צוין ייחשב ללא תופעות לוואי. אם לא רוצים להוסיף אותו לקובץ package.json
, אפשר לציין את הסימון הזה גם בהגדרת ה-webpack דרך module.rules
.
ייבוא של מה שצריך בלבד
אחרי שמורים ל-Babel להשאיר את המודולים של ES6 ללא שינוי, צריך לבצע שינוי קל בתחביר import
כדי לייבא רק את הפונקציות שדרושות מהמודול utils
. בדוגמה של המדריך הזה, כל מה שנחוץ הוא הפונקציה simpleSort
:
import { simpleSort } from "../../utils/utils";
מכיוון שרק simpleSort
מיובא במקום כל המודול utils
, יהיה צורך לשנות כל מופע של utils.simpleSort
ל-simpleSort
:
if (this.state.sortBy === "model") {
json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
json = simpleSort(json, "type", this.state.sortOrder);
} else {
json = simpleSort(json, "manufacturer", this.state.sortOrder);
}
בדוגמה הזו, ניעור העצים צריך להיות כל מה שצריך. זהו הפלט של ה-webpack לפני ניעור עץ התלות:
Asset Size Chunks Chunk Names
js/vendors.16262743.js 37.1 KiB 0 [emitted] vendors
js/main.797ebb8b.js 20.8 KiB 1 [emitted] main
זה הפלט אחרי רעידת העצים בהצלחה:
Asset Size Chunks Chunk Names
js/vendors.45ce9b64.js 36.9 KiB 0 [emitted] vendors
js/main.559652be.js 8.46 KiB 1 [emitted] main
שתי החבילות כווצו, אבל למעשה זו החבילה של main
שהכי מועילה. כשמשביתים את החלקים שלא נמצאים בשימוש במודול utils
, החבילה של main
מתכווצת ב-60%. אפשרות זו לא רק מקצרת את משך הזמן שנדרש לסקריפט, אלא גם את זמן העיבוד.
קדימה, עודדו כמה עצים!
מרחק הנסיעה המצטבר של רעידות העצים תלוי באפליקציה, בתלות ובארכיטקטורה שלה. רוצה לנסות? אם אתם יודעים שעדיין לא הגדרתם את ה-bundler של המודול לביצוע אופטימיזציה זו, אין בעיה לנסות ולראות איך זה מועיל לאפליקציה שלכם.
ייתכן שתבחינו בשיפור משמעותי בביצועים כתוצאה מרעידת עצים, או בהיעדר משמעותי. עם זאת, הגדרת מערכת ה-build שלך כדי לנצל את האופטימיזציה של האופטימיזציה של גרסאות build לייצור ולייבא באופן סלקטיבי רק את מה שהאפליקציה שלך צריכה, תוביל לכך שחבילות האפליקציות שלך יהיו קטנות ככל האפשר.
תודה מיוחדת לכריסטופר בקסטר, ג'ייסון מילר, אדי אוסמאני, ג'ף פוסניק, סם סקקוני ופיליפ וולטון על המשוב המועיל שלהם, שעזר לנו לשפר באופן משמעותי את איכות המאמר.