تقليل حجم الواجهة الأمامية

كيفية استخدام webpack لتصغير حجم تطبيقك قدر الإمكان

من أوّل الإجراءات التي يجب اتّخاذها عند تحسين تطبيق هو تصغير حجمه بقدر الإمكان. في ما يلي كيفية إجراء ذلك باستخدام webpack.

استخدام وضع الإنتاج (Webpack 4 فقط)

أدخلت Webpack 4 علامة mode الجديدة. يمكنك ضبط هذه العلامة على 'development' أو 'production' للإشارة إلى Webpack بأنّك بصدد إنشاء التطبيق لبيئة معيّنة:

// webpack.config.js
module.exports = {
  mode: 'production',
};

احرص على تفعيل وضع production عند إنشاء تطبيقك للإصدار العلني. سيؤدي ذلك إلى تطبيق webpack لتحسينات مثل التصغير وإزالة الرموز البرمجية المخصّصة للتطوير فقط في المكتبات، وغير ذلك.

مراجع إضافية

تفعيل التصغير

إزالة البيانات غير الضرورية هي عملية ضغط الرمز البرمجي عن طريق إزالة المسافات الإضافية وتقصير أسماء المتغيّرات وغيرها. مثال:

// Original code
function map(array, iteratee) {
  let index = -1;
  const length = array == null ? 0 : array.length;
  const result = new Array(length);

  while (++index < length) {
    result[index] = iteratee(array[index], index, array);
  }
  return result;
}

// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}

يتيح Webpack طريقتَين لتصغير الرمز البرمجي: التصغير على مستوى الحزمة و الخيارات الخاصة ببرنامج التحميل. ويجب استخدامهما في الوقت نفسه.

تصغير الملفات على مستوى الحِزمة

يؤدي تصغير الحِزمة على مستوى الحِزمة إلى ضغط الحِزمة بأكملها بعد الترجمة. إليك آلية عملها:

  1. يمكنك كتابة رمز برمجي على النحو التالي:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. يُجمِّع Webpack هذه العناصر في ما يلي تقريبًا:

    // bundle.js (part of)
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony export (immutable) */ __webpack_exports__["render"] = render;
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1);
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default =
    __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__);
    
    function render(data, target) {
    console.log('Rendered!');
    }
    
  3. ويضغطها برنامج تصغير إلى ما يلي تقريبًا:

    // minified bundle.js (part of)
    "use strict";function t(e,n){console.log("Rendered!")}
    Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)
    

في webpack 4، يتم تفعيل التصغير على مستوى الحِزمة تلقائيًا، سواء في وضع التطوير أو بدونه. ويستخدم أداة تصغير UglifyJS في الخلفية. (إذا أردت إيقاف تصغير الملفات، ما عليك سوى استخدام وضع المطوّر أو تمرير false إلى خيار optimization.minimize).

في webpack 3، عليك استخدام مكوّن UglifyJS الإضافي مباشرةً. يأتي المكوّن الإضافي مجمّعًا مع webpack، لتفعيله، أضِفه إلى القسم plugins في الإعدادات:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.UglifyJsPlugin(),
  ],
};

الخيارات الخاصة ببرنامج التحميل

الطريقة الثانية لتصغير الرمز هي الخيارات الخاصة ببرنامج التحميل (ما هو برنامج التحميل؟). باستخدام خيارات أداة التحميل، يمكنك ضغط العناصر التي لا يمكن لأداة التصغير تصغيرها. على سبيل المثال، عند استيراد ملف CSS يحتوي على css-loader، يتم تجميع الملف في سلسلة:

/* comments.css */
.comment {
  color: black;
}
// minified bundle.js (part of)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n  color: black;\r\n}",""]);

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

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          { loader: 'css-loader', options: { minimize: true } },
        ],
      },
    ],
  },
};

مراجع إضافية

حدِّد NODE_ENV=production

هناك طريقة أخرى لتقليل حجم الواجهة الأمامية وهي ضبط NODE_ENV متغيّر البيئة في الرمز على القيمة production.

تقرأ المكتبات المتغيّر NODE_ENV لرصد الوضع الذي يجب أن تعمل فيه، سواءً كان في وضع التطوير أو وضع الإنتاج. تتصرف بعض المكتبات بشكل مختلف استنادًا إلى هذا المتغيّر. على سبيل المثال، عندما لا يتم ضبط NODE_ENV على production، تُجري Vue.js عمليات تحقّق إضافية وتُطبع تحذيرات:

// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
  warn('props must be strings when using array syntax.');
}
// …

يعمل React بالطريقة نفسها، فهو يحمّل إصدارًا مخصّصًا للتطوير يتضمّن التحذيرات:

// react/index.js
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

// react/cjs/react.development.js
// …
warning$3(
    componentClass.getDefaultProps.isReactClassApproved,
    'getDefaultProps is only used on classic React.createClass ' +
    'definitions. Use a static property named `defaultProps` instead.'
);
// …

لا تكون عمليات التحقّق والتحذيرات هذه ضرورية عادةً في مرحلة الإنتاج، ولكنّها تظلّ في الرمز البرمجي و تزيد من حجم المكتبة. في webpack 4، أزِلها عن طريق إضافة الخيار optimization.nodeEnv: 'production':

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    nodeEnv: 'production',
    minimize: true,
  },
};

في webpack 3، استخدِم DefinePlugin بدلاً من ذلك:

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': '"production"'
    }),
    new webpack.optimize.UglifyJsPlugin()
  ]
};

يعمل كل من الخيار optimization.nodeEnv والخيار DefinePlugin بالطريقة نفسها، ويؤديان إلى استبدال جميع مظاهر process.env.NODE_ENV بالقيمة المحدّدة. باستخدام الإعدادات الواردة أعلاه:

  1. سيستبدِل Webpack جميع موضعَي ورود process.env.NODE_ENV ب "production":

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if (process.env.NODE_ENV !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if ("production" !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    
  2. وبعد ذلك، سيزيل أداة تصغير الملفات كل هذه الفرعَين if لأنّ "production" !== 'production' يكون خطأ دائمًا، ويتفهّم المكوّن الإضافي أنّه لن يتم تنفيذ الرمز البرمجي داخل هذه الفروع مطلقًا:

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if ("production" !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    

    // vue/dist/vue.runtime.esm.js (without minification)
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    }
    

مراجع إضافية

استخدام وحدات ES

الطريقة التالية لتقليل حجم الواجهة الأمامية هي استخدام ES modules.

عند استخدام وحدات ES، يصبح Webpack قادرًا على تنفيذ عملية إزالة المحتوى غير المُستخدَم. تهدف عملية "إزالة المحتوى غير المُستخدَم" إلى أن ينتقل أداة تجميع التطبيقات عبر شجرة التبعيات بالكامل، ويتحقّق من التبعيات المستخدَمة ويزيل التبعيات غير المستخدَمة. وبالتالي، إذا كنت تستخدِم بنية وحدة ES، يمكن أن تزيل أداة webpack الرمز غير المستخدَم:

  1. إذا كتبت ملفًا يتضمّن عمليات تصدير متعددة، ولكنّ التطبيق يستخدم عملية واحدة فقط منها:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. يدرك Webpack أنّه لا يتم استخدام commentRestEndpoint ولا ينشئ نقطة تصدير منفصلة في الحِزمة:

    // bundle.js (part that corresponds to comments.js)
    (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    const render = () => { return 'Rendered!'; };
    /* harmony export (immutable) */ __webpack_exports__["a"] = render;
    
    const commentRestEndpoint = '/rest/comments';
    /* unused harmony export commentRestEndpoint */
    })
    
  3. يزيل أداة تصغير الملفات المتغيّر غير المستخدَم:

    // bundle.js (part that corresponds to comments.js)
    (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
    

ويعمل هذا الإجراء حتى مع المكتبات إذا كانت مكتوبة باستخدام وحدات ES.

ومع ذلك، لا يُطلب منك استخدام أداة تصغير مضمّنة في webpack (UglifyJsPlugin) على وجه التحديد. يمكنك استخدام أي أداة تصغير تتيح إزالة الرموز البرمجية غير الصالحة (مثل مكوّن Babel Minify الإضافي أو مكوّن Google Closure Compiler الإضافي) .

مراجع إضافية

تحسين الصور

تشكّل الصور أكثر من النصف من حجم الصفحة. على الرغم من أنّها ليست مهمة مثل JavaScript (على سبيل المثال، لا تحظر العرض)، إلا أنّها تستهلك جزءًا كبيرًا من النطاق الترددي. استخدِم url-loader وsvg-url-loader وimage-webpack-loader لتحسينها في webpack.

تُدرج url-loader الملفات الثابتة الصغيرة في التطبيق. وبدون ضبط الإعدادات، يتم أخذ ملف تم تمريره ووضعه بجانب الحِزمة المجمّعة وعرض عنوان URL لذلك الملف. ومع ذلك، إذا حدّدنا الخيار limit، سيتم ترميز الملفات الأصغر من هذا الحدّ على أنّها عنوان URL لبيانات Base64 وعرض عنوان URL هذا. يؤدي ذلك إلى تضمين الصورة في رمز JavaScript وحفظ طلب HTTP:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
        options: {
          // Inline files smaller than 10 kB (10240 bytes)
          limit: 10 * 1024,
        },
      },
    ],
  }
};
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`

يعمل svg-url-loader تمامًا مثل url-loader، باستثناء أنّه يُشفّر الملفات باستخدام ترميز عنوان URL بدلاً من ترميز Base64. يكون هذا مفيدًا لصور SVG، لأنّ ملفات SVG هي مجرد نص عادي، ويكون ترميزها أكثر فعالية من حيث الحجم.

module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: "svg-url-loader",
        options: {
          limit: 10 * 1024,
          noquotes: true
        }
      }
    ]
  }
};

تضغط image-webpack-loader الصور التي تمرّ من خلالها. وهو متوافق مع صور JPG وPNG وGIF وSVG، لذا سنستخدمه مع كل هذه الأنواع.

لا يعمل أداة التحميل هذه على تضمين الصور في التطبيق، لذا يجب أن تعمل مع url-loader و svg-url-loader. ولتجنُّب نسخه ولصقه في كلتا القاعدتَين (قاعدة لصور JPG/PNG/GIF وقاعدة أخرى لصور SVG)، سنُدرِج أداة التحميل هذه كقاعدة منفصلة باستخدام enforce: 'pre':

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|svg)$/,
        loader: 'image-webpack-loader',
        // This will apply the loader before the other ones
        enforce: 'pre'
      }
    ]
  }
};

إنّ الإعدادات التلقائية لبرنامج التحميل جيدة للاستخدام، ولكن إذا أردت ضبطه بشكلٍ إضافي، يمكنك الاطّلاع على خيارات المكوّن الإضافي. لتحديد الخيارات التي تريدها، يمكنك الاطّلاع على دليل Addy Osmani الرائع حول تحسين الصور.

مراجع إضافية

تحسين التبعيات

يُمثّل حجم التبعيات أكثر من نصف متوسط حجم JavaScript، وقد يكون جزء من هذا الحجم غير ضروري.

على سبيل المثال، تضيف Lodash (بدءًا من الإصدار 4.17.4) 72 كيلوبايت من الرموز البرمجية المُكثَّفة إلى الحِزمة. ولكن إذا كنت تستخدم مثلاً 20 من طرقها فقط، لن يكون هناك أي فائدة من 65 كيلوبايت تقريبًا من التعليمات البرمجية المُكثَّفة.

ومن الأمثلة الأخرى على ذلك Moment.js. يشغل الإصدار 2.19.1 من هذا الإطار 223 كيلوبايت من الرمز المُصغَّر، وهو حجم كبير. كان متوسط حجم JavaScript على الصفحة 452 كيلوبايت في تشرين الأول (أكتوبر) 2017. ومع ذلك، فإنّ 170 كيلوبايت من هذا الحجم هي ملفات ترجمة . إذا لم تكن تستخدم Moment.js مع لغات متعددة، ستؤدي هذه الملفات إلى زيادة حجم الحِزمة بدون فائدة.

ويمكن تحسين كل هذه التبعيات بسهولة. لقد جمعنا طرق التحسين في مستودع GitHub، ويمكنك الاطّلاع عليه.

تفعيل تسلسل الوحدات لوحدات ES (المعروفة أيضًا باسم تصعيد النطاق)

عند إنشاء حِزمة، يُغلِف Webpack كل وحدة في دالة:

// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
  console.log('Rendered!');
}

// bundle.js (part  of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
  var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
  Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  __webpack_exports__["a"] = render;
  function render(data, target) {
    console.log('Rendered!');
  }
})

كان هذا الإجراء مطلوبًا في السابق لعزل وحدات CommonJS/AMD عن بعضها. ومع ذلك، أدّى ذلك إلى زيادة حجم كل وحدة وتأثيرها في الأداء.

وفّرت أداة Webpack 2 إمكانية استخدام وحدات ES التي يمكن تجميعها بدون لف كل وحدة منها بدالة، على عكس وحدات CommonJS وAMD. وقد وفّرت أداة webpack 3 إمكانية تجميع هذه الوحدات باستخدام ميزة تسلسل الوحدات. في ما يلي دور تسلسل الوحدات:

// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
  console.log('Rendered!');
}

// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files

// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

  // CONCATENATED MODULE: ./comments.js
    function render(data, target) {
    console.log('Rendered!');
  }

  // CONCATENATED MODULE: ./index.js
  render();
})

هل ترى الفرق؟ في الحِزمة العادية، كانت الوحدة 0 تتطلّب render من الوحدة 1. باستخدام تسلسل الوحدات، يتم استبدال require ببساطة بالدالة المطلوبة، ويتم إزالة الوحدة 1. تحتوي الحزمة على عدد أقل من الوحدات، وبالتالي تتطلب طاقة أقل.

لتفعيل هذا السلوك، فعِّل الخيار optimization.concatenateModules في webpack 4:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    concatenateModules: true
  }
};

في webpack 3، استخدِم ModuleConcatenationPlugin:

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
};

مراجع إضافية

استخدِم externals إذا كان لديك رمز webpack ورمز غير webpack.

قد يكون لديك مشروع كبير يتم فيه تجميع بعض الرموز البرمجية باستخدام webpack، بينما لا يتم تجميع بعض الرموز البرمجية. مثل موقع إلكتروني يستضيف الفيديوهات، حيث قد يتم إنشاء تطبيق مشغّل الفيديو باستخدام webpack، وقد لا يتم استخدام هذا الإطار في الصفحة المحيطة:

لقطة شاشة لموقع إلكتروني يستضيف الفيديوهات
(موقع إلكتروني عشوائي تمامًا لاستضافة الفيديوهات)

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

إذا كانت التبعيات متاحة في window

إذا كان الرمز البرمجي غير المستند إلى Webpack يعتمد على تبعيات متاحة كمتغيّرات في window، يمكنك استخدام الأسماء البديلة لأسماء التبعية كأسماء متغيّرات:

// webpack.config.js
module.exports = {
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM'
  }
};

باستخدام هذا الإعداد، لن يجمِّع Webpack حِزم react وreact-dom. بدلاً من ذلك، سيتم استبدالها بما يلي:

// bundle.js (part of)
(function(module, exports) {
  // A module that exports `window.React`. Without `externals`,
  // this module would include the whole React bundle
  module.exports = React;
}),
(function(module, exports) {
  // A module that exports `window.ReactDOM`. Without `externals`,
  // this module would include the whole ReactDOM bundle
  module.exports = ReactDOM;
})

في حال تحميل التبعيات كحِزم AMD

إذا كان الرمز البرمجي غير المستند إلى Webpack لا يعرض التبعيات في window، تصبح الأمور أكثر تعقيدًا. ومع ذلك، لا يزال بإمكانك تجنُّب تحميل الرمز نفسه مرّتين إذا كان الرمز غير المرتبط بـ webpack يستخدِم هذه التبعيات كـ حِزم AMD.

لإجراء ذلك، يمكنك تجميع رمز webpack كمجموعة AMD وإنشاء أسماء بديلة للوحدات تشير إلى عناوين URL للمكتبة:

// webpack.config.js
module.exports = {
  output: {
    libraryTarget: 'amd'
  },
  externals: {
    'react': {
      amd: '/libraries/react.min.js'
    },
    'react-dom': {
      amd: '/libraries/react-dom.min.js'
    }
  }
};

سيُغلِف Webpack الحِزمة في define() ويجعلها تعتمد على عناوين URL التالية:

// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });

إذا كان الرمز البرمجي غير المرتبط بـ webpack يستخدم عناوين URL نفسها لتحميل التبعيات، سيتم تحميل هذه الملفات مرة واحدة فقط، وستستخدم الطلبات الإضافية ذاكرة التخزين المؤقت لبرنامج التحميل.

مراجع إضافية

ملخّص

  • تفعيل وضع الإنتاج في حال استخدام webpack 4
  • تقليل حجم الرمز البرمجي باستخدام خيارات أداة تصغير الرموز البرمجية وأدوات التحميل على مستوى الحزمة
  • أزِل الرمز المخصّص للتطوير فقط عن طريق استبدال NODE_ENV بـ production.
  • استخدام وحدات ES لتفعيل ميزة "إزالة العناصر غير الضرورية من الشجرة"
  • ضغط الصور
  • تطبيق تحسينات خاصة بالتبعيات
  • تفعيل تسلسل الوحدات
  • استخدِم externals إذا كان ذلك مناسبًا لك.