关于前端:CommonJS-是如何导致打包后体积增大的

30次阅读

共计 4601 个字符,预计需要花费 12 分钟才能阅读完成。

明天的文章,将介绍什么是 CommonJS,以及它为什么会导致咱们打包后的文件体积增大。

本文概要:为了确保打包工具(webpack 之类的)可能对你的我的项目代码进行优化,请防止在我的项目中应用 CommonJS 模块,并且整个我的项目都应该应用 ESM(ECMAScript Module)的模块语法。

什么是 CommonJS?

CommonJS 是 2009 年公布的 JavaScript 模块化的一项规范,最后它只打算在浏览器之外的场景应用,次要用于服务器端的应用程序。

你能够应用 CommonJS 来定义模块,并从中导出局部模块。例如,上面的代码定义了一个模块,该模块导出了五个函数:addsubtractmultiplydividemax:

// 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');
console.log(add(1, 2));

通过 node 运行 index.js,会在控制台输入数字 3

在 2010 年,因为浏览器不足标准化的模块化能力,CommonJS 成了过后 JavaScript 客户端较为风行的模块化规范。

CommonJS 如何影响包体?

服务端的 JavaScript 程序对代码体积并不像浏览器中那么敏感,这就是为什么在设计 CommonJS 的时候,并没有思考缩小生产包大小的起因。同时,研表究明 JavaScript 代码的体积仍然是影响页面加载速度的一个重要因素。

JavaScript 的打包工具(webpackterser)会进行许多优化以减小最初生成的包体大小。他们在构建时,会剖析你的代码,尽可能的删除不会应用的局部。例如,下面的代码中,最终生成的包应该只蕴含 add 函数,因为这是 index.js 惟一从 utils.js 中导入的局部。

上面咱们应用如下 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,能够通过如下形式统计它的大小:

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

打包后的文件高达 625 KB。如果看下 out.js 文件,会发现 utils.js 导入 lodash 的所有模块都打包到了输入的文件中,只管咱们在 index.js 并没有应用到 lodash 的任何办法,然而这给咱们的包体带来了微小的影响。

当初咱们将代码的模块化计划改为 ESM,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 也改为 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 的模块,则会收到正告。

正文完
 0