بیاموزید که چگونه ماژول های CommonJS بر تکان دادن درخت برنامه شما تأثیر می گذارد
در این پست به بررسی CommonJS خواهیم پرداخت و چرا بستههای جاوا اسکریپت شما را بزرگتر از حد لازم میکند.
خلاصه: برای اطمینان از اینکه باندلر می تواند برنامه شما را با موفقیت بهینه کند، از وابستگی به ماژول های CommonJS اجتناب کنید و از نحو ماژول ECMAScript در کل برنامه خود استفاده کنید.
CommonJS چیست؟
CommonJS استانداردی از سال 2009 است که کنوانسیون هایی را برای ماژول های جاوا اسکریپت ایجاد کرده است. در ابتدا برای استفاده در خارج از مرورگر وب، در درجه اول برای برنامه های سمت سرور در نظر گرفته شده بود.
با CommonJS میتوانید ماژولها را تعریف کنید، عملکردها را از آنها صادر کنید و آنها را در ماژولهای دیگر وارد کنید. به عنوان مثال، قطعه زیر ماژولی را تعریف می کند که پنج تابع را صادر می کند: add
، subtract
، multiply
، divide
و max
:
// utils.js
const { maxBy } = require('lodash-es');
const fns = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);
بعداً، ماژول دیگری می تواند برخی یا همه این توابع را وارد و استفاده کند:
// index.js
const { add } = require('./utils.js');
console.log(add(1, 2));
فراخوانی index.js
با node
، عدد 3
را در کنسول خروجی می دهد.
به دلیل عدم وجود یک سیستم ماژول استاندارد در مرورگر در اوایل دهه 2010، CommonJS به یک قالب ماژول محبوب برای کتابخانه های سمت کلاینت جاوا اسکریپت نیز تبدیل شد.
CommonJS چگونه بر اندازه نهایی بسته نرم افزاری شما تأثیر می گذارد؟
اندازه برنامه جاوا اسکریپت سمت سرور شما به اندازه مرورگر مهم نیست، به همین دلیل CommonJS با در نظر گرفتن کاهش اندازه بسته تولید طراحی نشده است. در عین حال، تجزیه و تحلیل نشان می دهد که اندازه بسته نرم افزاری جاوا اسکریپت هنوز هم دلیل شماره یک برای کندتر کردن برنامه های مرورگر است.
بستهکنندهها و کوچککنندههای جاوا اسکریپت، مانند webpack
و terser
، بهینهسازیهای مختلفی را برای کاهش اندازه برنامه شما انجام میدهند. با تجزیه و تحلیل برنامه شما در زمان ساخت، آنها سعی می کنند تا حد امکان از کد منبعی که شما استفاده نمی کنید حذف کنند.
به عنوان مثال، در قطعه بالا، بسته نهایی شما باید فقط شامل تابع add
باشد زیرا این تنها نمادی از utils.js
است که در index.js
وارد میکنید.
بیایید برنامه را با استفاده از پیکربندی webpack
زیر بسازیم:
const path = require('path');
module.exports = {
entry: 'index.js',
output: {
filename: 'out.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'production',
};
در اینجا مشخص می کنیم که می خواهیم از بهینه سازی های حالت تولید استفاده کنیم و از index.js
به عنوان نقطه ورود استفاده کنیم. پس از فراخوانی webpack
، اگر اندازه خروجی را بررسی کنیم، چیزی شبیه به این خواهیم دید:
$ cd dist && ls -lah
625K Apr 13 13:04 out.js
توجه داشته باشید که بسته نرم افزاری 625 کیلوبایت است . اگر به خروجی نگاه کنیم، تمام توابع utils.js
به اضافه ماژول های زیادی از lodash
را پیدا خواهیم کرد . اگرچه ما از lodash
در index.js
استفاده نمی کنیم، اما بخشی از خروجی است که وزن اضافی زیادی به دارایی های تولید ما اضافه می کند.
حالا اجازه دهید قالب ماژول را به ماژول های ECMAScript تغییر دهیم و دوباره امتحان کنیم. این بار، utils.js
به شکل زیر خواهد بود:
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;
import { maxBy } from 'lodash-es';
export const max = arr => maxBy(arr);
و index.js
با استفاده از سینتکس ماژول ECMAScript از utils.js
وارد می شود:
import { add } from './utils.js';
console.log(add(1, 2));
با استفاده از همان پیکربندی webpack
، می توانیم برنامه خود را بسازیم و فایل خروجی را باز کنیم. اکنون 40 بایت با خروجی زیر است:
(()=>{"use strict";console.log(1+2)})();
توجه داشته باشید که بسته نهایی حاوی هیچ یک از توابع utils.js
نیست که ما از آنها استفاده نمی کنیم، و هیچ اثری از lodash
وجود ندارد! حتی بیشتر از آن، terser
(مینیفایر جاوا اسکریپت که webpack
از آن استفاده میکند) تابع add
را در console.log
گنجانده است.
یک سوال منصفانه که ممکن است بپرسید این است که چرا استفاده از CommonJS باعث می شود بسته خروجی تقریبا 16000 برابر بزرگتر شود ؟ البته، این یک نمونه اسباب بازی است، در واقعیت، تفاوت اندازه ممکن است زیاد نباشد، اما به احتمال زیاد CommonJS وزن قابل توجهی به ساخت تولید شما اضافه می کند.
بهینه سازی ماژول های CommonJS در حالت کلی سخت تر است زیرا پویاتر از ماژول های ES هستند. برای اطمینان از اینکه باندلر و مینیفایر شما میتوانند با موفقیت برنامه شما را بهینه کنند، از وابستگی به ماژولهای CommonJS خودداری کنید و از نحو ماژول ECMAScript در کل برنامه خود استفاده کنید.
توجه داشته باشید که حتی اگر از ماژولهای ECMAScript در index.js
استفاده میکنید، اگر ماژولی که مصرف میکنید یک ماژول CommonJS باشد، اندازه بسته نرم افزاری شما آسیب خواهد دید.
چرا CommonJS برنامه شما را بزرگتر می کند؟
برای پاسخ به این سوال، ما به رفتار ModuleConcatenationPlugin
در webpack
نگاه می کنیم و پس از آن، قابلیت تجزیه و تحلیل استاتیک را مورد بحث قرار می دهیم. این افزونه دامنه همه ماژول های شما را در یک بسته به هم متصل می کند و به کد شما اجازه می دهد زمان اجرای سریع تری در مرورگر داشته باشد. بیایید به یک مثال نگاه کنیم:
// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// index.js
import { add } from './utils.js';
const subtract = (a, b) => a - b;
console.log(add(1, 2));
در بالا، ما یک ماژول ECMAScript داریم که آن را در index.js
وارد می کنیم. یک تابع subtract
هم تعریف می کنیم. ما میتوانیم پروژه را با استفاده از همان پیکربندی webpack
مانند بالا بسازیم، اما این بار، حداقل سازی را غیرفعال میکنیم:
const path = require('path');
module.exports = {
entry: 'index.js',
output: {
filename: 'out.js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
minimize: false
},
mode: 'production',
};
اجازه دهید به خروجی تولید شده نگاه کنیم:
/******/ (() => { // webpackBootstrap
/******/ "use strict";
// CONCATENATED MODULE: ./utils.js**
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// CONCATENATED MODULE: ./index.js**
const index_subtract = (a, b) => a - b;**
console.log(add(1, 2));**
/******/ })();
در خروجی بالا، همه توابع در فضای نام یکسانی قرار دارند. برای جلوگیری از برخورد، webpack تابع subtract
در index.js
را به index_subtract
تغییر نام داد.
اگر یک Minifier کد منبع بالا را پردازش کند، این کار را انجام می دهد:
- توابع استفاده نشده
subtract
وindex_subtract
حذف کنید - تمام نظرات و فضای خالی اضافی را حذف کنید
- متن تابع
add
را در فراخوانیconsole.log
درون خطی کنید
اغلب توسعه دهندگان از این حذف واردات بلااستفاده به عنوان تکان دادن درخت یاد می کنند. تکان دادن درخت تنها به این دلیل امکان پذیر بود که webpack قادر بود به صورت ایستا (در زمان ساخت) بفهمد کدام نمادها را از utils.js
وارد می کنیم و چه نمادهایی را صادر می کند.
این رفتار بهطور پیشفرض برای ماژولهای ES فعال است، زیرا در مقایسه با CommonJS ، از نظر استاتیکی بیشتر قابل تجزیه و تحلیل هستند .
اجازه دهید دقیقاً به همان مثال نگاه کنیم، اما این بار utils.js
را تغییر دهید تا از CommonJS به جای ماژولهای ES استفاده کنید:
// utils.js
const { maxBy } = require('lodash-es');
const fns = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);
این به روز رسانی کوچک به طور قابل توجهی خروجی را تغییر می دهد. از آنجایی که برای جاسازی در این صفحه بسیار طولانی است، من فقط بخش کوچکی از آن را به اشتراک گذاشته ام:
...
(() => {
"use strict";
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(288);
const subtract = (a, b) => a - b;
console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .add */ .IH)(1, 2));
})();
توجه داشته باشید که بسته نهایی حاوی مقداری "زمان اجرا" webpack
است: کد تزریقی که مسئول وارد کردن/صادرات عملکرد از ماژول های همراه است. این بار، به جای قرار دادن تمام نمادهای utils.js
و index.js
در یک فضای نام، به صورت پویا، در زمان اجرا، تابع add
را با استفاده از __webpack_require__
نیاز داریم.
این ضروری است زیرا با CommonJS می توانیم نام صادرات را از یک عبارت دلخواه دریافت کنیم. به عنوان مثال، کد زیر یک ساختار کاملاً معتبر است:
module.exports[localStorage.getItem(Math.random())] = () => { … };
هیچ راهی برای بستهکننده وجود ندارد که در زمان ساخت بداند نام نماد صادر شده چیست، زیرا این به اطلاعاتی نیاز دارد که فقط در زمان اجرا و در زمینه مرورگر کاربر در دسترس باشد.
به این ترتیب، مینیفایر نمیتواند بفهمد index.js
دقیقاً چه چیزی را از وابستگیهای خود استفاده میکند، بنابراین نمیتواند آن را با درخت تکان دهد. ما دقیقاً همین رفتار را برای ماژول های شخص ثالث نیز مشاهده خواهیم کرد. اگر یک ماژول CommonJS را از node_modules
وارد کنیم، زنجیره ابزار ساخت شما نمی تواند آن را به درستی بهینه کند.
تکان دادن درخت با CommonJS
تجزیه و تحلیل ماژول های CommonJS بسیار سخت تر است زیرا آنها طبق تعریف پویا هستند. به عنوان مثال، مکان واردات در ماژولهای ES در مقایسه با CommonJS که یک عبارت است، همیشه یک رشته واقعی است.
در برخی موارد، اگر کتابخانهای که استفاده میکنید از قراردادهای خاصی در مورد نحوه استفاده از CommonJS پیروی میکند، میتوانید در زمان ساخت با استفاده از یک افزونه webpack
شخص ثالث، صادرات بلااستفاده را حذف کنید. اگرچه این افزونه پشتیبانی از تکان دادن درخت را اضافه می کند، اما تمام راه های مختلفی که وابستگی های شما می توانند از CommonJS استفاده کنند را پوشش نمی دهد. این بدان معنی است که شما تضمین های مشابه با ماژول های ES را دریافت نمی کنید. علاوه بر این، هزینه اضافی را به عنوان بخشی از فرآیند ساخت شما در بالای رفتار webpack
پیشفرض اضافه میکند.
نتیجه گیری
برای اطمینان از اینکه باندلر می تواند برنامه شما را با موفقیت بهینه کند، از وابستگی به ماژول های CommonJS اجتناب کنید و از نحو ماژول ECMAScript در کل برنامه خود استفاده کنید.
در اینجا چند نکته قابل اجرا برای تأیید اینکه در مسیر بهینه هستید آورده شده است:
- از پلاگین Node-resolve Rollup.js استفاده کنید و پرچم
modulesOnly
را طوری تنظیم کنید که مشخص کنید میخواهید فقط به ماژولهای ECMAScript وابسته باشید. - از بسته
is-esm
برای تأیید اینکه یک بسته npm از ماژول های ECMAScript استفاده می کند استفاده کنید. - اگر از Angular استفاده می کنید، اگر به ماژول های غیرقابل تکان دادن درخت وابسته باشید، به طور پیش فرض یک اخطار دریافت خواهید کرد.