速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

不要再依赖 CommonJS 了

  • 2020-05-20
  • 本文字数:3491 字

    阅读完需:约 11 分钟

不要再依赖CommonJS了

在这篇文章中,我们将研究什么是 CommonJS,以及为什么它会让你的 JavaScript 包大小过分膨胀。为了确保打包器(bundler)能成功优化你的应用程序大小,请避免依赖 CommonJS 模块,并在整个应用程序中使用 ES2015 模块语法。


本文最初发布于web.dev网站,经原作者 Minko Gechev 授权由 InfoQ 中文站翻译并分享。

什么是 CommonJS?

CommonJS 是 2009 年的标准,为 JavaScript 模块建立了约定。它最初打算在 Web 浏览器之外的场景中使用,主要用于服务端应用程序。


使用 CommonJS,你可以定义模块,从中导出功能,并将它们导入其他模块中。例如,下面的代码片段定义了一个模块,其导出五个函数:add,subtract,multiply,divide 和 max:


// utils.jsconst { 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.jsconst { add } = require(‘./utils');console.log(add(1, 2));
复制代码


使用 node 调用 index.js 将在控制台中输出数字 3。


由于 2010 年代初期浏览器中缺乏标准化的模块系统,CommonJS 也成为了 JavaScript 客户端库的流行模块格式。

CommonJS 如何影响最终的打包大小?

服务端 JavaScript 应用程序的大小并不像浏览器中那样重要,所以 CommonJS 并没有在设计时考虑到包大小的控制。与此同时,有分析表明 JavaScript 的包体积仍然是拖慢浏览器应用的主要因素之一。


JavaScript 打包器和压缩器(minifier),例如 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 -lah625K Apr 13 13:04 out.js
复制代码


请注意,这个包的大小为 625KB。看一下输出,我们将找到来自 utils.js 的所有函数,外加来自 lodash 的很多模块。尽管我们在 index.js 中不使用 lodash,但它也被加进了输出,这给我们的生产资产增加了很多额外负担。


现在我们将模块格式更改为 ECMAScript 2015,然后重试。这次,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 将使用 ES2015 模块语法从 utils.js 导入:


import { add } from './utils';console.log(add(1, 2));
复制代码


使用相同的 webpack 配置,我们可以构建应用程序并打开输出文件。现在大小只有 40 字节,输出如下:


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


请注意,最后的打包中并没有包含 utils.js 中我们没有用到的任何函数,而且也没有 lodash 的痕迹!更进一步,terser(webpack 使用的 JavaScript 压缩器)在 console.log 中内联了 add 函数。


你可能会问一个问题,为什么使用 CommonJS 会导致输出包大了接近 16,000 倍?当然,上面这个应用只是一个简单的示例,实际应用中的体积差异可能没那么大,但 CommonJS 也很有可能给你的生产构建增添了很大的负担。


一般情况下,CommonJS 模块难以优化,因为它们比 ES 模块动态得多。为确保打包器和压缩器可以成功优化应用程序,请避免依赖 CommonJS 模块,并在整个应用程序中使用 ES2015 模块语法。


请注意,即使你在 index.js 中使用了 ES2015,但如果你使用的模块是 CommonJS,应用程序的打包大小也会受到影响。

为什么 CommonJS 会让应用程序体积更大?

为了回答这个问题,我们将研究 webpack 中 ModuleConcatenationPlugin 的行为,然后讨论静态可分析性。这个插件将所有模块合并为一个闭包,并能让你的代码在浏览器中执行得更快。我们来看一个例子:


// utils.jsexport const add = (a, b) => a + b;export const subtract = (a, b) => a - b;
复制代码


// index.jsimport { add } from ‘./utils';const subtract = (a, b) => a - b;console.log(add(1, 2));
复制代码


如上所示,我们有一个 ES2015 模块,然后将其导入 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 将 index.js 中的 subtract 函数重命名为 index_subtract。


如果让一个压缩器处理上面的源代码,它将:


  • 删除未使用的 subtract 和 index_subtract 函数

  • 删除所有注释和多余的空格

  • 在 console.log 调用中内联 add 函数的主体


开发人员通常将这种移除未使用的导入的操作称为摇树优化(tree-shaking)。因为 webpack 能够静态地(在构建时)了解我们从 utils.js 导入及导出的符号,所以它才能实现摇树优化。


ES 模块默认启用此行为,因为与 CommonJS 相比,它们更容易进行静态分析


我们来看完全相同的示例,但是这次将 utils.js 更改为使用 CommonJS 模块:


// utils.jsconst { 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 中的所有符号放在同一个命名空间下,而是在运行时动态请求使用__webpack_require__的 add 函数。


这是必需的,因为使用 CommonJS,我们可以从任意表达式中获取导出名称。例如,下面的代码是绝对有效的构造:


module.exports[localStorage.getItem(Math.random())] = () => { … };
复制代码


打包器无法在构建时知道导出的符号是什么名称,因为这里需要的信息在用户浏览器的上下文中,而且仅在运行时可用。


