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

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

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

ابدأ بإضافة إعدادات النص البرمجي القديم إلى 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"})
];

وبالمثل، يمكنك الآن إنشاء كائن ضبط للنص البرمجي للوحدة أدناه حيث يتم تحديد 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 والتحويل المطلوبة. إليك لقطة شاشة لجميع الطلبات التي تم إجراؤها على إصدار قديم من Chrome (الإصدار 38).

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

الخلاصة

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