פרסום, שליחה והתקנה של JavaScript מודרני לאפליקציות מהירות יותר

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

ג'ייסון מילר
ג'ייסון מילר

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

JavaScript מודרני

JavaScript מודרני לא מאופיין כקוד שנכתב בגרסת מפרט ספציפית של ECMAScript, אלא בתחביר שנתמך על ידי כל הדפדפנים המודרניים. דפדפני אינטרנט מודרניים כמו Chrome, Edge, Firefox ו-Safari מהווים יותר מ-90% משוק הדפדפנים. בנוסף, דפדפנים שונים שמסתמכים על אותם מנועי עיבוד מהווים 5% נוספים. כלומר, 95% מהתנועה הגלובלית באינטרנט מגיעה מדפדפנים שתומכים בתכונות הכי נפוצות של שפת JavaScript מ-10 השנים האחרונות, כולל:

  • Classes (ES2015)
  • פונקציות חיצים (ES2015)
  • גנרטורים (ES2015)
  • חסימת היקף (ES2015)
  • Destructring (ES2015)
  • פרמטרים של מנוחה ופיזור (ES2015)
  • קיצור של אובייקט (ES2015)
  • Async/await (ES2017)

בדרך כלל, בתכונות בגרסאות חדשות יותר של מפרט השפה יש תמיכה פחות עקבית בדפדפנים מודרניים. לדוגמה, תכונות רבות של ES2020 ו-ES2021 נתמכות רק ב-70% משוק הדפדפנים. עדיין רוב הדפדפנים, אבל לא מספיק שאפשר להסתמך על התכונות האלה ישירות. כלומר, על אף ש-JavaScript "מודרני" הוא יעד נע, ל-ES2017 יש את הטווח הרחב ביותר של תאימות לדפדפנים, למרות שהוא כולל את רוב תכונות התחביר המודרניות שנמצאות בשימוש נפוץ. במילים אחרות, ES2017 הוא התחביר המודרני ביותר כיום.

JavaScript מדור קודם

JavaScript מדור קודם הוא קוד שנמנע באופן ספציפי משימוש בכל תכונות השפה שלמעלה. רוב המפתחים כותבים את קוד המקור שלהם באמצעות תחביר מודרני, אבל מהדרים את כל הטקסט בהתאם לתחביר מהדור הקודם כדי להגביר את התמיכה בדפדפנים. הידור לתחביר מדור קודם אכן משפר את התמיכה בדפדפן, אבל בדרך כלל ההשפעה קטנה ממה שאנחנו רואים. במקרים רבים התמיכה עולה מכ-95% ל-98%, אבל היא כרוכה בעלות משמעותית:

  • JavaScript מדור קודם גדול בדרך כלל ב-20% ואיטי יותר מקוד מקביל מודרני. לעיתים קרובות, חוסרים בכלים והגדרה שגויה מרחיבים עוד יותר את הפער.

  • ספריות מותקנות מהוות עד 90% מקוד JavaScript האופייני בסביבת הייצור. קוד הספרייה משתמש בתקורה גבוהה יותר של JavaScript מדור קודם עקב כפילויות ב-polyfill ובכלי עזר, שניתן להימנע מהן על ידי פרסום קוד מודרני.

JavaScript מודרני ב-NPM

לאחרונה, ב-Node.js ביצע סטנדרטיזציה של שדה "exports" כדי להגדיר נקודות כניסה לחבילה:

{
  "exports": "./index.js"
}

במודולים עם ההפניה בשדה "exports" מופיעה גרסת צומת של לפחות 12.8, שתומכת ב-ES2019. המשמעות היא שאפשר לכתוב ב-JavaScript מודרני כל מודול שמתבצע אליו הפניה באמצעות השדה "exports". צרכני חבילות צריכים להניח שמודולים עם השדה "exports" מכילים קוד מודרני וטרנספורמציה במקרה הצורך.

מודרני בלבד

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

{
  "name": "foo",
  "exports": "./modern.js"
}

מודרני עם גיבוי מדור קודם

משתמשים בשדה "exports" יחד עם "main" על מנת לפרסם את החבילה באמצעות קוד מודרני, אבל גם לכלול חלופה ל-ES5 + CommonJS לדפדפנים מדור קודם.

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs"
}

מודרני עם גיבוי מדור קודם ואופטימיזציה של חבילות ESM

בנוסף להגדרת נקודת כניסה חלופית כחלופה, אפשר להשתמש בשדה "module" כדי להצביע על חבילת גיבוי דומה מדור קודם, אבל כזו שמשתמשת בתחביר של מודול JavaScript (import ו-export).

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs",
  "module": "./module.js"
}

