בקודלאב הזה תלמדו לשפר את הביצועים של האפליקציה הפשוטה הזו, שמאפשרת למשתמשים לדרג חתולים אקראיים. איך מבצעים אופטימיזציה של חבילת ה-JavaScript על ידי צמצום כמות הקוד שעובר טרנספיליציה.
באפליקציית הדוגמה, אפשר לבחור מילה או אמוג'י כדי להביע את מידת החיבה לכל חתול. כשמקישים על לחצן, האפליקציה מציגה את הערך של הלחצן מתחת לתמונה הנוכחית של החתול.
מדידה
תמיד מומלץ להתחיל בבדיקה של האתר לפני שמוסיפים אופטימיזציות:
- כדי לראות תצוגה מקדימה של האתר, לוחצים על הצגת האפליקציה. לאחר מכן לוחצים על מסך מלא .
- מקישים על Control+Shift+J (או על Command+Option+J ב-Mac) כדי לפתוח את DevTools.
- לוחצים על הכרטיסייה רשתות.
- מסמנים את התיבה Disable cache (השבתת המטמון).
- טוענים מחדש את האפליקציה.
האפליקציה הזו משתמשת ביותר מ-80KB! עכשיו הגיע הזמן לבדוק אם יש חלקים בחבילה שלא בשימוש:
מקישים על
Control+Shift+P
(או עלCommand+Shift+P
ב-Mac) כדי לפתוח את התפריט Command.מזינים
Show Coverage
ומקישים עלEnter
כדי להציג את הכרטיסייה Coverage.בכרטיסייה Coverage, לוחצים על Reload כדי לטעון מחדש את האפליקציה בזמן תיעוד הכיסוי.
כדאי לבדוק כמה קוד נעשה בו שימוש לעומת כמה קוד נטען בחבילה הראשית:
יותר ממחצית החבילה (44KB) אפילו לא נמצאת בשימוש. הסיבה לכך היא שחלק גדול מהקוד מורכב מ-polyfills כדי להבטיח שהאפליקציה תפעל בדפדפנים ישנים יותר.
שימוש ב- @babel/preset-env
התחביר של שפת JavaScript תואם לתקן שנקרא ECMAScript או ECMA-262. גרסאות חדשות של המפרט מתפרסמות מדי שנה, והן כוללות תכונות חדשות שעברו את תהליך ההצעה. בכל דפדפן עיקרי יש תמיד שלב שונה של תמיכה בתכונות האלה.
האפליקציה משתמשת בתכונות הבאות של ES2015:
נעשה שימוש גם בתכונה הבאה של ES2017:
מומלץ לעיין בקוד המקור ב-src/index.js
כדי לראות איך משתמשים בכל זה.
כל התכונות האלה נתמכות בגרסה האחרונה של Chrome, אבל מה לגבי דפדפנים אחרים שלא תומכים בהן? Babel, שכלולה באפליקציה, היא הספרייה הפופולרית ביותר שמשמשת לקמפלור של קוד שמכיל תחביר חדש יותר לקוד שדפדפנים וסביבות ישנים יותר יכולים להבין. הוא עושה זאת בשתי דרכים:
- Polyfills כלולים כדי לדמות פונקציות חדשות יותר מ-ES2015, כך שאפשר להשתמש בממשקי ה-API שלהן גם אם הדפדפן לא תומך בהן. הנה דוגמה לpolyfill של השיטה
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 אחר?
המאפיין 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 לא מציג פוליפולים ספציפיים שבהם נעשה שימוש:
הסיבה לכך היא שכל @babel/polyfill
מיובא ישירות.
טעינת פוליפולים בנפרד
כברירת מחדל, Babel כולל את כל ה-polyfills הנדרשים לסביבה מלאה של ES2015 ואילך כש@babel/polyfill
מיובאים לקובץ. כדי לייבא פוליפולים ספציפיים שנדרשים לדפדפני היעד, מוסיפים 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";
עכשיו נכללים רק ה-polyfills הנדרשים לאפליקציה.
גודל חבילת האפליקציות מצטמצם באופן משמעותי.
צמצום רשימת הדפדפנים הנתמכים
מספר יעדי הדפדפנים שכלולים עדיין גדול למדי, ומספר המשתמשים שמשתמשים בדפדפנים שהוצאו משימוש, כמו 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">
עדיין יש מקום לשיפור. אמנם הסרנו כמה polyfills שלא בשימוש, אבל יש הרבה polyfills שנשלחים ולא נחוצים לדפדפנים מסוימים. השימוש במודולים מאפשר לכתוב תחביר חדש יותר ולשלוח אותו ישירות לדפדפנים, בלי להשתמש ב-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 כולל את כל הטרנספורמציות והפוליפילים הנדרשים כדי לטרגט כל דפדפן שעדיין לא תומך במודולים של 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, כלומר הקוד שמופלט למודול הזה הוא סקריפט קטן יותר ופחות מבוצע, שלא עובר טרנספורמציה בדוגמה הזו כי כל התכונות שבהן נעשה שימוש כבר נתמכות בדפדפנים שתומכים במודולים.
בסוף הקובץ, מייצאים את שתי ההגדרות במערך אחד.
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
כדי לספק רק את ה-polyfills הנדרשים לדפדפנים המטורגטים. אתם גם יודעים איך מודול JavaScript יכול לשפר את הביצועים עוד יותר על ידי שליחת שתי גרסאות שונות של אפליקציה שעברו תרגום. עכשיו, אחרי שהבנתם איך שתי השיטות האלה יכולות להקטין באופן משמעותי את נפח החבילה, תוכלו להתחיל לבצע אופטימיזציה.