عرض الرمز الحديث في المتصفِّحات الحديثة لتحميل الصفحات بشكلٍ أسرع

في هذا الدرس التطبيقي حول الترميز، سنعمل على تحسين أداء هذا التطبيق البسيط الذي يتيح للمستخدمين تقييم القطط بشكل عشوائي. تعرَّف على كيفية تحسين حزمة JavaScript من خلال تقليل مقدار الرمز الذي يتم تحويله.

لقطة شاشة التطبيق

في التطبيق النموذجي، يمكنك اختيار كلمة أو رمز إيموجي للتعبير عن مدى إعجابك بكل قطة. عند النقر على زر، يعرض التطبيق قيمة الزر أسفل صورة القطة الحالية.

القياس

من الأفضل دائمًا البدء بفحص موقع إلكتروني قبل إضافة أي تحسينات:

  1. لمعاينة الموقع الإلكتروني، انقر على عرض التطبيق، ثم انقر على ملء الشاشة ملء الشاشة.
  2. اضغط على Control+Shift+J (أو Command+Option+J على أجهزة Mac) لفتح "أدوات مطوّلي البرامج".
  3. انقر على علامة التبويب الشبكة.
  4. ضَع علامة في مربّع الاختيار إيقاف ذاكرة التخزين المؤقت.
  5. أعِد تحميل التطبيق.

طلب حجم الحزمة الأصلي

يتم استخدام أكثر من 80 كيلوبايت لهذا التطبيق. حان الوقت لمعرفة ما إذا كانت بعض أجزاء الحزمة غير مستخدَمة:

  1. اضغط على Control+Shift+P (أو Command+Shift+P على جهاز Mac) لفتح قائمة الأوامر. قائمة الأوامر

  2. أدخِل Show Coverage واضغط على Enter لعرض علامة التبويب التغطية.

  3. في علامة التبويب التغطية، انقر على إعادة التحميل لإعادة تحميل التطبيق أثناء تسجيل بيانات التغطية.

    إعادة تحميل التطبيق مع تغطية الرموز البرمجية

  4. اطّلِع على مقدار الرمز الذي تم استخدامه مقارنةً بمقدار الرمز الذي تم تحميله للحزمة الرئيسية:

    نسبة استخدام رموز الحزمة

ولا يتم حتى استخدام أكثر من نصف الحزمة (44 كيلوبايت). ويرجع ذلك إلى أنّ الكثير من التعليمات البرمجية المضمّنة تتألف من عمليات تعبئة لضمان عمل التطبيق في المتصفحات القديمة.

استخدام @babel/preset-env

تتوافق بنية لغة JavaScript مع معيار يُعرف باسم ECMAScript أو ECMA-262. يتم إصدار نسخ أحدث من المواصفات كل عام، وتتضمّن ميزات جديدة اجتازت عملية الاقتراح. ويكون كل متصفّح رئيسي دائمًا في مرحلة مختلفة من توفير هذه الميزات.

يتم استخدام ميزات ES2015 التالية في التطبيق:

يتم أيضًا استخدام ميزة ES2017 التالية:

يمكنك الاطّلاع على رمز المصدر في src/index.js لمعرفة كيفية استخدام كل هذه العناصر.

تتوفّر كل هذه الميزات في أحدث إصدار من Chrome، ولكن ماذا عن المتصفّحات الأخرى التي لا تتوافق معها؟ Babel، المضمّنة في التطبيق، هي المكتبة الأكثر شيوعًا التي تُستخدم لتجميع الرموز البرمجية التي تتضمّن بنية أحدث في رموز برمجية يمكن للمتصفحات والبيئات القديمة فهمها. ويتم ذلك بطريقتين:

  • يتم تضمين الرموز البرمجية polyfill لمحاكاة وظائف ES2015+ الأحدث كي يمكن استخدام واجهات برمجة التطبيقات الخاصة بها حتى إذا كان المتصفّح لا يتيحها. في ما يلي مثال على 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 جميع رموز 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. تعرَّف على كيفية تضمين Babel في تطبيقك إذا كنت تستخدم أداة تجميع وحدات مختلفة عن webpack.

تحدّد السمة 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 أيّ عمليات تعبئة مستخدَمة:

لم تتم إضافة أي polyfills

يرجع ذلك إلى أنّه يتم استيراد @babel/polyfill بأكمله مباشرةً.

تحميل polyfill بشكل فردي

تتضمّن Babel تلقائيًا كل عملية تعبئة ضرورية لبيئة ES2015+ كاملة عند استيراد @babel/polyfill إلى ملف. لاستيراد polyfills معيّنة مطلوبة للمتصفّحات المستهدَفة، أضِف useBuiltIns: 'entry' إلى الإعدادات.

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