חבילות רבות, כמו Webpack ו-Rollup, מסתמכים על השדה הזה כדי לנצל את היתרונות של תכונות המודול ולאפשר ניעור עצים. זו עדיין חבילה מדור קודם שלא מכילה קוד מודרני מלבד התחביר import/export, לכן כדאי להשתמש בגישה הזו כדי לשלוח קוד מודרני עם חלופה קודמת שעדיין מותאמת לקיבוץ.

JavaScript מודרני באפליקציות

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

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

חבילת Webpack

החל מגרסה Webpack 5, אפשר להגדיר את התחביר של webpack יהיה בשימוש כשיוצרים קוד לחבילות ולמודולים. הפעולה הזו לא משנה את הקוד או את יחסי התלות, היא משפיעה רק על קוד ה'דבק' שנוצר על ידי Webpack. כדי לציין את יעד התמיכה בדפדפן, צריך להוסיף לפרויקט הגדרה של רשימת הדפדפנים או ישירות בהגדרות של חבילת ה-webpack:

module.exports = {
  target: ['web', 'es2017'],
};

אפשר גם להגדיר את Webpack ליצור חבילות שעברו אופטימיזציה, ללא פונקציות wrapper מיותרות כשמטרגטים סביבת מודולים מודרנית של ES. הפעולה הזו גם מגדירה את ה-Webpack לטעון חבילות פיצול קוד באמצעות <script type="module">.

module.exports = {
  target: ['web', 'es2017'],
  output: {
    module: true,
  },
  experiments: {
    outputModule: true,
  },
};

יש כמה יישומי פלאגין זמינים של Webpack, שמאפשרים להדר ולשלוח JavaScript מודרני ועדיין לתמוך בדפדפנים מדור קודם, כמו הפלאגין Optimize ו-BabelEsmPlugin.

הפלאגין Optimize

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

מאחר שהפלאגין של Optimize פועל בחבילות במקום במודולים נפרדים, הוא מעבד באופן שווה את קוד האפליקציה ואת יחסי התלות שלכם. כך אפשר להשתמש בקלות בתלות של JavaScript מודרני מ-NPM, כי הקוד יחובר לתחביר הנכון וי לעבור טרנספורמציה כזו. הוא גם יכול להיות מהיר יותר מפתרונות מסורתיים שכוללים שני שלבי הידור, ועדיין ליצור חבילות נפרדות לדפדפנים מודרניים ולדפדפנים מדור קודם. שתי הקבוצות של החבילות נועדו להיטען באמצעות תבנית המודול/nomodule.

// webpack.config.js
const OptimizePlugin = require('optimize-plugin');

module.exports = {
  // ...
  plugins: [new OptimizePlugin()],
};

Optimize Plugin יכול להיות מהיר ויעיל יותר מהגדרות Webpack מותאמות אישית, שבדרך כלל מקבצים בנפרד קוד מודרני וקוד מדור קודם. הוא גם מטפל עבורכם בהרצת Babel ומקטין את החבילות באמצעות Terser עם הגדרות אופטימליות נפרדות לפלט מודרני ומדור קודם. לבסוף, הפוליגונים הדרושים בחבילות הקודמות שנוצרו מחולצים לסקריפט ייעודי, כך שהם אף פעם לא מועתקים או נטענים שלא לצורך בדפדפנים חדשים יותר.

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

BabelEsmPlugin

BabelEsmPlugin הוא פלאגין Webpack שפועל יחד עם @babel/preset-env כדי ליצור גרסאות מודרניות של חבילות קיימות, במטרה לשלוח קוד עם פחות טרנספורמציה לדפדפנים מודרניים. זה הפתרון הכי פופולרי למודול או למודול ב-nomodule, שנעשה בו שימוש ב-Next.js וב-Preact CLI.

// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');

