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

באפליקציה לדוגמה, אתם יכולים לבחור מילה או אמוג'י כדי להביע את מידת החיבה שלכם לכל חתול. כשלוחצים על כפתור, האפליקציה מציגה את הערך של הכפתור מתחת לתמונה הנוכחית של החתול.
מדידה
תמיד כדאי להתחיל בבדיקת האתר לפני שמוסיפים אופטימיזציות:
- כדי לראות תצוגה מקדימה של האתר, לוחצים על View App (הצגת האפליקציה) ואז על Fullscreen (מסך מלא)
.
- מקישים על `Control+Shift+J` (או על `Command+Option+J` ב-Mac) כדי לפתוח את כלי הפיתוח.
- לוחצים על הכרטיסייה רשת.
- מסמנים את התיבה השבתת המטמון.
- טוענים מחדש את האפליקציה.

האפליקציה הזו משתמשת ביותר מ-80KB. כדי לבדוק אם יש חלקים בחבילה שלא נמצאים בשימוש:
מקישים על
Control+Shift+P(או עלCommand+Shift+Pב-Mac) כדי לפתוח את התפריט פקודה.
מזינים
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, כלומר אפשר למצוא רשימה מלאה של שאילתות תואמות שאפשר להשתמש בהן בשדה הזה במסמכי התיעוד של browserlist.
הערך "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 מיובא ישירות.
טעינת polyfills בנפרד
כברירת מחדל, Babel כולל כל polyfill שנדרש לסביבת ES2015+ מלאה כשמייבאים את @babel/polyfill לקובץ. כדי לייבא polyfills ספציפיים שנדרשים לדפדפני היעד, מוסיפים useBuiltIns: 'entry' להגדרות האישיות.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
"useBuiltIns": "entry"
}
]
]
}
טוענים מחדש את האפליקציה. עכשיו אפשר לראות את כל ה-polyfills הספציפיים שכלולים:

למרות שרק ה-polyfills הדרושים ל-"last 2 versions" נכללים עכשיו, זו עדיין רשימה ארוכה מאוד. הסיבה לכך היא שעדיין נכללים פוליפילים שנדרשים לדפדפני היעד עבור כל תכונה חדשה יותר. כדי לכלול רק את התכונות שבהן משתמשים בקוד, משנים את הערך של המאפיין ל-usage.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true,
"useBuiltIns": "entry"
"useBuiltIns": "usage"
}
]
]
}
כך, פוליפילים נכללים באופן אוטומטי במקומות שבהם הם נדרשים.
זה אומר שאפשר להסיר את הייבוא של @babel/polyfill ב-src/index.js.
import "./style.css";
import "@babel/polyfill";
עכשיו נכללים רק ה-polyfills שנדרשים לאפליקציה.

גודל ה-App Bundle מצטמצם באופן משמעותי.

צמצום רשימת הדפדפנים הנתמכים
מספר יעדי הדפדפן שנכללים עדיין גדול מאוד, ואין הרבה משתמשים שמשתמשים בדפדפנים שהופסקו, כמו Internet Explorer. מעדכנים את ההגדרות:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"targets": [">0.25%", "not ie 11"],
"debug": true,
"useBuiltIns": "usage",
}
]
]
}
בודקים את הפרטים של החבילה שאוחזרה.

האפליקציה כל כך קטנה, שאין הבדל משמעותי עם השינויים האלה. עם זאת, הגישה המומלצת היא להשתמש באחוז נתח השוק של הדפדפן (למשל ">0.25%") ולהחריג דפדפנים ספציפיים שאתם בטוחים שהמשתמשים שלכם לא משתמשים בהם. כדי לקבל מידע נוסף, מומלץ לקרוא את המאמר "Last 2 versions" considered harmful של ג'יימס קייל.
שימוש ב-<script type="module">
עדיין יש מקום לשיפור. למרות שהוסרו כמה פוליפילים שלא היו בשימוש, יש הרבה פוליפילים שנשלחים ואינם נחוצים לדפדפנים מסוימים. באמצעות מודולים, אפשר לכתוב תחביר חדש יותר ולשלוח אותו ישירות לדפדפנים בלי להשתמש ב-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 כדי לשלוח שתי גרסאות שונות של האפליקציה לדפדפן:
- גרסה שתפעל בדפדפנים חדשים יותר שתומכים במודולים, וכוללת מודול שרובו לא עבר טרנספילציה אבל גודל הקובץ שלו קטן יותר
- גרסה שכוללת סקריפט גדול יותר שעבר טרנספילציה ויפעל בכל דפדפן מדור קודם
שימוש במודולים של 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, כלומר הקוד שמופק במודול הזה הוא סקריפט קטן יותר, שעבר פחות קומפילציה ולא עובר שום טרנספורמציה בדוגמה הזו, כי כל התכונות שנעשה בהן שימוש כבר נתמכות בדפדפנים שתומכים במודולים.
בסוף הקובץ, מייצאים את שתי ההגדרות במערך אחד.
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, לא תומך כרגע בפלט של הסקריפטים module ו-nomodule. יש פתרונות עקיפים ותוספים נפרדים שנוצרו כדי לפתור את הבעיה הזו, כמו BabelMultiTargetPlugin ו-HTMLWebpackMultiBuildPlugin, אבל במדריך הזה נשתמש בגישה פשוטה יותר של הוספת רכיב הסקריפט של המודול באופן ידני.
מוסיפים את הטקסט הבא לקובץ src/index.js בסוף הקובץ:
...
</form>
<script type="module" src="main.mjs"></script>
</body>
</html>
עכשיו טוענים את האפליקציה בדפדפן שתומך במודולים, כמו הגרסה האחרונה של Chrome.

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

סיכום
עכשיו אתם יודעים איך להשתמש ב-@babel/preset-env כדי לספק רק את ה-polyfills שנדרשים לדפדפנים מטורגטים. אתם גם יודעים איך מודולים של JavaScript יכולים לשפר את הביצועים עוד יותר על ידי שליחת שתי גרסאות שונות של אפליקציה שעברה טרנספילציה. עכשיו, אחרי שהבנתם איך שתי השיטות האלה יכולות להקטין את גודל החבילה באופן משמעותי, אתם יכולים להתחיל לבצע אופטימיזציה.