إزالة الكود غير المستخدم

Tree shaking مصطلح شائع في سياق JavaScript ويُقصد به إزالة الكود الميت. يعتمد على البنية الساكنة لصيغة وحدات ES2015، أي import و export. وقد شاع الاسم والمفهوم بفضل أداة تجميع وحدات ES2015 وهي rollup.

جاء إصدار webpack 2 بدعم مدمج لوحدات ES2015 (المعروفة أيضًا باسم harmony modules)، إضافةً إلى اكتشاف صادرات الوحدات غير المستخدمة. ويوسّع webpack 4 هذه القدرة بطريقة تتيح تقديم تلميحات للمصرّف عبر خاصية "sideEffects" في package.json لتحديد الملفات "النقية" في مشروعك، وبالتالي الآمنة للحذف إذا لم تكن مستخدمة.

إضافة أداة مساعدة

لنضف ملف أداة مساعدة جديدًا إلى مشروعنا، src/math.js، يصدّر دالتين:

project

 webpack-demo
  ├── package.json
  ├── package-lock.json
  ├── webpack.config.js
  ├── /dist
  │   ├── bundle.js
  │   └── index.html
  ├── /src
  │   ├── index.js
+ │   └── math.js
  └── /node_modules

src/math.js

export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

عيّن خيار الإعداد mode إلى development للتأكد من أن الحزمة لن تُصغَّر:

webpack.config.js

import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
+ mode: 'development',
+ optimization: {
+   usedExports: true,
+ },
};

بعد ذلك، لنحدّث سكربت الإدخال لاستخدام إحدى هاتين الدالتين الجديدتين، ونزيل lodash للتبسيط:

src/index.js

- import _ from 'lodash';
+ import { cube } from './math.js';

  function component() {
-   const element = document.createElement('div');
+   const element = document.createElement('pre');

-   // Lodash, now imported by this script
-   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.innerHTML = [
+     'Hello webpack!',
+     '5 cubed is equal to ' + cube(5)
+   ].join('\n\n');

    return element;
  }

  document.body.appendChild(component());

لاحظ أننا لم نقم باستيراد الدالة square من الوحدة src/math.js. هذه الدالة هي ما يُعرف بـ "الكود الميت"، أي export غير مستخدم يفترض إسقاطه. لنشغّل الآن سكربت npm، أي npm run build، ونفحص الحزمة الناتجة:

dist/bundle.js (حول الأسطر 90 - 100)

/* 1 */
/***/ (function (module, __webpack_exports__, __webpack_require__) {
  "use strict";

  /* unused harmony export square */
  /* harmony export (immutable) */ __webpack_exports__.a = cube;
  function square(x) {
    return x * x;
  }

  function cube(x) {
    return x * x * x;
  }
});

لاحظ التعليق unused harmony export square أعلاه. إذا نظرت إلى الكود تحته، ستلاحظ أن square لا يتم استيرادها، ومع ذلك ما زالت موجودة داخل الحزمة. سنعالج ذلك في القسم التالي.

وسم الملف بأنه خالٍ من الآثار الجانبية

في عالم وحدات ESM بنسبة 100%، يكون تحديد الآثار الجانبية مباشرًا. لكننا لم نصل إلى ذلك تمامًا بعد، لذلك من الضروري في الوقت الحالي تقديم تلميحات إلى مصرّف webpack حول "نقاء" الكود.

تتم هذه العملية عبر خاصية "sideEffects" في package.json.

{
  "name": "your-project",
  "sideEffects": false
}

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

إذا كان كودك يحتوي على بعض الآثار الجانبية، فيمكن تقديم مصفوفة بدلًا من ذلك:

{
  "name": "your-project",
  "sideEffects": ["./src/some-side-effectful-file.js"]
}

