הצגת קוד מודרני בדפדפנים מודרניים לטעינת דפים מהירה יותר

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

צילום מסך של האפליקציה

באפליקציה לדוגמה, תוכלו לבחור מילה או אמוג'י כדי לציין עד כמה כל חתול אוהב. בלחיצה על לחצן, האפליקציה מציגה את ערך הלחצן מתחת לתמונת החתול הנוכחית.

מדידה

תמיד כדאי להתחיל בבדיקת האתר לפני הוספת אופטימיזציות:

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

בקשה לגודל החבילה המקורי

באפליקציה הזו נעשה שימוש ביותר מ-80KB! הגיע הזמן לברר אם לא נעשה שימוש בחלקים מהחבילה:

  1. הקש על Control+Shift+P (או Command+Shift+P ב-Mac) כדי לפתוח את תפריט Command. תפריט הפקודות

  2. מזינים Show Coverage ומקישים על Enter כדי להציג את הכרטיסייה כיסוי.

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

    טעינה מחדש של האפליקציה עם כיסוי קוד

  4. כדי לראות את כמות הקוד שנעשה בו שימוש לעומת כמות הטעינה של החבילה הראשית:

    כיסוי הקוד של החבילה

יותר מחצי מהחבילה (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 ספציפיים שבהם נעשה שימוש:

לא נוספו 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"
      }
    ]
  ]
}

כך, polyfills נכללים באופן אוטומטי בעת הצורך. המשמעות היא שאפשר להסיר את הייבוא של @babel/polyfill בsrc/index.js.

import "./style.css";
import "@babel/polyfill";

עכשיו נכללים רק המשתנים הנדרשים עבור האפליקציה.

רשימת polyfills נכללת באופן אוטומטי

גודל חבילת האפליקציה הופחת באופן משמעותי.

גודל החבילה הופחת ל-30.1KB

צמצום רשימת הדפדפנים הנתמכים

מספר יעדי הדפדפן שכלולים עדיין גדול למדי, ואין הרבה משתמשים שמשתמשים בדפדפנים הופסקו כמו Internet Explorer. מעדכנים את ההגדרות להגדרות הבאות:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

יש לבדוק את הפרטים של החבילה שאוחזרה.

גודל החבילה: 30KB

מכיוון שהאפליקציה כל כך קטנה, אין הרבה הבדל בין השינויים האלה. עם זאת, הגישה המומלצת היא להשתמש באחוז נתח שוק של דפדפנים (כמו ">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.

מודול בגודל 5.2KB אוחזר דרך הרשת לדפדפנים חדשים יותר

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

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

סקריפט בגודל 30KB אוחזר עבור דפדפנים ישנים יותר

סיכום

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