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

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

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

استخدام وضع الإنتاج (حزمة الويب 4 فقط)

قدّمت حزمة 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)
    

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

في حزمة الويب 3، عليك استخدام المكوّن الإضافي UglifyJS مباشرةً. يكون المكوّن الإضافي مضمَّنًا مع حزمة الويب. ولتفعيله، يمكنك إضافته إلى القسم 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.'
);
// …

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

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

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

ومع ذلك، لست مطالبًا باستخدام أداة التصغير المضمنة في حزمة الويب (UglifyJsPlugin). يمكن استخدام أي أداة مصغَّرة تتيح إزالة الرموز غير الصالحة (مثل المكوّن الإضافي Babel Minify أو المكوّن الإضافي 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'
      }
    ]
  }
};

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

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

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

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

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

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

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

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

عند إنشاء حزمة، تلف حزمة الويب كل وحدة في دالة:

// 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. كما أتاحت حزمة الويب 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 من الوحدة الأولى. باستخدام تسلسل الوحدات، يتم ببساطة استبدال require بالدالة المطلوبة، وتتم إزالة الوحدة 1. تحتوي الحزمة على عدد وحدات أقل، مع أعباء أقل.

لتفعيل هذا السلوك، في حزمة الويب 4، فعِّل الخيار optimization.concatenateModules:

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

في حزمة الويب 3، يمكنك استخدام ModuleConcatenationPlugin:

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

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

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

استخدام externals إذا كان لديك رمز حزمة ويب ورمز غير حزمة ويب

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

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

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

في حال توفّر التبعيات في "window"

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

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

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

التلخيص

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