تقبل المصفوفة أنماط glob بسيطة للملفات المعنية. وهي تستخدم glob-to-regexp داخليًا (تدعم: *، **، {a,b}، [a-z]). الأنماط مثل *.css، التي لا تتضمن /، ستُعامل مثل **/*.css.

{
  "name": "your-project",
  "sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}

أخيرًا، يمكن أيضًا تعيين "sideEffects" من خيار الإعداد module.rules.

توضيح tree shaking و sideEffects

تحسينا sideEffects و usedExports (المعروف أكثر باسم tree shaking) شيئان مختلفان.

sideEffects أكثر فعالية بكثير لأنه يسمح بتخطي وحدات/ملفات كاملة والشجرة الفرعية كاملة.

يعتمد usedExports على terser لاكتشاف الآثار الجانبية في العبارات. وهذه مهمة صعبة في JavaScript وليست بفعالية علم sideEffects المباشر. كما أنه لا يستطيع تخطي الشجرة الفرعية/التبعيات لأن المواصفة تقول إن الآثار الجانبية يجب أن تُقيَّم. وبينما يعمل تصدير الدوال جيدًا، فإن مكونات React ذات الرتبة الأعلى (HOC) تمثل مشكلة في هذا السياق.

إذا كنت تستخدم import() الديناميكي، فيمكنك أيضًا استخدام التعليق السحري webpackExports لتحديد الصادرات التي يجب كشفها، ما يسمح لـ webpack بإزالة الصادرات الأخرى. راجع Magic Comments.

لنأخذ مثالًا:

import { Button } from "@shopify/polaris";

تبدو النسخة المجمعة مسبقًا هكذا:

import hoistStatics from "hoist-non-react-statics";

function Button(_ref) {
  // ...
}

function merge() {
  const _final = {};

  for (
    let _len = arguments.length, objs = Array.from({ length: _len }), _key = 0;
    _key < _len;
    _key++
  ) {
    objs[_key] = arguments[_key];
  }

  for (let _i = 0, _objs = objs; _i < _objs.length; _i++) {
    const obj = _objs[_i];
    mergeRecursively(_final, obj);
  }

  return _final;
}

function withAppProvider() {
  return function addProvider(WrappedComponent) {
    const WithProvider =
      /*#__PURE__*/
      (function (_React$Component) {
        // ...
        return WithProvider;
      })(Component);

    WithProvider.contextTypes = WrappedComponent.contextTypes
      ? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes)
      : polarisAppProviderContextTypes;
    const FinalComponent = hoistStatics(WithProvider, WrappedComponent);
    return FinalComponent;
  };
}

const Button$1 = withAppProvider()(Button);

export {
  // ...,
  Button$1,
};

عندما لا يكون Button مستخدمًا، يمكنك فعليًا إزالة export { Button$1 };، لكن ذلك يترك كل الكود المتبقي. لذلك يصبح السؤال: "هل لهذا الكود أي آثار جانبية، أم يمكن حذفه بأمان؟". من الصعب الجزم، خصوصًا بسبب هذا السطر withAppProvider()(Button). يتم استدعاء withAppProvider، ويتم أيضًا استدعاء القيمة المعادة. هل توجد آثار جانبية عند استدعاء merge أو hoistStatics؟ هل توجد آثار جانبية عند إسناد WithProvider.contextTypes (Setter؟) أو عند قراءة WrappedComponent.contextTypes (Getter؟).

يحاول Terser فعلًا معرفة ذلك، لكنه لا يعرف على وجه اليقين في كثير من الحالات. هذا لا يعني أن terser لا يؤدي عمله جيدًا لأنه لا يستطيع اكتشاف ذلك. الأمر صعب جدًا لتحديده على نحو موثوق في لغة ديناميكية مثل JavaScript.

لكن يمكننا مساعدة terser باستخدام التعليق /*#__PURE__*/. فهو يعلّم العبارة بأنها خالية من الآثار الجانبية. لذلك سيجعل تغيير صغير الكود قابلًا للإزالة عبر tree shaking:

const Button$1 = /* #__PURE__ */ withAppProvider()(Button);

سيسمح ذلك بإزالة هذه القطعة من الكود. لكن ما زالت هناك أسئلة حول الاستيرادات التي يجب تضمينها/تقييمها لأنها قد تحتوي على آثار جانبية.

لمعالجة ذلك، نستخدم خاصية "sideEffects" في package.json.

هي مشابهة لـ /*#__PURE__*/، لكن على مستوى الوحدة بدلًا من مستوى العبارة. تقول خاصية "sideEffects": "إذا لم يُستخدم أي export مباشر من وحدة موسومة بأنها بلا sideEffects، فيمكن للمجمّع تخطي تقييم الوحدة بحثًا عن آثار جانبية".

في مثال Shopify Polaris، تبدو الوحدات الأصلية هكذا:

index.js

import "./configure";

export * from "./types";
export * from "./components";

components/index.js

// ...
export { default as Breadcrumbs } from "./Breadcrumbs";
export { buttonFrom, buttonsFrom, default as Button } from "./Button";
export { default as ButtonGroup } from "./ButtonGroup";
// ...

package.json

// ...
"sideEffects": [
  "**/*.css",
  "**/*.scss",
  "./esnext/index.js",
  "./esnext/configure.js"
],
// ...

