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

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

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

JavaScript מודרני

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

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

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

JavaScript מדור קודם

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

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

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

JavaScript מודרני ב-NPM

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

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

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

מודרני בלבד

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

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

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

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

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

מודרני עם גיבוי מדור קודם ואופטימיזציות ESM Bundler

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

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

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

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

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

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

חבילה ו-Webpack

החל מגרסה 5 של Webpack, עכשיו אפשר להגדיר באיזה תחביר ייעשה שימוש ב-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 Plugin ו-BabelEsmPlugin.

הפלאגין של Optimize

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

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

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

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

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

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

BabelEsmPlugin

BabelEsmPlugin הוא חבילת אינטרנט פלאגין שפועל יחד עם @babel/preset-env ליצור גרסאות מודרניות של חבילות קיימות כדי לשלוח קוד פחות שעבר טרנספורמציה לדפדפנים מודרניים. זהו הפתרון המקורי הפופולרי ביותר מודול/noModule, נמצאים בשימוש על ידי Next.js וגם גרסת 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 תומך במגוון רחב של הגדרות אישיות בשביל חבילות אינטרנט, כי מפעיל שני גרסאות build נפרדות בעיקרו של האפליקציה. הידור כפול יכול להימשך מעט זמן נוסף לאפליקציות גדולות, אבל השיטה הזו מאפשרת BabelEsmPlugin כדי לשלב בצורה חלקה בהגדרות הקיימות של חבילות האינטרנט והופך אותה לאחת מהאפשרויות הנוחות ביותר שקיימות.

הגדרת babel-loader ל-transpileNode_Modules

אם אתם משתמשים ב-babel-loader בלי אחד משני יישומי הפלאגין הקודמים, נדרש שלב חשוב כדי להשתמש ב-JavaScript NPM מודרני מודולים. הגדרה של שתי הגדרות נפרדות של babel-loader מאפשרת כדי להדר באופן אוטומטי תכונות שפה מודרניות שנמצאות ב-node_modules ES2017, ועדיין במהלך טרנספורמציה של קוד צד ראשון שלכם עם 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 נוספים

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

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