原文链接:https://web.dev/commonjs-larger-bundles今天的文章将介绍什么是CommonJS以及为什么它会增加我们打包文件的大小。什么是CommonJS?CommonJS是2009年发布的JavaScript模块化标准,最初只是打算用于浏览器以外的场景,主要是服务端应用。您可以使用CommonJS来定义模块并导出其中的一部分。例如,下面的代码定义了一个导出五个函数的模块:add,subtract,multiply,divide,max://utils.jsconst{maxBy}=require('lodash-es');constfns={add:(a,b)=>a+b,减法:(a,b)=>a-b,乘法:(a,b)=>a*b,除法:(a,b)=>a/b,max:arr=>maxBy(arr)};Object.keys(fns).forEach(fnName=>module.exports[fnName]=fns[fnName]);其他模块可以导入本模块的部分功能。//index.jsconst{add}=require('./utils');控制台日志(添加(1,2));通过node运行index.js会在控制台输出数字3。2010年,由于浏览器缺乏标准化的模块化能力,CommonJS成为当时JavaScript客户端比较流行的模块化标准。CommonJS如何影响包体?服务器端JavaScript程序对代码大小不像在浏览器中那样敏感,这就是为什么CommonJS没有设计来减少生产包大小的原因。同时,研究表明,JavaScript代码的大小仍然是影响页面加载速度的重要因素。JavaScript打包工具(webpack、terser)会进行许多优化以减少最终包的大小。他们在构建的时候,会分析你的代码,把不会用到的部分尽量删掉。例如,在上面的代码中,最终生成的bundle应该只包含add函数,因为这是从utils.js导入的index.js的唯一部分。下面我们使用如下的webpack配置来打包应用:constpath=require('path');module.exports={entry:'index.js',output:{filename:'out.js',path:path.resolve(__dirname,'dist'),},mode:'production',};我们需要指定webpack的模式为production,以index.js作为入口。运行webpack后会输出一个文件:dist/out.js,可以通过以下方法统计其大小:$cddist&&ls-lah625KAPr1313:04out.js打包后的文件最大625KB。如果你查看out.js文件,你会发现utils.js导入到lodash中的所有模块都被打包到输出文件中。虽然我们没有在index.js中使用lodash的任何方法,但这给我们的包体带来了巨大的影响。现在我们将代码的模块化方案改为ESM,utils.js部分的代码如下:exportconstadd=(a,b)=>a+b;exportconstsubtract=(a,b)=>a-b;exportconstmultiply=(a,b)=>a*b;exportconstdivide=(a,b)=>a/b;import{maxBy}from'lodash-es';exportconstmax=arr=>maxBy(arr);index.js也是改为ESMImportmodulefromutils.js:import{add}from'./utils';console.log(add(1,2));使用同样的webpack配置,构建完成后,我们打开out.js,只有40words部分,输出如下:(()=>{"usestrict";console.log(1+2)})();值得注意的是,最终的输出不包含任何utils.js的代码,lodash也消失了。而terser(webpack使用的压缩工具)直接将add函数内联到console.log中。可能有小朋友会问(这里用的是李永乐语法),为什么使用CommonJS会导致输出文件大16000倍?当然,这只是一个例子来展示CommonJS和ESM之间的区别,实际上并没有那么大的区别,但是使用CommonJS肯定会导致包体积变大。一般来说,CommonJS模块的大小更难优化,因为它比ES模块更动态。为确保构建工具和压缩器能够成功优化您的代码,请避免使用CommonJS模块。当然,如果你只在utils.js中使用ESM模块化方案,而index.js仍然维护CommonJS,包体还是会受到影响。为什么CommonJS让包更大?要回答这个问题,我们需要研究一下webpack的ModuleConcatenationPlugin的行为,看看它是如何进行静态分析的。该插件将所有模块放入一个闭包中,这将使您的代码在浏览器中执行得更快。我们来看看下面的代码://utils.jsexportconstadd=(a,b)=>a+b;exportconstsubtract=(a,b)=>a-b;//index.jsimport{add}from'./utils';constsubtract=(a,b)=>a-b;console.log(add(1,2));我们有一个新的ESM模块(utils.js),将它导入到index.js中,我们还重新定义了一个减法函数。接下来使用之前的webpack配置来构建项目,不过这次我将禁用压缩配置。constpath=require('path');module.exports={entry:'index.js',output:{filename:'out.js',path:path.resolve(__dirname,'dist'),},+优化:{+最小化:false+},mode:'production',};输出out.js如下:/******/(()=>{//webpackBootstrap/******/"usestrict";//CONCATENATEDMODULE:./utils.js**constadd=(a,b)=>a+b;constsubtract=(a,b)=>a-b;//CONCATENATEDMODULE:./index.js**constindex_subtract=(a,b)=>a-b;console.log(add(1,2));/******/})();在输出代码中,所有函数都在一个命名空间中,为了防止冲突,webpack将index.js中的subtract函数重命名为index_subtract函数。如果开启压缩配置,会执行以下操作:删除不用的subtract函数和index_subtract函数;删除所有评论和空格;直接在console.log中内联add函数;一些开发人员将删除这种未使用的代码的行为称为“tree-shaking”。webpack可以通过导出和导入符号来静态分析utils.js(在构造期间),这使得tree-shaking成为可能。使用ESM时默认启用此行为,因为它比CommonJS更容易进行静态分析。让我们看另一个示例,这次将utils.js更改为CommonJS模块而不是ESM模块。//utils.jsconst{maxBy}=require('lodash-es');constfns={add:(a,b)=>a+b,subtract:(a,b)=>a-b,multiply:(a,b)=>a*b,划分:(a,b)=>a/b,max:arr=>maxBy(arr)};Object.keys(fns).forEach(fnName=>module.exports[fnName]=fns[fnName]);这个小改动显然会影响输出代码。由于输出文本太大,我们只显示其中的一小部分。...(()=>{"usestrict";/*harmonyimport*/var_utils__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(288);constsubtract=(a,b)=>a-b;console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__/*.add*/.IH)(1,2));})();可以看到最终生成的代码中包含了一些webpack运行时代码,负责模块导入导出的能力。这次utils.js和index.js的所有变量都没有放在同一个命名空间,动态导入的模块都是通过__webpack_require__导入的。在使用CommonJS时,我们可以通过任意表达式构造导出名称。例如下面的代码也可以正常运行:module.exports[(Math.random()]=()=>{…};这导致构建工具在构建时,无法知道导出的变量name,因为这个名字只有在用户浏览器运行时才能确定,压缩工具无法准确知道index.js使用了模块的哪一部分,所以无法正确进行tree-shaking。如果我们从导入一个CommonJS模块node_modules,你的构建工具将无法对其进行适当的优化。由于CommonJS模块化方案的动态特性,对CommonJS使用Tree-shaking尤其难以分析。与表达式导入模块的CommonJS相比,ESM模块导入始终使用静态字符串文字。在某些情况下,如果你使用的库遵循一些与CommonJS相关的约定,你可以使用第三方webpack插件:webpack-common-shake,在构建过程中,删除未使用的模块。虽然该插件添加了CommonJS支持对于树沙king,它没有涵盖所有的CommonJS依赖,这意味着你无法获得相同的ESM效果。此外,这不是webpack的默认行为,它会增加构建时间的额外成本。总结为确保构建工具尽可能优化您的代码,请避免使用CommonJS模块并使用UseESM语法。这里有一些方法可以验证你的项目是否是最佳实践:使用Rollup.js提供的node-resolve插件,并启用modulesOnly选项,表明你的项目将只使用ESM。使用is-esm验证npm安装的模块是否使用ESM。如果您使用的是Angular,默认情况下,如果您依赖的模块无法执行tree-shaking,您将收到警告,您可以通过以下二维码关注。转载本文请联系更牛逼前端公众号。
