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

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

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

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

מדידה

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

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

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

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

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

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

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

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

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

    מה נכלל בקוד של החבילה

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

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

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

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

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

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

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

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

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

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

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

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

הערך "last 2 versions" מעביר את הקוד באפליקציה לשתי הגרסאות האחרונות של כל דפדפן.

ניפוי באגים

כדי לקבל תמונה מלאה של כל יעדי Babel של הדפדפן וגם את כל הטרנספורמציות וה-polyfills הכלולים, מוסיפים את השדה debug ל-.babelrc:.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • לוחצים על כלים.
  • לוחצים על יומנים.

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

דפדפנים מטורגטים

Babel רושם במסוף מספר פרטים על תהליך הידור, כולל כל סביבות היעד שעבורן הקוד עבר הידור.

דפדפנים מטורגטים

שימו לב איך דפדפנים שהוצאו משימוש, כמו Internet Explorer, כלולים ברשימה הזו. זו בעיה כי לא יתווספו פיצ'רים חדשים יותר לדפדפנים לא נתמכים, ו-Babel ממשיך להעביר עבורם תחביר ספציפי. הפעולה הזו מגדילה שלא לצורך את החבילה, אם המשתמשים לא משתמשים בדפדפן הזה כדי לגשת לאתר.

Babel רושמת גם רשימה של יישומי פלאגין לטרנספורמציה שנעשה בהם שימוש:

רשימת יישומי הפלאגין שנמצאים בשימוש

זו רשימה די ארוכה! אלה כל יישומי הפלאגין שבהם Babel צריך להשתמש כדי להמיר כל תחביר ES2015+ לתחביר ישן יותר של כל הדפדפנים המטורגטים.

עם זאת, ב-Babel לא מוצגים פוליגונים ספציפיים שנמצאים בשימוש:

לא נוספו מילוי פוליגונים

הסיבה לכך היא שייבוא ישיר של כל @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", היא עדיין רשימה ארוכה מאוד! הסיבה לכך היא ש-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";

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

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

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

גודל החבילה הוקטן ל-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%") ולהחריג דפדפנים ספציפיים שאתם בטוחים שהמשתמשים שלכם לא משתמשים בהם. למידע נוסף כדאי לקרוא את המאמר '2 הגרסאות האחרונות' שנחשבות למזיקות מאת ג'יימס קייל.

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

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

שימוש במודולי ES באמצעות Babel

כדי ליצור הגדרות @babel/preset-env נפרדות לשתי הגרסאות של האפליקציה, מסירים את הקובץ .babelrc. אפשר להוסיף את הגדרות Babel לתצורת Webpack על ידי ציון שני פורמטים שונים של הידור (compilation) לכל גרסה של האפליקציה.

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

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

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

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

סיכום

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