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

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

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

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

القياس

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • يتم تضمين Polyfills لمحاكاة دوال 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 يحدِّد عمليات التحويل وpolyfills التي تكون ضرورية لأي متصفّحات أو بيئات تم اختيارها كأهداف.

اطّلِع على ملف إعدادات Babel، .babelrc، لمعرفة كيفية تضمينه:

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

هذا هو إعداد Babel وWebpack. تعرَّف على كيفية تضمين Babel في تطبيقك إذا كنت تستخدم أداة تجميع ملفّات برمجية مختلفة عن webpack.

تحدّد السمة targets في .babelrc المتصفّحات التي يتم استهدافها. @babel/preset-env يتم دمجها مع browserslist، ما يعني أنّه يمكنك العثور على قائمة كاملة بالطلبات المتوافقة التي يمكن استخدامها في هذا الحقل في مستندات browserslist.

تُحوِّل قيمة "last 2 versions" الرمز البرمجي في التطبيق إلى الإصدارَين الأخيرَين من كل متصفّح.

تصحيح الأخطاء

للحصول على نظرة شاملة على جميع استهدافات Babel للمتصفّح بالإضافة إلى جميع عمليات التحويل وpolyfills المضمّنة، أضِف حقل debug إلى .babelrc:.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • انقر على الأدوات.
  • انقر على السجّلات.

أعِد تحميل التطبيق واطّلِع على سجلّات حالة Glitch في أسفل المحرِّر.

المتصفّحات المستهدَفة

يسجِّل Babel عددًا من التفاصيل في وحدة التحكّم حول عملية الترجمة، بما في ذلك جميع البيئات المستهدَفة التي تم ترجمة الرمز البرمجي لها.

المتصفّحات المستهدَفة

يُرجى ملاحظة أنّ المتصفّحات التي تم إيقافها نهائيًا، مثل Internet Explorer، مضمّنة في هذه القائمة. ويشكّل ذلك مشكلة لأنّ المتصفّحات غير المتوافقة لن تحصل على ميزات أحدث، وسيواصل Babel تحويل بنية معيّنة لها. يؤدي ذلك إلى زيادة حجم الحِزمة بدون داعٍ إذا لم يكن المستخدمون يستخدمون هذا browser للوصول إلى موقعك الإلكتروني.

يسجِّل Babel أيضًا قائمة بالمكوّنات الإضافية للتحويل المستخدَمة:

قائمة المكونات الإضافية المستخدَمة

هذه قائمة طويلة جدًا. هذه هي جميع المكوّنات الإضافية التي يحتاج Babel إلى استخدامها لتحويل أي بنية جملة ES2015 والإصدارات الأحدث إلى بنية جملة قديمة لجميع المتصفّحات المستهدَفة.

ومع ذلك، لا يعرض Babel أيّ polyfills محدّدة يتم استخدامها:

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

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

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

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

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

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

قائمة مكونات polyfill المستورَدة

على الرغم من أنّه تمّ الآن تضمين مجموعات polyfills المطلوبة فقط لنظام التشغيل "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";

يتم الآن تضمين وحدات polyfill المطلوبة فقط للتطبيق.

قائمة بالعناصر القابلة للتضمين تلقائيًا

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

تم تقليل حجم الحزمة إلى 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%") مع استبعاد متصفّحات معيّنة تثق بأنّ المستخدِمين لا يستخدمونها هو النهج المُقترَح. اطّلِع على مقالة "آخر إصدارَين" من التطبيق يُعتبَران ضارَّين penned by James Kyle للتعرّف على مزيد من المعلومات حول هذا الموضوع.

استخدِم علامة <script type="module">.

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

عرض الوحدات في ملف HTML

الخطوة الأخيرة التي يجب اتّخاذها هي إخراج عناصر النصوص البرمجية القديمة والحديثة إلى ملف HTML. إنّ المكوّن الإضافي الذي ينشئ ملف HTML النهائي، وهو HTMLWebpackPlugin، لا يتيح حاليًا عرض ناتج النص البرمجي لكل من module وnomodule. على الرغم من توفّر حلول بديلة ومكونات إضافية منفصلة تم إنشاؤها لحلّ هذه المشكلة، مثل BabelMultiTargetPlugin وHTMLWebpackMultiBuildPlugin، يتم استخدام نهج أبسط لإضافة عنصر نص البرنامج للوحدة يدويًا بغرض هذا الدليل التعليمي.

أضِف ما يلي إلى src/index.js في نهاية الملف:

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

الآن، حمِّل التطبيق في متصفّح متوافق مع الوحدات، مثل أحدث إصدار من Chrome.

وحدة بحجم 5.2 كيلوبايت تم جلبها عبر الشبكة للمتصفّحات الأحدث

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

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

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

الخاتمة

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