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

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

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

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

מדידה

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

  1. כדי לראות תצוגה מקדימה של האתר, לוחצים על הצגת האפליקציה. לאחר מכן לוחצים על מסך מלא מסך מלא.
  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 כדי להציג את הכרטיסייה Coverage.

  3. בכרטיסייה Cover לוחצים על Reload כדי לטעון מחדש את האפליקציה תוך כדי צילום הכיסוי.

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

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

    רמת הכיסוי של הקוד בחבילה

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

לא נוספו polyfills

הסיבה לכך היא שכל @babel/polyfill מיובא באופן ישיר.

טעינת פוליפולים בנפרד

כברירת מחדל, כשמייבאים את @babel/polyfill לקובץ ב-Babel, נדרשים כל polyfill שנדרש לסביבת ES2015+ מלאה. כדי לייבא שדות פוליגונים ספציפיים שנדרשים לדפדפני היעד, צריך להוסיף 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";

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

רשימה של מילויי פוליגונים נכללים באופן אוטומטי

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

גודל החבילה הוקטן ל-30.1KB

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

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

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

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

גודל החבילה הוא 30.0KB

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

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

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

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

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

סיכום

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