בשיעור ה-Lab הזה תוכלו לשפר את הביצועים של האפליקציה הפשוטה הזו, שמאפשרת למשתמשים לדרג חתולים אקראיים. למדו כיצד לבצע אופטימיזציה של חבילת JavaScript על ידי מזעור כמות הקוד שמשתנה.
באפליקציה לדוגמה, תוכלו לבחור מילה או אמוג'י כדי לציין עד כמה כל חתול אוהב. בלחיצה על לחצן, האפליקציה מציגה את ערך הלחצן מתחת לתמונת החתול הנוכחית.
מדידה
תמיד כדאי להתחיל בבדיקת האתר לפני הוספת אופטימיזציות:
- כדי לראות תצוגה מקדימה של האתר, מקישים על View App ואז על Fullscreen .
- לוחצים על 'Control+Shift+J' (או 'Command+Option+J' ב-Mac) כדי לפתוח את כלי הפיתוח.
- לוחצים על הכרטיסייה רשתות.
- מסמנים את התיבה Disable cache (השבתת המטמון).
- טוענים מחדש את האפליקציה.
באפליקציה הזו נעשה שימוש ביותר מ-80KB! הגיע הזמן לברר אם לא נעשה שימוש בחלקים מהחבילה:
הקש על
Control+Shift+P
(אוCommand+Shift+P
ב-Mac) כדי לפתוח את תפריט Command.מזינים
Show Coverage
ומקישים עלEnter
כדי להציג את הכרטיסייה כיסוי.בכרטיסייה כיסוי, לוחצים על טעינה מחדש כדי לטעון מחדש את האפליקציה בזמן יצירת הכיסוי.
כדי לראות את כמות הקוד שנעשה בו שימוש לעומת כמות הטעינה של החבילה הראשית:
יותר מחצי מהחבילה (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
ברמה הבסיסית (root) של הפרויקט.babel-loader
כולל את Babel בתהליך ה-build של חבילת האינטרנט.
עכשיו אפשר לראות את 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. אם אתם משתמשים ב-bundler אחר מאשר ב-Webpack, כאן מוסבר איך לכלול את Babel באפליקציה.
המאפיין targets
ב-.babelrc
מזהה לאילו דפדפנים מתבצע טירגוט. השדה @babel/preset-env
משולב עם רשימת הדפדפנים, כך שבמסמכי התיעוד של רשימת הדפדפנים אפשר למצוא רשימה מלאה של שאילתות תואמות שאפשר להשתמש בהן בשדה הזה.
הערך "last 2 versions"
מעביר את הקוד באפליקציה לשתי הגרסאות האחרונות של כל דפדפן.
ניפוי באגים
כדי לקבל תמונה מלאה של כל יעדי Babel בדפדפן, וגם של כל הטרנספורמציות והפוליפולים הכלולים, מוסיפים שדה debug
ל-.babelrc:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
}
]
]
}
- לוחצים על כלים.
- לוחצים על יומנים.
טוענים מחדש את האפליקציה ובודקים את יומני הסטטוס של תקלה בחלק התחתון של העורך.
דפדפנים מטורגטים
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 הספציפיים שכלולים:
על אף שחלק מהפולימלאים הנדרשים עבור "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%"
), לצד החרגה של דפדפנים ספציפיים שאתם בטוחים שהמשתמשים שלכם לא משתמשים בהם. למידע נוסף, מומלץ לקרוא את המאמר
"2 הגרסאות האחרונות" שנחשב למזיק
של ג'יימס קייל.
צריך להשתמש ב-<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.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"})
];
באופן דומה, יוצרים אובייקט config עבור סקריפט המודול שבהמשך שבו מוגדר 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 יכולים לשפר את הביצועים עוד יותר, כי הם מאפשרים לשלוח שתי גרסאות שונות של אפליקציה שמותאמת למכשיר. אם יש לכם הבנה טובה של האופן שבו שתי הטכניקות האלה יכולות לצמצם משמעותית את גודל החבילה, תוכלו להמשיך ולבצע אופטימיזציה.