CommonJS 是如何致使打包後體積增大的?

今天的文章,將介紹什麼是 CommonJS,以及它爲何會致使咱們打包後的文件體積增大。javascript

本文概要:爲了確保打包工具(webpack之類的)可以對你的項目代碼進行優化,請避免在項目中使用 CommonJS 模塊,而且整個項目都應該使用 ESM(ECMAScript Module) 的模塊語法。

什麼是 CommonJS?

CommonJS 是 2009 年發佈的 JavaScript模塊化的一項標準,最初它只打算在瀏覽器以外的場景使用,主要用於服務器端的應用程序。java

你可使用 CommonJS 來定義模塊,並從中導出部分模塊。例如,下面的代碼定義了一個模塊,該模塊導出了五個函數:addsubtractmultiplydividemax:node

// 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]);

其餘模塊能夠導入這個模塊的部分函數。webpack

// index.js
const { add } = require(‘./utils');
console.log(add(1, 2));

經過 node 運行 index.js ,會在控制檯輸出數字 3git

在 2010 年,因爲瀏覽器缺少標準化的模塊化能力,CommonJS 成了當時 JavaScript 客戶端較爲流行的模塊化標準。github

CommonJS 如何影響包體?

服務端的 JavaScript 程序對代碼體積並不像瀏覽器中那麼敏感,這就是爲何在設計 CommonJS 的時候,並無考慮減小生產包大小的緣由。同時,研表究明 JavaScript 代碼的體積依然是影響頁面加載速度的一個重要因素。web

JavaScript 的打包工具(webpackterser)會進行許多優化以減少最後生成的包體大小。他們在構建時,會分析你的代碼,儘量的刪除不會使用的部分。例如,上面的代碼中,最終生成的包應該只包含 add 函數,由於這是 index.js 惟一從 utils.js 中導入的部分。npm

下面咱們使用以下 webpack 配置對應用進行打包:瀏覽器

const path = require('path');
module.exports = {
  entry: 'index.js',
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'production',
};

咱們須要將 webpackmode 指定爲 production,而且將 index.js 作爲入口。運行 webpack 後,會輸出一個文件:dist/out.js,能夠經過以下方式統計它的大小:bash

$ cd dist && ls -lah
625K Apr 13 13:04 out.js

打包後的文件高達 625 KB。若是看下 out.js 文件,會發現 utils.js 導入 lodash 的全部模塊都打包到了輸出的文件中,儘管咱們在 index.js 並無使用到 lodash 的任何方法,可是這給咱們的包體帶來了巨大的影響。

如今咱們將代碼的模塊化方案改成 ESMutils.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 也改成 ESM 的方式從 utils.js 導入模塊:

import { add } from './utils';

console.log(add(1, 2));

使用相同的 webpack 配置,構建完畢以後,咱們打開 out.js僅有 40 字節,輸出以下:

(()=>{"use strict";console.log(1+2)})();

值得注意的是,最終的輸出並無包含 utils.js 的任何代碼,並且 lodash 也消失了。並且 terserwebpack 使用的壓縮工具)直接將 add 函數內聯到了 console.log 內部。

有的小朋友可能就會問了(此處採用了李永樂語法),爲何使用 CommonJS 會致使輸出的文件大了 16,000 倍?固然,這只是用來展現 CommonJS 與 ESM 差別的案例,實際上並不會出現這麼大的差別,可是使用 CommonJS 確定會致使打包後的體積更大。

通常狀況下,CommonJS 模塊的體積更加難優化,由於它比 ES 模塊更加的動態化。爲了確保構建工具以及壓縮工具能成功優化代碼,請避免使用 CommonJS 模塊。

固然,若是你只在 utils.js 採用了 ESM 的模塊化方案,而 index.js 仍是維持 CommonJS,則包體依舊會受到影響。

爲何 CommonJS 會使包體更大?

要回答這個問題,咱們須要研究 webpackModuleConcatenationPlugin 的行爲,而且看看它是如何進行靜態分析的。該插件將全部的模塊都放入一個閉包內,這會讓你的代碼在瀏覽器中更快的執行。咱們來看看下面的代碼:

// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// index.js
import { add } from ‘./utils';
const subtract = (a, b) => a - b;

console.log(add(1, 2));

咱們有一個新的 ESM 模塊(utils.js),將其導入 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',
};

輸出的 out.js 以下:

/******/ (() => { // 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));

/******/ })();

輸出的代碼中,全部的函數都在一個命名空間裏,爲了防止衝突,webpackindex.js 中的 subtract 函數從新命名爲了 index_subtract 函數。

若是開啓壓縮配置,它會進行以下操做:

  1. 刪除沒有使用的 subtract 函數和 index_subtract 函數;
  2. 刪除全部的註釋和空格;
  3. console.log 中直接內聯 add 函數;

一些開發人員會把這種刪除未使用代碼的行爲稱爲「tree-shaking(樹搖)」。webpack 可以經過導出、導入符號靜態的分析 utils.js(在構建的過程當中),這使得 tree-shaking 有了可行性。當使用 ESM 時,這種行爲是默認開啓的,由於相比於 CommonJS,它更加易於靜態分析。

讓咱們看看另外的示例,這一次將 utils.js 改成 CommonJS 模塊,而不是 ESM 模塊。

// 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));

})();

能夠看到,最終生成的代碼包含一些 webpackruntime 代碼,這部分代碼負責模塊的導入導出的能力。此次並無將 utils.jsindex.js 全部的變量放到了同一命名空間下,動態引入的模塊都是經過 __webpack_require__ 進行導入。

使用 CommonJS 的時候,咱們能夠經過任意的表達式構造導出名稱,例以下面的代碼也是能正常運行的:

module.exports[(Math.random()] = () => { … };

這致使構建工具在構建時,沒有辦法知道導出的變量名,由於這個名稱只有在用戶瀏覽器運行時纔可以真正肯定。壓縮工具沒法準確的知道 index.js 使用了模塊的哪部份內容,所以沒法正確的進行 tree-shaking。若是咱們從 node_modules 導入了 CommonJS 模塊,你的構建工具將沒法正確的優化它。

對 CommonJS 使用 Tree-shaking

因爲 CommonJS 的模塊化方案是動態的,想要分析他們是特別困難的。與經過表達式導入模塊的 CommonJS 相比,ESM 模塊的導入始終使用的是靜態的字符串文本。

在某些狀況下,若是你使用的庫遵循 CommonJS 的相關的一些約定,你可使用第三方的 webpack 插件:webpack-common-shake,在構建的過程當中,刪除未使用的模塊。儘管該插件增長了 CommonJS 對 tree-shaking 的支持,但並無涵蓋全部的 CommonJS 依賴,這意味着你不能得到 ESM 相同的效果。

此外,這並不是是 webpack 默認行爲,它會對你的構建耗時增長額外的成本。

總結

爲了確保構建工具對你的代碼儘量的進行優化,請避免使用 CommonJS 模塊,並在整個項目中使用 ESM 語法。

下面是一些檢驗你的項目是不是最佳實踐的方法:

  • 使用 Rollup.js 提供的 node-resolve 插件,並開啓 modulesOnly 選項,表示你的項目只會使用 ESM。
  • 使用 is-esm 來驗證 npm 安裝的模塊是否使用 ESM。
  • 若是您使用的是Angular,默認狀況下,若是你依賴了不能進行 tree-shaking 的模塊,則會收到警告。

image

相關文章
相關標籤/搜索