יישומי האינטרנט של היום יכולים להיות גדולים למדי, במיוחד החלק של ה-JavaScript בהם. החל מאמצע 2018, גודל ההעברה החציוני של JavaScript במכשירים ניידים הוא כ-350KB בארכיון HTTP. וזה רק גודל ההעברה! בדרך כלל, JavaScript דחוס כשהוא נשלח ברשת. כלומר, הכמות בפועל של JavaScript הרבה יותר גבוהה אחרי שהדפדפן מבטל את הדחיסה שלו. חשוב לציין כי מבחינת עיבוד המשאבים, הדחיסה אינה רלוונטית. 900KB של JavaScript מפורק עדיין 900KB עבור המנתח והמהדר
JavaScript הוא משאב יקר לעיבוד. בשונה מתמונות, רק זמן הפענוח שלהן קצר יחסית, לאחר ההורדה, צריך לנתח, להדר את JavaScript ולבסוף להפעיל אותן. בייט של בייטים, גורם לכך ש-JavaScript יקר יותר מסוגים אחרים של משאבים.
אנחנו ממשיכים לבצע שיפורים כדי לשפר את היעילות של מנועי JavaScript, אבל שיפור הביצועים של JavaScript הוא כמו תמיד משימה למפתחים.
לשם כך, יש שיטות לשיפור ביצועי JavaScript. פיצול קוד היא שיטה אחת שמשפרת את הביצועים באמצעות חלוקה למחיצות של JavaScript של האפליקציה למקטעי נתונים, והצגת המקטעים האלה רק למסלולים של האפליקציה שצריכים אותם.
השיטה הזו אמנם פועלת, אבל לא מטפלת בבעיה נפוצה של אפליקציות שכוללות הרבה JavaScript, שהיא הכללת קוד שמעולם לא היה בשימוש. רעידת העצים מנסה לפתור את הבעיה.
מהי רעידת עצים?
רעידת עץ היא סוג של ביטול קוד מת. המונח פופולרי באוסף הערוצים, אבל המושג 'ביטול קוד מת' היה קיים כבר זמן מה. המונח כולל גם רכישה ב-webpack. ההדגמה מוצגת במאמר הזה באמצעות אפליקציה לדוגמה.
המונח "רעידת עץ" מגיע מהמודל התודעתי של האפליקציה ומיחסי התלות שלה כמבנה דמוי עץ. כל צומת בעץ מייצג תלות שמספקת פונקציונליות ייחודית לאפליקציה שלכם. באפליקציות מודרניות, יחסי התלות האלה מוזנים באמצעות הצהרות 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"
— שיכול להיות הרבה קוד) — בדוגמה הזו מייבאת רק חלקים ספציפיים ממנו. ב-builds של פיתוח, הפעולה הזו לא תשנה כלום, כי כל המודול מיובא ללא קשר. בגרסאות build של סביבת ייצור, אפשר להגדיר את ה-Webpack לביצוע 'רעידה' את הייצוא ממודולים של ES6 שלא יובאו באופן מפורש, וכתוצאה מכך קטנות יותר גרסאות ה-build האלה של סביבת הייצור. במדריך הזה תלמדו איך לעשות את זה.
איתור הזדמנויות לרעידת עץ
לצורך המחשה, יש אפליקציה לדוגמה שכוללת דף אחד שממחישה איך פועלת רעידת העצים. אתם יכולים לשכפל אותו ולעקוב אחריו אם תרצו, אבל נעבור על כל השלבים במסלול ביחד, כך שאין צורך בשכפול (אלא אם אתם מעוניינים ללמוד באופן מעשי).
האפליקציה לדוגמה היא מסד נתונים ניתן לחיפוש של פדלים לאפקטים של גיטרה. כשמזינים שאילתה, מופיעה רשימה של דוושות לאפקטים.
ההתנהגות שמובילה לאפליקציה הזו מופרדת לספק (כלומר, קודמים ורגש) וחבילות קוד ספציפיות לאפליקציה (או 'מקטעים', כמו ב-webpack)):
חבילות ה-JavaScript שמוצגות באיור שלמעלה הן גרסאות build לסביבת הייצור, כלומר מתבצעת אופטימיזציה שלהן באמצעות הטמעה. 21.1KB לחבילה ספציפית לאפליקציה אינו רע, אך חשוב לציין שאף פעם לא מתרחשת רעידת עצים. בואו נסתכל על קוד האפליקציה ונראה מה אפשר לעשות כדי לפתור את הבעיה.
בכל אפליקציה, איתור הזדמנויות לרעידת עצים כרוך בחיפוש הצהרות 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% בערך. האפשרות הזו מקצרת את זמן העיבוד של הסקריפט, אלא גם את זמן העיבוד שלו.
קדימה, מנערים כמה עצים!
כל מרחק שתנצלו מרעידת עצים תלוי באפליקציה, בתלויות ובארכיטקטורה של האפליקציה. נסה בעצמך! אם ידוע לך שעדיין לא הגדרת את חבילת ה-מודולים לביצוע האופטימיזציה הזו, אין טעם לנסות ולראות איך הוא מועיל לאפליקציה שלך.
יכול להיות שתבחינו בשיפור משמעותי בביצועים כתוצאה מרעידת עצים, או שלא יהיו ביצועים טובים בכלל. עם זאת, הגדרת מערכת ה-build שלך לנצל את האופטימיזציה הזו בגרסאות build לסביבת הייצור ולייבא באופן סלקטיבי רק את מה שהאפליקציה שלך צריכה, מעודדת שמירה על חבילות אפליקציות קטנות ככל האפשר.
תודה מיוחדת לכריסטופר בקסטר, ג'ייסון מילר, Addy Osmani, ג'ף פוסניק, סאם Saccone ופיליפ וולטון על המשוב החשוב שלהם, ששיפר באופן משמעותי את איכות המאמר הזה.