这样压缩器无法从 index.js 的依赖项中了解它到底使用了哪些内容,因此无法将无用代码优化。我们还能观察到第三方模块也有完全相同的行为。如果我们从 node_modules 导入 CommonJS 模块,你的构建工具链将无法正确优化它。

基于 CommonJS 实现摇树优化

由于 CommonJS 模块是动态定义的,因此它们分析起来要困难得多。例如,与 CommonJS 相比,ES 模块中的导入位置始终是一个字面量(前者则是一个表达式)。


在某些情况下,如果你使用的库遵循有关 CommonJS 用法的特别约定,则可以在构建时使用这个第三方webpack插件删除未使用的导出。但尽管这个插件增加了对摇树优化的支持,但并未涵盖依赖项使用 CommonJS 的所有可能方式。这意味着你无法获得与 ES 模块相同的保障。此外,除了默认的 webpack 行为外,它还会在构建过程中增加额外的成本。

结论

总之,再次强调,为了确保打包器可以成功优化你的应用程序,请避免依赖 CommonJS 模块,并在整个应用程序中使用 ES2015 模块语法。


原文链接:


https://web.dev/commonjs-larger-bundles/


2020-05-20 08:007778

评论 3 条评论

发布
用户头像
那就不要用webpack啊
2020-05-20 14:28
回复
用户头像
好文章
2020-05-20 13:01
回复
用户头像
infoq 真是个宝藏!
2020-05-20 09:42
回复
没有更多了
发现更多内容

元宇宙用户已准备就绪,但技术瓶颈仍制约其真正“落地”

CECBC

【智人智语】史赛克全球数字化、机器人及赋能技术总裁柯若博:世界智能大会是一个非常重要的盛会

InfoQ 天津

从Oracle日志解析学习数据库内核原理

沃趣科技

oracle 数据库内核

浪潮云应用质效管理系统(AOM)荣获2022数博会领先科技成果“优秀项目”奖

云计算

模块七:作业

本人法海

「架构实战营」

技术分享 | Javaer 如何做单元测试?

LigaAI

Java 单元测试 LigaAI

谁需要实验室内部管理系统?

低代码小观

实验室管理系统 数据管理系统 LIMS实验室信息管理系统 企业管理工具 检查系统服务

大数据ETL自动化调度运维专家-TASKCTL

敏捷调度TASKCTL

Docker 大数据 程序员 DevOps 数据仓库

如何实现24/7客户服务自动化?建设智能客服知识库

小炮

免费领 CRMEB 移动社交电商系统源码与授权

CRMEB

200多家ISV入驻!阿里云计算巢发布一周年

阿里云弹性计算

计算巢

图解|高性能服务器设计之缓存系统一致性

C++后台开发

redis 后端开发 Linux服务器开发 高性能服务器 系统缓存

Hadoop3.0时代,怎么能不懂EC纠删码技术?

个推

大数据 hadoop hadoop3 大数据 开源

HashSet源码分析-基础结构

zarmnosaj

5月月更

怎么用netty开发一个同时提供http和websocket的服务?

风斩断晚霞

Java spring Netty springboot websocket

React Echarts 使用教程 - 如何在 React 中加入图表(内附数据看板实战搭建案例)

蒋川

react.js 可视化 eCharts 组件库 图表库

直播预告|来啦!「DevOps+MLOps Meetup」火热报名中!

第四范式开发者社区

人工智能 DevOps 极狐GitLab 特征平台 MLOps

虚拟化解决方案 virtio 的技术趋势与 DPU 实践解读 | 龙蜥技术

OpenAnolis小助手

虚拟化 技术分享 DPU 龙蜥大讲堂 云豹智能

元宇宙的十大经济规则

CECBC

打造会“找”主人的麦克风,腾讯个性化语音增强技术荣获2022数博会领先科技成果奖

科技热闻

密码学系列之:ASN.1接口描述语言详解

程序那些事

密码学 程序那些事 5月月更 ASN.1

中国20强游戏公司2021年财报分析:业绩大分化时代,开始优胜劣汰

易观分析

游戏公司

ETH合并在即,这次升级将带来哪些机遇?

区块链前沿News

以太坊 eth Hoo

MASA Auth - SSO与Identity设计

MASA技术团队

C# .net 设计 构架 Auth

面向服务的架构

力软低代码开发平台

融云漫话:通信中台

融云 RongCloud

MDM结合ESB数据集成说明

agileai

数据治理 数据集成 企业服务总线 主数据平台 基础数据方案

最佳实践|从Producer 到 Consumer,如何有效监控 Kafka

阿里巴巴云原生

kafka 阿里云 云原生 Promethues

周六晚8点,如何基于 eBPF 技术构建应用可观测平台?

OpenAnolis小助手

Linux 直播 内核 龙蜥社区 sig

【LeetCode】单词距离Java题解

Albert

LeetCode 5月月更

什么是区块哈希?哈希游戏开发技术原理方案(源码搭建)

开发微hkkf5566

不要再依赖CommonJS了_语言 & 开发_Minko Gechev_InfoQ精选文章