ב-Codelab הזה, משפרים את הביצועים של האפליקציה הפשוטה, שמאפשרת למשתמשים לדרג חתולים אקראיים. איך מבצעים אופטימיזציה של חבילת ה-JavaScript על ידי צמצום כמות הקוד שמומר?
באפליקציית הדוגמה, אפשר לבחור מילה או אמוג'י כדי להביע את מידת החיבה לכל חתול. כשמקישים על לחצן, האפליקציה מציגה את הערך של הלחצן מתחת לתמונה הנוכחית של החתול.
מדידה
תמיד כדאי להתחיל בבדיקה של האתר לפני שמוסיפים אופטימיזציה:
- כדי לראות תצוגה מקדימה של האתר, לוחצים על הצגת האפליקציה. לאחר מכן לוחצים על מסך מלא .
- מקישים על 'Control+Shift+J' (או על 'Command+Option+J' ב-Mac) כדי לפתוח את כלי הפיתוח.
- לוחצים על הכרטיסייה רשתות.
- מסמנים את התיבה Disable cache (השבתת המטמון).
- טוענים מחדש את האפליקציה.
האפליקציה הזו משתמשת ביותר מ-80KB! זמן כדי לבדוק אם לא נעשה שימוש בחלקים מהחבילה:
מקישים על
Control+Shift+P
(או עלCommand+Shift+P
ב-Mac) כדי לפתוח את התפריט Command.מזינים
Show Coverage
ומקישים עלEnter
כדי להציג את הכרטיסייה Coverage.בכרטיסייה Cover לוחצים על Reload כדי לטעון מחדש את האפליקציה תוך כדי צילום הכיסוי.
כדאי לבדוק כמה קוד נעשה בו שימוש לעומת כמה קוד נטען בחבילה הראשית:
יותר ממחצית מהחבילה (44KB) לא מנוצלת אפילו. הסיבה לכך היא שחלק גדול מהקודים ב-polyfills כדי לוודא שהאפליקציה פועלת בדפדפנים ישנים יותר.
שימוש ב- @babel/preset-env
התחביר של שפת JavaScript תואם לתקן שנקרא ECMAScript או ECMA-262. גרסאות חדשות של המפרט מתפרסמות מדי שנה, והן כוללות תכונות חדשות שעברו את תהליך ההצעה. כל דפדפן ראשי נמצא תמיד בשלב שונה של תמיכה בתכונות האלה.
האפליקציה משתמשת בתכונות הבאות של ES2015:
בנוסף, נעשה שימוש בתכונה הבאה של ES2017:
אתם מוזמנים לצלול לעומק קוד המקור ב-src/index.js
כדי לראות איך משתמשים בכל הדברים האלה.
כל התכונות האלה נתמכות בגרסה האחרונה של Chrome, אבל מה קורה בדפדפנים אחרים שלא תומכים בהן? Babel, שכלולה באפליקציה, היא הספרייה הפופולרית ביותר שמשמשת לקמפלור של קוד שמכיל תחביר חדש יותר לקוד שסביבות וגם דפדפנים ישנים יותר יכולים להבין. הוא עושה זאת בשתי דרכים:
- Polyfills כלולים כדי לדמות פונקציות חדשות יותר מ-ES2015, כך שאפשר להשתמש בממשקי ה-API שלהן גם אם הדפדפן לא תומך בהן. הדוגמה הבאה היא ל-polyfill של ה-method
Array.includes
. - פלאגינים משמשים להמרת קוד ES2015 (או גרסה מתקדמת יותר) לתחביר ES5 ישן יותר. מכיוון שמדובר בשינויים שקשורים לתחביר (כמו פונקציות חץ), אי אפשר לדמות אותם באמצעות polyfills.
אפשר לעיין ב-package.json
כדי לראות אילו ספריות של Babel נכללות:
"dependencies": {
"@babel/polyfill": "^7.0.0"
},
"devDependencies": {
//...
"babel-loader": "^8.0.2",
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
//...
}
@babel/core
הוא המהדר המרכזי של Babel. כך, כל הגדרות Babel מוגדרות בקובץ.babelrc
ברמה הבסיסית של הפרויקט.babel-loader
כולל את Babel בתהליך ה-build של webpack.
עכשיו הסתכלו על webpack.config.js
כדי לראות איך babel-loader
נכלל ככלל:
module: { rules: [ //... { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ] },
@babel/polyfill
מספק את כל ה-polyfills הנדרשים לכל התכונות החדשות יותר של ECMAScript, כדי שיוכלו לפעול בסביבות שלא תומכות בהן. הנתונים כבר מיובאים בחלק העליון שלsrc/index.js.
import "./style.css";
import "@babel/polyfill";
@babel/preset-env
מזהה אילו טרנספורמציות ו-polyfills נדרשים לכל הדפדפנים או הסביבות שנבחרו כיעדים.
אפשר לעיין בקובץ ההגדרות של Babel, .babelrc
, כדי לראות איך הוא נכלל:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions"
}
]
]
}
זוהי הגדרה של Babel ו-webpack. כך כוללים את Babel באפליקציה אם אתם משתמשים ב-webpack ולא ב-Webpack Bundler.
המאפיין targets
ב-.babelrc
מזהה את הדפדפנים שאליהם מוצגת הטירגוט. @babel/preset-env
משתלב עם browserslist, כך שאפשר למצוא רשימה מלאה של שאילתות תואמות שאפשר להשתמש בהן בשדה הזה במסמכי התיעוד של browserslist.
הערך "last 2 versions"
מעביר את הקוד באפליקציה לשתי הגרסאות האחרונות של כל דפדפן.
ניפוי באגים
כדי לקבל תמונה מלאה של כל היעדים של Babel בדפדפן, וגם של כל הטרנספורמציות והפוליפילים הכלולים, מוסיפים שדה debug
ל-.babelrc:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
}
]
]
}
- לוחצים על כלים.
- לוחצים על יומנים.
צריך לטעון מחדש את האפליקציה ולבדוק את יומני הסטטוס של Glitch בתחתית העורכת.
דפדפנים מטורגטים
Babel מתעד במסוף מספר פרטים על תהליך הידור הקוד, כולל כל סביבות היעד שהקוד עבר בהן הידור.
שימו לב שדפדפנים שהוצאו משימוש, כמו Internet Explorer, כלולים ברשימה הזו. זו בעיה כי לא יתווספו תכונות חדשות לדפדפנים שלא נתמכים, ו-Babel ימשיך להמיר תחביר ספציפי בשבילם. הפעולה הזו מגדילה את גודל החבילה ללא צורך, אם המשתמשים לא משתמשים בדפדפן הזה כדי לגשת לאתר.
ב-Babel מתועדת גם רשימה של יישומי הפלאגין לטרנספורמציה שבהם נעשה שימוש:
זו רשימה ארוכה למדי! אלה כל הפלאגינים שבהם Babel צריך להשתמש כדי להמיר כל תחביר מ-ES2015 ואילך לתחביר ישן יותר לכל הדפדפנים המטורגטים.
עם זאת, Babel לא מציג polyfills ספציפיים שבהם נעשה שימוש:
הסיבה לכך היא שכל @babel/polyfill
מיובא באופן ישיר.
טעינת פוליפולים בנפרד
כברירת מחדל, כשמייבאים את @babel/polyfill
לקובץ ב-Babel, נדרשים כל polyfill שנדרש לסביבת ES2015+ מלאה. כדי לייבא שדות פוליגונים ספציפיים שנדרשים לדפדפני היעד, צריך להוסיף useBuiltIns: 'entry'
להגדרות.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
"useBuiltIns": "entry"
}
]
]
}
טוענים מחדש את האפליקציה. עכשיו אפשר לראות את כל הפוליפילים הספציפיים שכלולים:
עכשיו נכללים רק פוליפולים נחוצים ל-"last 2 versions"
, אבל עדיין מדובר ברשימה ארוכה מאוד. הסיבה לכך היא שעדיין נכללים פוליפילים שנדרשים לדפדפני היעד עבור כל התכונות החדשות יותר. משנים את ערך המאפיין ל-usage
כך שיכלול רק את התכונות שמשמשות את הקוד.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true,
"useBuiltIns": "entry"
"useBuiltIns": "usage"
}
]
]
}
כך, ה-polyfills נכללים באופן אוטומטי במקומות הנדרשים.
כלומר, אפשר להסיר את הייבוא של @babel/polyfill
ב-src/index.js.
import "./style.css";
import "@babel/polyfill";
עכשיו, נכללים רק הפרטים הממלאים הנדרשים לאפליקציה.
גודל חבילת האפליקציה הצטמצם באופן משמעותי.
צמצום רשימת הדפדפנים הנתמכים
מספר יעדי הדפדפנים שכלולים עדיין גדול למדי, ומספר המשתמשים שמשתמשים בדפדפנים שהוצאו משימוש, כמו Internet Explorer, הוא קטן. מעדכנים את ההגדרות כך:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"targets": [">0.25%", "not ie 11"],
"debug": true,
"useBuiltIns": "usage",
}
]
]
}
בודקים את הפרטים של החבילה שאוחזרה.
מכיוון שהאפליקציה קטנה מאוד, אין הבדל משמעותי בין השינויים האלה. עם זאת, מומלץ להשתמש באחוז של נתח שוק בדפדפן (כמו ">0.25%"
) ולהחריג דפדפנים ספציפיים שאתם בטוחים שהמשתמשים שלכם לא משתמשים בהם. למידע נוסף, כדאי לקרוא את המאמר של James Kyle בנושא 'Last 2 versions' נחשבים מזיקים.
צריך להשתמש ב- <script type="Module">
עדיין יש מקום לשיפור. למרות שהוסרו מספר שדות polyfill שלא נמצאים בשימוש, יש רבים שנשלחים שלא נדרשים בחלק מהדפדפנים. השימוש במודולים מאפשר לכתוב תחביר חדש יותר ולשלוח אותו ישירות לדפדפנים, בלי להשתמש ב-polyfills מיותרים.
מודולים של JavaScript הם תכונה חדשה יחסית שנתמכת בכל הדפדפנים העיקריים.
אפשר ליצור מודולים באמצעות מאפיין type="module"
כדי להגדיר סקריפטים שייבאו וייצאו ממודולים אחרים. לדוגמה:
// math.mjs
export const add = (x, y) => x + y;
<!-- index.html -->
<script type="module">
import { add } from './math.mjs';
add(5, 2); // 7
</script>
תכונות רבות יותר של ECMAScript כבר נתמכות בסביבות שתומכות במודולים של JavaScript (במקום צריך ב-Babel). המשמעות היא שאפשר לשנות את קובץ התצורה של Babel כדי לשלוח לדפדפן שתי גרסאות שונות של האפליקציה:
- גרסה שתעבוד בדפדפנים חדשים יותר שתומכים במודולים, וכוללת מודול שלא עבר טרנספיילציה במידה רבה אבל בגודל קובץ קטן יותר
- גרסה שכוללת סקריפט גדול יותר שעובר תרגום (transpilation) ויכול לפעול בכל דפדפן מדור קודם
שימוש במודולי ES באמצעות Babel
כדי ליצור הגדרות @babel/preset-env
נפרדות לשתי הגרסאות של האפליקציה, מסירים את הקובץ .babelrc
. כדי להוסיף הגדרות של Babel להגדרות של webpack, מציינים שני פורמטים שונים של הידור לכל גרסה של האפליקציה.
מתחילים בהוספת הגדרה של הסקריפט הקודם אל webpack.config.js
:
const legacyConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].bundle.js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: false
}
}]
]
}
},
cssRule
]
},
plugins
}
שימו לב שבמקום להשתמש בערך targets
עבור "@babel/preset-env"
, המערכת משתמשת ב-esmodules
עם הערך false
. כלומר, Babel כולל את כל הטרנספורמציות וה-polyfills הנדרשים כדי לטרגט כל דפדפן שעדיין לא תומך במודולים של ES.
מוסיפים את האובייקטים entry
, cssRule
ו-corePlugins
לתחילת הקובץ webpack.config.js
. כל אלה משותפים בין המודול לבין הסקריפטים הקודמים שמוצגים בדפדפן.
const entry = {
main: "./src"
};
const cssRule = {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
};
const plugins = [
new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
new HtmlWebpackPlugin({template: "./src/index.html"})
];
באופן דומה, יוצרים אובייקט תצורה לסקריפט המודול הבא, שבו מוגדר legacyConfig
:
const moduleConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].mjs"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: true
}
}]
]
}
},
cssRule
]
},
plugins
}
ההבדל העיקרי הוא שבשם הקובץ של הפלט נעשה שימוש בסיומת הקובץ .mjs
. הערך של esmodules
מוגדר כאן כ-True, כלומר הקוד שמופק למודול הוא סקריפט קטן יותר שעבר הידור (compile) ולא עובר טרנספורמציה כלשהי בדוגמה הזו, כי כל התכונות שמשתמשים בהן כבר נתמכות בדפדפנים שתומכים במודולים.
בקצה הקובץ, מייצאים את שתי התצורות במערך אחד.
module.exports = [
legacyConfig, moduleConfig
];
עכשיו המערכת יוצרת מודול קטן יותר לדפדפנים שתומכים בו, וסקריפט גדול יותר שעובר תרגום לדפדפנים ישנים יותר.
בדפדפנים שתומכים במודולים, סקריפטים עם המאפיין nomodule
מתעלמים.
לעומת זאת, דפדפנים שלא תומכים במודולים מתעלמים מרכיבי סקריפט עם הערך type="module"
. כלומר, אפשר לכלול מודול וגם חלופה מתומצתת. באופן אידיאלי, שתי הגרסאות של האפליקציה צריכות להיות ב-index.html
ככה:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>
בדפדפנים שתומכים במודולים, מתבצע אחזור והפעלה של main.mjs
והתעלמות מ-main.bundle.js.
. בדפדפנים שלא תומכים במודולים, מתבצע הפוך.
חשוב לציין שבניגוד לסקריפטים רגילים, סקריפטים של מודולים תמיד מושהים כברירת מחדל.
אם רוצים לדחות גם את הסקריפט המקביל של nomodule
ולהריץ אותו רק אחרי הניתוח, צריך להוסיף את המאפיין defer
:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>
השלב האחרון הוא להוסיף את המאפיינים module
ו-nomodule
למודול ולסקריפט הקודם, בהתאמה, ולייבא את ScriptExtHtmlWebpackPlugin בחלק העליון של webpack.config.js
:
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
עכשיו צריך לעדכן את המערך plugins
בהגדרות כך שיכלול את הפלאגין הזה:
const plugins = [ new ExtractTextPlugin({filename: "[name].css", allChunks: true}), new HtmlWebpackPlugin({template: "./src/index.html"}), new ScriptExtHtmlWebpackPlugin({ module: /\.mjs$/, custom: [ { test: /\.js$/, attribute: 'nomodule', value: '' }, ] }) ];
הגדרות הפלאגין האלה מוסיפות מאפיין type="module"
לכל רכיבי הסקריפט .mjs
, וגם מאפיין nomodule
לכל המודולים של הסקריפט .js
.
הצגת מודולים במסמך ה-HTML
השלב האחרון הוא להפיק את רכיבי הסקריפט הקודמים והמודרניים לקובץ ה-HTML. לצערנו, הפלאגין שיוצר את קובץ ה-HTML הסופי, HTMLWebpackPlugin
, לא תומך כרגע בפלט של הסקריפטים של המודול ושל nomodule. למרות שיש פתרונות אפשריים ויישומי פלאגין נפרדים שנוצרו כדי לפתור את הבעיה הזו, כמו BabelMultiTargetPlugin ו-HTMLWebpackMultiBuildPlugin, המטרה של המדריך הזה היא להשתמש בגישה פשוטה יותר להוספה ידנית של רכיב סקריפט המודול.
מוסיפים את הטקסט הבא לקובץ src/index.js
בסוף הקובץ:
...
</form>
<script type="module" src="main.mjs"></script>
</body>
</html>
עכשיו אפשר לטעון את האפליקציה בדפדפן שתומך במודולים, כמו הגרסה האחרונה של Chrome.
רק המודול מאוחזר, עם גודל חבילה קטן בהרבה כי הוא לא עבר טרנספיילציה במידה רבה. הדפדפן מתעלם לחלוטין מהאלמנט השני של הסקריפט.
אם תטעינו את האפליקציה בדפדפן ישן יותר, יוחזרו רק הסקריפט הגדול יותר שעבר טרנספיילציה עם כל הפונקציות החסרות (polyfills) והטרנספורמציות הנדרשות. זהו צילומי מסך של כל הבקשות שנשלחו בגרסה ישנה יותר של Chrome (גרסה 38).
סיכום
עכשיו ברור לכם איך להשתמש ב-@babel/preset-env
כדי לספק רק את ה-polyfill שנדרש לדפדפנים המטורגטים. אתם גם יודעים איך מודולים של JavaScript יכולים לשפר את הביצועים עוד יותר על ידי שליחת שתי גרסאות שונות של אפליקציה שעברו תרגום. עכשיו, אחרי שהבנתם איך שתי השיטות האלה יכולות להקטין באופן משמעותי את נפח החבילה, תוכלו להתחיל לבצע אופטימיזציה.