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

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

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

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

מדידה

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

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

בקשה לגודל מקורי של חבילת אפליקציות

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

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

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

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

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

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

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

יותר ממחצית החבילה (44KB) לא מנוצלת. הסיבה לכך היא שחלק גדול מהקוד מורכב מ-polyfills כדי לוודא שהאפליקציה פועלת בדפדפנים ישנים יותר.

שימוש ב-‎ @babel/preset-env

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

התכונות הבאות של ES2015 נמצאות בשימוש באפליקציה:

נעשה גם שימוש בתכונה הבאה של ES2017:

אתם מוזמנים לעיין בקוד המקור ב-src/index.js כדי לראות איך כל זה עובד.

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

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

כדי לראות איך הוא נכלל, אפשר לעיין בקובץ ההגדרות של Babel, ‏ .babelrc:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

זוהי הגדרה של Babel ו-webpack. אם אתם משתמשים בחבילת מודולים שונה מ-webpack, תוכלו לקרוא איך לכלול את Babel באפליקציה שלכם.

המאפיין 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

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

טעינת polyfills בנפרד

כברירת מחדל, Babel כולל כל polyfill שנדרש לסביבת ES2015+‎ מלאה כשמייבאים את @babel/polyfill לקובץ. כדי לייבא פוליפילים ספציפיים שנדרשים לדפדפני היעד, מוסיפים useBuiltIns: 'entry' להגדרה.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

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

רשימה של polyfills שיובאו

למרות שרק הפוליפילים שנדרשים ל-"last 2 versions" נכללים עכשיו, הרשימה עדיין ארוכה מאוד. הסיבה לכך היא שעדיין נכללים polyfills שנדרשים לדפדפני היעד עבור כל תכונה חדשה יותר. משנים את הערך של המאפיין ל-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 שנדרשים לאפליקציה.

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

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

גודל ה-Bundle ירד ל-30.1KB

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

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

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

כדאי לעיין בפרטים של החבילה שאוחזרה.

גודל ה-Bundle הוא 30.0KB

האפליקציה קטנה מאוד, ולכן השינויים האלה לא משפיעים עליה באופן משמעותי. עם זאת, הגישה המומלצת היא להשתמש באחוז נתח השוק של הדפדפן (למשל ">0.25%") ולהחריג דפדפנים ספציפיים שאתם בטוחים שהמשתמשים שלכם לא משתמשים בהם. כדי לקבל מידע נוסף על הנושא, אפשר לקרוא את המאמר "Last 2 versions" considered harmful של ג'יימס קייל.

משתמשים בתג <script type="module">

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

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

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

מוסיפים את הטקסט הבא לקובץ src/index.js בסוף הקובץ:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

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

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

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

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

סקריפט של 30KB נטען בדפדפנים ישנים

סיכום

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