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

في هذا الدرس التطبيقي حول الترميز، يمكنك تحسين أداء هذا التطبيق البسيط الذي يتيح للمستخدمين تقييم قطط عشوائية. تعرَّف على كيفية تحسين حِزمة 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 كيلوبايت). وذلك لأن الكثير من الرموز داخلها تتكون من رموز polyfill لضمان عمل التطبيق في المتصفحات القديمة.

استخدام @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 جميع polyfills اللازمة لأي ميزات ECMAScript أحدث حتى تتمكّن من العمل في البيئات التي لا تتوافق معها. تم استيرادها من قبل في أعلى src/index.js..
import "./style.css";
import "@babel/polyfill";
  • تحدِّد @babel/preset-env عمليات التحويل ورموز polyfill اللازمة لأي متصفِّحات أو بيئات يتم اختيارها كأهداف.

اطّلِع على ملف إعدادات 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 نقل بنية معيّنة لها. يؤدي ذلك إلى زيادة حجم الحزمة بدون داعٍ إذا كان المستخدمون لا يستخدمون هذا المتصفّح للوصول إلى موقعك الإلكتروني.

يسجِّل 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 الأداء بشكلٍ أكبر من خلال شحن نسختَين مختلفتَين من تطبيق تم تحويلهما. بعد أن تعرّفت على كيفية خفض حجم الحِزمة بشكلٍ كبير باستخدام هاتين الطريقتَين، يمكنك بدء عملية التحسين.