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

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

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

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

قياس

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

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

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

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

  1. اضغط على Control+Shift+P (أو Command+Shift+P على نظام التشغيل Mac) لفتح قائمة Command. قائمة الأوامر

  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 قديمة. نظرًا لأن هذه تغييرات متعلقة ببناء الجملة (مثل دوال الأسهم)، فلا يمكن محاكاتها باستخدام رموز polyfill.

يمكنك الاطّلاع على 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.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 عمليات التحويل ورموز polyfill المهمة لأي متصفِّحات أو بيئات يتم اختيارها كأهداف.

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

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

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

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

تنقل القيمة "last 2 versions" الرمز في التطبيق لآخر إصدارين من كل متصفح.

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

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

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

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

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

تقوم Babel بتسجيل عدد من التفاصيل إلى وحدة التحكم حول عملية التجميع، بما في ذلك جميع البيئات المستهدفة التي تم تجميع التعليمة البرمجية من أجلها.

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

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

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

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

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

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

لم تتم إضافة أي رموز polyfill.

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

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

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

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

أعِد تحميل التطبيق. يمكنك الآن مشاهدة جميع رموز polyfill المحددة المضمَّنة:

قائمة رموز polyfill التي تم استيرادها

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

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

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

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

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

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

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

وحدات 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.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 يتضمّن جميع عمليات التحويل ورموز polyfill اللازمة لاستهداف كل متصفِّح لا يتيح بعد استخدام وحدات 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"})
];

وبالمثل، يمكنك الآن إنشاء كائن config للنص البرمجي الخاص بالوحدة حيث يتم تحديد 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. للأسف، HTMLWebpackPlugin، لا يتيح حاليًا ناتج كل من الوحدة ولا نصوص برمجية للوحدات المكوِّنة الإضافية التي تنشئ ملف HTML النهائي. على الرغم من وجود حلول بديلة ومكوِّنات إضافية منفصلة تم إنشاؤها لحل هذه المشكلة، مثل BabelMultiTargetPlugin وHTMLWebpackMultiBuildPlugin، يتم استخدام طريقة أبسط لإضافة عنصر النص البرمجي للوحدة يدويًا لغرض هذا البرنامج التعليمي.

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

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

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

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

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

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

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

الخلاصة

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