أعِد تحميل التطبيق. يمكنك الآن الاطّلاع على جميع عمليات التعبئة المحدّدة المضمّنة:

قائمة عمليات التعبئة التي تم استيرادها

على الرغم من أنّنا نضمّن الآن فقط polyfills المطلوبة لـ "last 2 versions"، لا تزال القائمة طويلة جدًا. ويرجع ذلك إلى أنّ البرامج المساعدة المطلوبة للمتصفحات المستهدَفة لكل ميزة أحدث لا تزال مضمَّنة. غيِّر قيمة السمة إلى usage لتضمين تلك المطلوبة للميزات المستخدَمة في الرمز فقط.

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

وبذلك، يتم تضمين polyfills تلقائيًا عند الحاجة إليها. وهذا يعني أنّه يمكنك إزالة عملية استيراد @babel/polyfill في src/index.js.

import "./style.css";
import "@babel/polyfill";

يتم الآن تضمين polyfills المطلوبة للتطبيق فقط.

قائمة بالرموز البرمجية المتوافقة المضمَّنة تلقائيًا

يتم تقليل حجم حِزمة التطبيق بشكلٍ كبير.

تم تقليل حجم الحزمة إلى 30.1 كيلوبايت

تضييق قائمة المتصفّحات المتوافقة

لا يزال عدد استهدافات المتصفّحات المضمّنة كبيرًا جدًا، ولا يستخدم العديد من المستخدمين متصفّحات متوقّفة نهائيًا، مثل Internet Explorer. عدِّل الإعدادات إلى ما يلي:

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

اطّلِع على تفاصيل الحزمة التي تم جلبها.

حجم الحزمة 30.0 كيلوبايت

بما أنّ التطبيق صغير جدًا، لن يكون هناك فرق كبير عند إجراء هذه التغييرات. ومع ذلك، فإنّ استخدام نسبة مئوية من حصة السوق للمتصفّح (مثل ">0.25%") مع استبعاد متصفّحات معيّنة تثق بأنّ المستخدمين لا يستعملونها هو الأسلوب الذي ننصح به. يمكنك الاطّلاع على مقالة "آخر إصدارَين" ضارّان لـ "جيمس كايل" لمعرفة المزيد حول هذا الموضوع.

استخدام <script type="module">

لا يزال هناك مجال كبير للتحسين. على الرغم من أنّه تمت إزالة عدد من عمليات التعبئة غير المستخدَمة، لا يزال يتم تضمين العديد من عمليات التعبئة التي لا تحتاج إليها بعض المتصفّحات. باستخدام الوحدات، يمكن كتابة صيغة أحدث وشحنها إلى المتصفحات مباشرةً بدون استخدام أي رموز تعبئة غير ضرورية.

وحدات 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 على "صحيح" هنا، ما يعني أنّ الرمز الذي يتم إخراجه في هذه الوحدة النمطية هو نص برمجي أصغر حجمًا وأقل تجميعًا ولا يخضع لأي تحويل في هذا المثال لأنّ جميع الميزات المستخدَمة متوافقة مع المتصفّحات التي تتوافق مع الوحدات النمطية.

في نهاية الملف، صدِّر كلا الإعدادَين في مصفوفة واحدة.

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.2 كيلوبايت تم جلبها عبر الشبكة للمتصفّحات الأحدث

يتم جلب الوحدة فقط، ويكون حجم الحزمة أصغر بكثير لأنّها غير مترجمة إلى حد كبير. يتجاهل المتصفّح عنصر البرنامج النصي الآخر تمامًا.

إذا حمّلت التطبيق على متصفّح قديم، سيتم جلب النص البرمجي الأكبر الذي تم تحويله فقط مع جميع الرموز البرمجية polyfill وtransform اللازمة. في ما يلي لقطة شاشة لجميع الطلبات التي تم إجراؤها على إصدار قديم من Chrome (الإصدار 38).

استرجاع نص برمجي بحجم 30 كيلوبايت للمتصفّحات القديمة

الخاتمة

أنت الآن على دراية بكيفية استخدام @babel/preset-env لتوفير polyfills الضرورية فقط للمتصفحات المستهدَفة. أنت على دراية أيضًا بكيفية مساهمة وحدات JavaScript في تحسين الأداء بشكل أكبر من خلال توفير إصدارَين مختلفَين من التطبيق بعد تحويل الرمز البرمجي. بعد أن أصبحت لديك فكرة جيدة عن كيفية مساهمة هاتين التقنيتين في خفض حجم الحِزمة بشكل كبير، يمكنك المضي قدمًا وتحسينها.