module.exports = {
  //...
  module: {
    rules: [
      // your existing babel-loader configuration:
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [new BabelEsmPlugin()],
};

BabelEsmPlugin תומך במגוון רחב של הגדרות Webpack, כי הוא מריץ שתי גרסאות build נפרדות בעיקרן של האפליקציה. כשמדובר באפליקציות גדולות, הידור כפול יכול להימשך קצת יותר זמן, אבל השיטה הזו מאפשרת ל-BabelEsmPlugin להשתלב בצורה חלקה בהגדרות של Webpack קיימות, והופכת אותה לאחת מהאפשרויות הנוחות ביותר שזמינות.

הגדרת babel-loader ל-transpile node_modules

אם אתם משתמשים ב-babel-loader בלי אחד משני יישומי הפלאגין הקודמים, יש שלב חשוב שנדרש כדי להשתמש במודולים מודרניים של JavaScript. הגדרה של שתי הגדרות נפרדות של babel-loader מאפשרת להדר באופן אוטומטי תכונות שפה מודרניות שנמצאות ב-node_modules עד ES2017, ועדיין להעביר את הקוד שלכם מאינטראקציה ישירה (First-Party) באמצעות יישומי הפלאגין וההגדרות הקבועות מראש של Babel שנקבעו בהגדרות הפרויקט. הפעולה הזו לא יוצרת חבילות מודרניות וחבילות מדור קודם להגדרת מודול או מודול ללא מודולים, אבל כן אפשר להתקין חבילות NPM ולהשתמש בהן שכוללות JavaScript מודרני בלי לפרוץ דפדפנים ישנים.

ב-webpack-plugin-modern-npm השיטה הזו משמשת כדי ליצור יחסי תלות של npm שיש להם שדה "exports" ב-package.json, כי יכול להיות שהם מכילים תחביר מודרני:

// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');

module.exports = {
  plugins: [
    // auto-transpile modern stuff found in node_modules
    new ModernNpmPlugin(),
  ],
};

לחלופין, אפשר להטמיע את השיטה באופן ידני בהגדרות של חבילת ה-webpack. אחרי שתפתרו את הבעיה, תוכלו לחפש את השדה "exports" ב-package.json של המודולים. אם משמיטים את השמירה במטמון לצורך קיצור, הטמעה מותאמת אישית עשויה להיראות כך:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      // Transpile for your own first-party code:
      {
        test: /\.js$/i,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      // Transpile modern dependencies:
      {
        test: /\.js$/i,
        include(file) {
          let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
          try {
            return dir && !!require(dir[0] + 'package.json').exports;
          } catch (e) {}
        },
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            configFile: false,
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
};

כשמשתמשים בגישה הזו צריך לוודא שהתחביר המודרני נתמך על ידי כלי ההקטנה. גם ל-Terser וגם ל-uglify-es יש אפשרות לציין {ecma: 2017} על מנת לשמור, ובמקרים מסוימים ליצור תחביר ES2017 בזמן הדחיסה והפירמוט.

אוסף ערוצים

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

@rollup/Plugin-babel

אם משתמשים בנכס-על, השיטה getBabelOutputPlugin() (שסופקה על ידי הפלאגין הרשמי של Babel) משנה את הקוד בחבילות שנוצרו במקום במודולים נפרדים של מקור. באוסף המשותף יש תמיכה מובנית ביצירה של כמה קבוצות של חבילות כחלק מ-build אחד, ולכל אחת מהן יש יישומי פלאגין משלה. אפשר להשתמש בה כדי ליצור חבילות שונות למודרנית ולדור קודם, על ידי העברת כל אחת מהן דרך הגדרה שונה של פלאגין פלט של Babel:

// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: [
    // modern bundles:
    {
      format: 'es',
      plugins: [
        getBabelOutputPlugin({
          presets: [
            [
              '@babel/preset-env',
              {
                targets: {esmodules: true},
                bugfixes: true,
                loose: true,
              },
            ],
          ],
        }),
      ],
    },
    // legacy (ES5) bundles:
    {
      format: 'amd',
      entryFileNames: '[name].legacy.js',
      chunkFileNames: '[name]-[hash].legacy.js',
      plugins: [
        getBabelOutputPlugin({
          presets: ['@babel/preset-env'],
        }),
      ],
    },
  ],
};

כלי build נוספים

ניתן להגדיר מאוד את התכונות איחוד ו-webpack, כך שכל פרויקט צריך לעדכן את ההגדרות שלו כדי לאפשר תחביר JavaScript מודרני ביחסי תלות. יש גם כלי פיתוח ברמה גבוהה יותר שמעדיפים להשתמש במוסכמה ולהגדרת ברירת מחדל על פני הגדרות כמו Parcel, Snowpack, Vite ו-WMR. רוב הכלים האלה מניחים שיחסי תלות של npm עשויים להכיל תחביר מודרני, והם ישתנו לרישות התחביר המתאימות בתהליך הבנייה.

בנוסף ליישומי פלאגין ייעודיים ל-webpack ול-Rollup, אפשר להוסיף לכל פרויקט חבילות JavaScript מודרניות עם חלופות מדור קודם באמצעות devolution. פיתוח הוא כלי עצמאי שמשנה את הפלט ממערכת build כדי ליצור וריאנטים של JavaScript מדור קודם, וכך מאפשר קיבוץ וטרנספורמציות כדי להניח יעד מודרני של פלט.