بالنسبة إلى import { Button } from "@shopify/polaris";، يترتب على ذلك ما يلي:

  • include it: تضمين الوحدة، وتقييمها، ومتابعة تحليل التبعيات
  • skip over: عدم تضمينها وعدم تقييمها، مع متابعة تحليل التبعيات
  • exclude it: عدم تضمينها وعدم تقييمها وعدم تحليل تبعياتها

وبشكل محدد لكل مورد مطابق:

  • index.js: لا يُستخدم أي export مباشر، لكنه موسوم بـ sideEffects -> تضمينه
  • configure.js: لا يُستخدم أي export، لكنه موسوم بـ sideEffects -> تضمينه
  • types/index.js: لا يُستخدم أي export، وليس موسومًا بـ sideEffects -> استبعاده
  • components/index.js: لا يُستخدم أي export مباشر، وليس موسومًا بـ sideEffects، لكن الصادرات المعاد تصديرها مستخدمة -> تخطيه
  • components/Breadcrumbs.js: لا يُستخدم أي export، وليس موسومًا بـ sideEffects -> استبعاده. يؤدي ذلك أيضًا إلى استبعاد كل التبعيات مثل components/Breadcrumbs.css حتى لو كانت موسومة بـ sideEffects.
  • components/Button.js: export مباشر مستخدم، وليس موسومًا بـ sideEffects -> تضمينه
  • components/Button.css: لا يُستخدم أي export، لكنه موسوم بـ sideEffects -> تضمينه

في هذه الحالة تُضمَّن 4 وحدات فقط في الحزمة:

  • index.js: شبه فارغ
  • configure.js
  • components/Button.js
  • components/Button.css

بعد هذا التحسين، يمكن أن تُطبق تحسينات أخرى أيضًا. على سبيل المثال: الصادرات buttonFrom و buttonsFrom من Button.js غير مستخدمة أيضًا. سيلتقط تحسين usedExports ذلك، وقد يتمكن terser من حذف بعض العبارات من الوحدة.

ينطبق Module Concatenation أيضًا. لذلك يمكن دمج هذه الوحدات الأربع مع وحدة الإدخال (وربما مزيد من التبعيات). في النهاية لا يُولّد أي كود لـ index.js.

مثال كامل: فهم الآثار الجانبية مع ملفات CSS

لفهم تأثير علم sideEffects بشكل أفضل، لننظر إلى مثال كامل لحزمة npm تحتوي على أصول CSS وكيف قد تتأثر أثناء tree shaking. سننشئ مكتبة مكونات واجهة وهمية باسم "awesome-ui".

بنية الحزمة

تبدو حزمة المثال لدينا هكذا:

awesome-ui/
├── package.json
└── dist/
    ├── index.js
    ├── components/
    │   ├── index.js
    │   ├── Button/
    │   │   ├── index.js
    │   │   └── Button.css
    │   ├── Card/
    │   │   ├── index.js
    │   │   └── Card.css
    │   └── Modal/
    │       ├── index.js
    │       └── Modal.css
    └── theme/
        ├── index.js
        └── defaultTheme.css

محتوى ملفات الحزمة

package.json

{
  "name": "awesome-ui",
  "version": "1.0.0",
  "main": "dist/index.js",
  "sideEffects": false
}

dist/index.js

export * from "./components";
export * from "./theme";

dist/components/index.js

export { default as Button } from "./Button";
export { default as Card } from "./Card";
export { default as Modal } from "./Modal";

dist/components/Button/index.js

import "./Button.css"; // لهذا الاستيراد أثر جانبي: فهو يطبّق الأنماط عند استيراده!

export default function Button(props) {
  // تنفيذ مكوّن Button
  return {
    type: "button",
    ...props,
  };
}

dist/components/Button/Button.css

.awesome-ui-button {
  background-color: #0078d7;
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  border: none;
  cursor: pointer;
}

سيكون لكل من dist/components/Card/index.js و dist/components/Modal/index.js بنية مشابهة.

dist/theme/index.js

import "./defaultTheme.css"; // لهذا الاستيراد أثر جانبي!

export const themeColors = {
  primary: "#0078d7",
  secondary: "#f3f2f1",
  danger: "#d13438",
};

ماذا يحدث عند استهلاك هذه الحزمة؟

تخيل الآن تطبيقًا مستهلكًا يريد استخدام مكوّن Button فقط:

import { Button } from "awesome-ui";

// استخدام مكوّن Button

مع sideEffects: false في package.json

عندما يعالج webpack هذا الاستيراد مع تفعيل tree shaking:

  1. يرى أن الاستيراد يخص Button فقط
  2. ينظر إلى package.json ويرى sideEffects: false
  3. يحدد أنه يحتاج إلى تضمين كود مكوّن Button فقط
  4. بما أن كل الملفات موسومة بأنها بلا آثار جانبية، فسيضمّن فقط كود JavaScript الخاص بـ Button
  5. سيُحذف استيراد ملف CSS! رغم أن Button.css مستورد في Button/index.js، يفترض webpack أن هذا الاستيراد لا يملك آثارًا جانبية.

