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

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

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

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

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

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

مراجع إضافية

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

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

// 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، يصبح بإمكان webpack إجراء اهتزاز في الشجرة. يحدث اهتزاز الشجرة عندما يجتاز برنامج التجميع شجرة التبعية بأكملها، ويتحقق من التبعيات المستخدمة، ويزيل التبعيات غير المستخدمة. لذلك، إذا كنت تستخدم بنية وحدة ES، فيمكن لحزمة الويب إزالة الرمز غير المستخدَم:

  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 Miniify أو المكوّن الإضافي Google Closure Compiler) العملية.

محتوى إضافي للقراءة

تحسين الصور

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

يضمِّن 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 في حزمة الويب، حيث يستبدل الوحدات بمتغيّرات أو عمليات استيراد خارجية أخرى.

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

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

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

باستخدام هذه الإعدادات، لن تجمع حزمة الويب حزمة 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

إذا كان الرمز الذي ليس ضِمن حزمة الويب لا يعرض الملحقات في window، ستصبح الأمور أكثر تعقيدًا. ومع ذلك، لا يزال بإمكانك تجنُّب تحميل الرمز نفسه مرتين إذا كان الرمز البرمجي بخلاف حزمة الويب يستخدم هذه التبعيات باعتبارها حزم 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 () {  });

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

محتوى إضافي للقراءة

الملخص

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