ارائه کد نو به مرورگرهای نو برای بار شدن سریع‌تر صفحه‌ها

در این codelab، عملکرد این برنامه ساده را که به کاربران اجازه می‌دهد به گربه‌های تصادفی امتیاز دهند بهبود دهید. با نحوه بهینه‌سازی بسته جاوا اسکریپت ازطریق به‌حداقل رساندن مقدار کد ترجمه‌شده مبدأ به مبدأ آشنا شوید.

نماگرفت برنامه

در برنامه نمونه، می‌توانید کلمه یا اموجی‌ای را انتخاب کنید تا نشان دهید چقدر هر گربه را دوست دارید. وقتی روی دکمه‌ای کلیک می‌کنید، برنامه مقدار دکمه را در زیر تصویر گربه فعلی نمایش می‌دهد.

اندازه گرفتن

همیشه ایده خوبی است که قبل‌از افزودن هرگونه بهینه‌سازی، با بازرسی وب‌سایت شروع کنید:

  1. برای پیش‌دید کردن سایت، مشاهده برنامه را فشار دهید. سپس تمام‌صفحه تمام‌صفحه را فشار دهید.
  2. برای باز کردن DevTools، کلیدهای `Control+Shift+J` (یا `Command+Option+J` در Mac) را فشار دهید.
  3. روی برگه شبکه کلیک کنید.
  4. چارگوش انتخاب Disable cache (غیرفعال کردن حافظه نهان) را انتخاب کنید.
  5. برنامه را مجدداً بار کنید.

درخواست اندازه دسته اصلی

بیش‌از ۸۰ کیلوبایت برای این برنامه استفاده شده است! زمان لازم برای تشخیص اینکه بخش‌هایی از بسته استفاده نمی‌شود:

  1. کلیدهای Control+Shift+P (یا Command+Shift+P در Mac) را فشار دهید تا منو فرمان باز شود. منو فرمان

  2. Show Coverage را وارد کنید و Enter را بزنید تا برگه پوشش نمایش داده شود.

  3. در برگه پوشش، روی مجدد بار کردن کلیک کنید تا برنامه درحین ضبط پوشش مجدداً بار شود.

    بار کردن مجدد برنامه با پوشش کد

  4. ببینید چه مقدار کد استفاده شده است درمقابل چه مقدار کد برای بسته اصلی بار شده است:

    پوشش کد بسته

بیش‌از نیمی از بسته (۴۴ کیلوبایت) حتی استفاده نشده است. دلیلش این است که بخش زیادی از کد درون آن شامل شبیه‌سازهای مرورگر است تا اطمینان حاصل شود که برنامه در مرورگرهای قدیمی‌تر کار می‌کند.

استفاده از @babel/preset-env

نحو زبان جاوا اسکریپت با استانداردی به‌نام ECMAScript یا ECMA-262 مطابقت دارد. نسخه‌های جدیدتر مشخصات هر سال منتشر می‌شود و شامل ویژگی‌های جدیدی است که فرایند پیشنهاد را گذرانده‌اند. هر مرورگر اصلی همیشه در مرحله متفاوتی از پشتیبانی این ویژگی‌ها قرار دارد.

ویژگی‌های ES2015 زیر در برنامه استفاده می‌شود:

از ویژگی ES2017 زیر نیز استفاده می‌شود:

می‌توانید کد منبع را در src/index.js بررسی کنید تا ببینید چگونه از همه این‌ها استفاده می‌شود.

همه این ویژگی‌ها در جدیدترین نسخه Chrome پشتیبانی می‌شوند، اما مرورگرهای دیگری که از آن‌ها پشتیبانی نمی‌کنند چطور؟ Babel، که در برنامه گنجانده شده است، محبوب‌ترین کتابخانه مورداستفاده برای همگردانی کد حاوی نحو جدیدتر به کدی است که مرورگرها و محیط‌های قدیمی‌تر بتوانند درک کنند. این کار را به دو روش انجام می‌دهد:

  • شبیه‌سازهای مرورگر برای شبیه‌سازی عملکردهای جدیدتر ES2015+‎ گنجانده شده‌اند تا میاناهای برنامه‌سازی کاربردی آن‌ها حتی اگر مرورگر از آن‌ها پشتیبانی نکند قابل‌استفاده باشند. در اینجا مثالی از شبیه‌ساز مرورگر روش Array.includes آورده شده است.
  • از افزایه‌ها برای تبدیل کد ES2015 (یا جدیدتر) به نحو قدیمی‌تر ES5 استفاده می‌شود. ازآنجایی‌که این تغییرات مربوط به نحو هستند (مانند توابع پیکانی)، نمی‌توان آن‌ها را با شبیه‌سازهای مرورگر شبیه‌سازی کرد.

به 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 همه شبیه‌سازهای مرورگر لازم را برای هر ویژگی جدیدتر 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 ادغام می‌شود، یعنی می‌توانید فهرست کاملی از پُرسمان‌های سازگار را که می‌توان در این فیلد استفاده کرد در سند browserlist پیدا کنید.

مقدار "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 هیچ شبیه‌ساز مرورگر خاصی را که استفاده می‌شود نشان نمی‌دهد:

هیچ شبیه‌ساز مرورگری اضافه نشده است

دلیل آن این است که کل @babel/polyfill مستقیماً وارد می‌شود.

بار کردن شبیه‌سازهای مرورگر به‌صورت جداگانه

به‌طور پیش‌فرض، Babel شامل هر شبیه‌ساز مرورگر موردنیاز برای یک محیط کامل ES2015+ است وقتی @babel/polyfill به یک فایل وارد می‌شود. برای وارد کردن شبیه‌سازهای مرورگر خاص موردنیاز برای مرورگرهای هدف، useBuiltIns: 'entry' را به پیکربندی اضافه کنید.

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