النتيجة: سيُصيَّر مكوّن Button، لكن دون أي تنسيق لأن Button.css أُزيل أثناء tree shaking.

الإعداد الصحيح لهذه الحزمة

لإصلاح ذلك، يجب تحديث package.json لوسم ملفات CSS بشكل صحيح على أنها ذات آثار جانبية:

{
  "name": "awesome-ui",
  "version": "1.0.0",
  "main": "dist/index.js",
  "sideEffects": ["**/*.css"]
}

مع هذا الإعداد:

  1. ما زال webpack يحدد أن مكوّن Button فقط مطلوب
  2. لكنه يدرك الآن أن ملفات CSS لها آثار جانبية
  3. لذلك يضمّن Button.css عند معالجة Button/index.js

شجرة القرار للآثار الجانبية

هكذا يقيّم webpack الوحدات أثناء tree shaking:

  1. هل يُستخدم export من هذه الوحدة بشكل مباشر أو غير مباشر؟

    • إذا نعم: ضمّن الوحدة
    • إذا لا: انتقل إلى الخطوة 2
  2. هل الوحدة موسومة بأنها ذات آثار جانبية؟

    • إذا نعم (sideEffects يتضمن هذا الملف أو قيمته true): ضمّن الوحدة
    • إذا لا (sideEffects قيمته false أو لا يتضمن هذا الملف): استبعد الوحدة وتبعياتها

بالنسبة إلى ملفات مكتبتنا مع إعداد sideEffects الصحيح:

  • dist/index.js: لا يُستخدم export مباشر، ولا آثار جانبية -> تخطيه
  • dist/components/index.js: لا يُستخدم export مباشر، ولا آثار جانبية -> تخطيه
  • dist/components/Button/index.js: export مباشر مستخدم -> تضمينه
  • dist/components/Button/Button.css: لا توجد exports، وله آثار جانبية -> تضمينه
  • dist/components/Card/*: لا توجد exports مستخدمة، ولا آثار جانبية -> استبعاده
  • dist/components/Modal/*: لا توجد exports مستخدمة، ولا آثار جانبية -> استبعاده
  • dist/theme/*: لا توجد exports مستخدمة، ولا آثار جانبية -> استبعاده

الأثر في العالم الحقيقي

قد يكون تأثير الإعداد الخاطئ للآثار الجانبية كبيرًا:

  1. عدم تضمين CSS: تُصيَّر المكونات دون أنماط
  2. عدم تشغيل JavaScript العام: لا تُنفذ polyfills أو الإعدادات العامة
  3. تخطي كود التهيئة: لا تعمل الدوال التي تسجل المكونات أو تجهّز مستمعي الأحداث

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

اختبار إعداد الآثار الجانبية

طريقة جيدة لاختبار صحة إعداد الآثار الجانبية:

  1. أنشئ تطبيقًا بسيطًا يستورد مكوّنًا واحدًا فقط
  2. ابنِه بإعدادات الإنتاج (مع تفعيل tree shaking)
  3. تحقق من أن كل الأنماط والسلوكيات الضرورية تعمل بشكل صحيح
  4. انظر إلى الحزمة الناتجة للتأكد من تضمين الملفات الصحيحة

وسم استدعاء دالة بأنه خالٍ من الآثار الجانبية

يمكن إخبار webpack بأن استدعاء دالة خالٍ من الآثار الجانبية (نقي) باستخدام التعليق /*#__PURE__*/. يمكن وضعه قبل استدعاءات الدوال لوسمها بأنها خالية من الآثار الجانبية. لا تُوسَم الوسائط الممررة إلى الدالة بهذا التعليق وقد تحتاج إلى وسمها بشكل منفصل. عندما تُعد القيمة الابتدائية في تصريح متغير غير مستخدم خالية من الآثار الجانبية (نقية)، تُوسَم ككود ميت، ولا تُنفذ، ويحذفها المصغّر. يُفعَّل هذا السلوك عندما تكون optimization.innerGraph مضبوطة على true.

file.js

/* #__PURE__ */ double(55);

وسم تصريح دالة بأنه خالٍ من الآثار الجانبية

5.107.0+

يدعم Webpack أيضًا التعليق #__NO_SIDE_EFFECTS__ لوسم تصريح دالة بأنه نقي. يمكن حذف استدعاءات الدالة الموسومة بهذه الطريقة من الحزمة عندما تكون قيمتها المعادة غير مستخدمة، حتى لو لم يكن جسم الدالة قابلًا للتحليل الساكن على أنه نقي. هذا مفيد لدوال المصنع أو البناء التي كانت مواقع استدعائها ستحتاج بخلاف ذلك إلى تعليق /*#__PURE__*/ في كل مرة.

// utils.js
/*#__NO_SIDE_EFFECTS__*/
export function createLogger(prefix) {
  return (msg) => console.log(`[${prefix}] ${msg}`);
}
// app.js
import { createLogger } from "./utils";

// يُحذف لأن `createLogger` موسومة وقيمتها الناتجة غير مستخدمة
const unused = createLogger("debug");

تصغير المخرجات

لقد جهّزنا "الكود الميت" ليُحذف باستخدام صيغتي import و export، لكن ما زلنا بحاجة إلى حذفه من الحزمة. لفعل ذلك، اضبط خيار الإعداد mode على production.

webpack.config.js

import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
- mode: 'development',
- optimization: {
-   usedExports: true,
- }
+ mode: 'production',
};

بعد ذلك، يمكننا تشغيل npm run build مرة أخرى ومعرفة ما إذا كان أي شيء قد تغير.

هل لاحظت شيئًا مختلفًا في dist/bundle.js؟ أصبحت الحزمة كلها مصغّرة ومشوّهة الأسماء، لكن إذا نظرت بعناية فلن ترى دالة square مضمنة، وسترى نسخة مشوّهة من دالة cube (function r(e){return e*e*e}n.a=r). مع التصغير و tree shaking، أصبحت حزمتنا أصغر ببضعة بايتات! قد لا يبدو ذلك كثيرًا في هذا المثال المصطنع، لكن tree shaking يمكن أن يحقق انخفاضًا ملحوظًا في حجم الحزمة عند العمل على تطبيقات أكبر ذات أشجار تبعيات معقدة.

مشكلات شائعة مع الآثار الجانبية

عند العمل مع tree shaking وعلم sideEffects، توجد عدة مشكلات شائعة يجب تجنبها:

1. التفاؤل الزائد مع sideEffects: false

تعيين sideEffects: false في package.json مغرٍ للحصول على tree shaking مثالي، لكن هذا قد يسبب مشكلات إذا كان كودك يحتوي فعلًا على آثار جانبية. أمثلة على آثار جانبية مخفية:

  • استيرادات CSS (كما أوضحنا أعلاه)
  • Polyfills التي تعدّل الكائنات العامة
  • مكتبات تسجل مستمعي أحداث عامة
  • كود يعدّل سلاسل prototype

2. إعادة التصدير مع آثار جانبية

تأمل هذا النمط:

// هذا الملف له آثار جانبية قد يتم تخطيها
import "./polyfill";

// إعادة تصدير المكونات
export * from "./components";

إذا استورد مستهلك مكونات محددة فقط، فقد يتم تخطي استيراد polyfill بالكامل إذا لم يُوسَم بشكل صحيح بأنه ذو آثار جانبية.

3. نسيان التبعيات المتداخلة

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

4. الاختبار في وضع التطوير فقط

عادةً لا يتفعّل tree shaking بالكامل إلا في وضع الإنتاج. الاختبار في التطوير فقط قد يخفي مشكلات tree shaking إلى حين النشر.

الخاتمة

ما تعلمناه هو أنه للاستفادة من tree shaking، يجب عليك:

  • استخدام صيغة وحدات ES2015 (أي import و export).
  • التأكد من أن أي مصرّفات لا تحوّل صيغة وحدات ES2015 إلى وحدات CommonJS (هذا هو السلوك الافتراضي للحزمة الشائعة Babel preset @babel/preset-env؛ راجع التوثيق لمزيد من التفاصيل).
  • إضافة خاصية "sideEffects" إلى ملف package.json الخاص بمشروعك.
  • الحذر عند وسم الملفات ذات الآثار الجانبية بشكل صحيح، خصوصًا استيرادات CSS.
  • استخدام خيار الإعداد production في mode لتفعيل تحسينات متنوعة، بما في ذلك التصغير و tree shaking (يتم تفعيل تحسين الآثار الجانبية في وضع التطوير باستخدام قيمة العلم).
  • التأكد من تعيين قيمة صحيحة لـ devtool، إذ لا يمكن استخدام بعض القيم في وضع production.

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

إذا كنت مهتمًا بمزيد من طرق تحسين المخرجات، فانتقل إلى الدليل التالي لمزيد من التفاصيل حول البناء من أجل الإنتاج.

Edit this page·

1 Contributor

arabpolice