برنامه را مجدداً بار کنید. اکنون می‌توانید همه شبیه‌سازهای مرورگر خاص گنجانده‌شده را ببینید:

فهرست شبیه‌سازهای مرورگر واردشده

اگرچه اکنون فقط شبیه‌سازهای مرورگر موردنیاز برای "last 2 versions" گنجانده شده است، اما هنوز فهرست بسیار طولانی‌ای است! دلیلش این است که شبیه‌سازهای مرورگر موردنیاز برای مرورگرهای هدف برای هر ویژگی جدیدتر همچنان گنجانده شده است. مقدار مشخصه را به 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";

اکنون فقط شبیه‌سازهای مرورگر موردنیاز برای برنامه گنجانده شده است.

فهرست شبیه‌سازهای مرورگر به‌طور خودکار اضافه شد

اندازه دسته برنامه به‌طور قابل‌توجهی کاهش می‌یابد.

اندازه بسته به ۳۰٫۱ کیلوبایت کاهش یافت

محدود کردن فهرست مرورگرهای پشتیبانی‌شده

تعداد مرورگرهای هدف گنجانده‌شده هنوز بسیار زیاد است و کاربران زیادی از مرورگرهای متوقف‌شده مانند Internet Explorer استفاده نمی‌کنند. پیکربندی‌ها را به موارد زیر به‌روز کنید:

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

جزئیات دسته واکشی‌شده را ببینید.

اندازه بسته ۳۰٫۰ کیلوبایت

ازآنجایی‌که برنامه بسیار کوچک است، واقعاً تفاوت چندانی با این تغییرات ندارد. بااین‌حال، استفاده از درصد سهم بازار مرورگر (مانند ">0.25%") همراه با مستثنی کردن مرورگرهای خاصی که مطمئن هستید کاربرانتان از آن‌ها استفاده نمی‌کنند، رویکرد توصیه‌شده است. برای کسب اطلاعات بیشتر درباره این موضوع، به مقاله «دو نسخه آخر» مضر تلقی می‌شود نوشته جیمز کایل مراجعه کنید.

از <script type="module"> استفاده کنید

هنوز جای بیشتری برای بهبود وجود دارد. اگرچه تعدادی از شبیه‌سازهای مرورگر استفاده‌نشده برداشته شده‌اند، اما بسیاری از آن‌ها که برای برخی‌از مرورگرها لازم نیستند همچنان ارسال می‌شوند. بااستفاده از ماژول‌ها، نحو جدیدتر را می‌توان نوشت و مستقیماً به مرورگرها ارسال کرد بدون اینکه نیازی به استفاده از شبیه‌سازهای مرورگر غیرضروری باشد.

مدول‌های جاوا اسکریپت ویژگی نسبتاً جدیدی هستند که در همه مرورگرهای اصلی پشتیبانی می‌شوند. می‌توان بااستفاده از مشخصه 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 درحال‌حاضر در محیط‌هایی که از ماژول‌های جاوا اسکریپت پشتیبانی می‌کنند (به‌جای نیاز به 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 نهایی را ایجاد می‌کند، HTMLWebpackPlugin، درحال‌حاضر از خروجی هر دو اسکریپت module و nomodule پشتیبانی نمی‌کند. اگرچه راه‌حل‌های موقت و افزایه‌های جداگانه‌ای برای حل این مشکل ایجاد شده‌اند، مانند BabelMultiTargetPlugin و HTMLWebpackMultiBuildPlugin، اما برای این آموزش از رویکرد ساده‌تر افزودن عنصر اسکریپت ماژول به‌صورت دستی استفاده می‌شود.

موارد زیر را به src/index.js در انتهای فایل اضافه کنید:

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

اکنون برنامه را در مرورگری که از ماژول‌ها پشتیبانی می‌کند، مانند جدیدترین نسخه Chrome، بار کنید.

ماژول ۵٫۲ کیلوبایتی ازطریق شبکه برای مرورگرهای جدیدتر واکشی شد

فقط ماژول واکشی می‌شود، با اندازه بسته‌ای بسیار کوچک‌تر، زیرا تا حد زیادی ترجمه نشده است! عنصر اسکریپت دیگر کاملاً توسط مرورگر نادیده گرفته می‌شود.

اگر برنامه را در مرورگر قدیمی‌تری بار کنید، فقط اسکریپت بزرگ‌تر و ترجمه مبدأ به مبدأ شده با همه شبیه‌سازهای مرورگر و تبدیل‌های موردنیاز واکشی می‌شود. در اینجا نماگرفتی از همه درخواست‌های انجام‌شده در نسخه قدیمی‌تر Chrome (نسخه ۳۸) ارائه شده است.

اسکریپت ۳۰ کیلوبایتی برای مرورگرهای قدیمی‌تر واکشی شد

نتیجه‌گیری

اکنون می‌دانید که چگونه از @babel/preset-env برای ارائه فقط شبیه‌سازهای مرورگر ضروری موردنیاز برای مرورگرهای هدف استفاده کنید. همچنین می‌دانید که چگونه ماژول‌های جاوا اسکریپت می‌توانند با ارسال دو نسخه ترجمه مبدأ به مبدأ متفاوت از یک برنامه، عملکرد را بیشتر بهبود بخشند. با درک مناسبی از اینکه چگونه هر دو این تکنیک‌ها می‌توانند اندازه بسته شما را به‌طور قابل‌توجهی کاهش دهند، پیش بروید و بهینه‌سازی کنید!