🔥 Is ESM asynchronous and high operating efficiency?
🧑💻A:
ES6 modules aren't asynchronous, at least not as they are in use right now most places. Put simply, unless you're using dynamic imports (see below), each import statement runs to completion before the next statement starts executing, and the process of fetching (and parsing) the module is part of that. They also don't do selective execution of module code like you seem to imply. import {bar, baz} from './foo.js' loads, parses, and runs all of './foo.js', then binds the exported entities named 'bar', and 'baz' to those names in the local scope, and only then does the next import statement get evaluated. They do, however, cache the results of this execution and do direct binding, so the above line called from separate files will produce multiple references to the single 'bar' and 'baz' entities.
Now, there is a way to make them asynchronous called 'dynamic import'. In essence, you use import as a function in the global scope, which then returns a Promise that resolves to the module you're importing once it's fetched and parsed. However, dynamic import support is somewhat limited right now (IE will never support it, Edge is only going to get it when they finish the switch to Chromium under the hood, and UC Browser, Opera Mini, and a handful of others still don't have it either), so you can't really use them if you want to be truly portable (especially since static imports (the original ES6 import syntax) are only valid at the top level, so you can't conditionally use them if you don't happen to have dynamic import support).
As a result of this, code built around ES6 modules is often slower than equivalent code built on AMD (or a good UMD syntax).
Hi A, thanks for the reply! I appreciate you taking a lot of time to write this.
According to this link, it says "ECMAScript 6 gives you the best of both worlds: The synchronous syntax of Node.js plus the asynchronous loading of AMD. ", and this article also says that " ESM is asynchronously loaded, while CommonJS is synchronous."
Regarding ESM speed, what I meant to say is that ESM creates static module structure (source, source), allowing bundlers to remove unnecessary code. If we remove unnecessary codes using bundlers like webpack/ rollup, wouldn't this allow the shipping of less codes, and if we ship less code, we get faster load time? (btw, just reread the article, I definitely didn't mention rollup usage. Will revise that).
There is a good chance I am wrong (still learning about JS modules) or interpreted what I read incorrectly (also likely, happened before), but based on what I've read, ESM is async and ESM in overall is faster because it removes unnecessary code. I really appreciate your comment - it forced me to look up more stuff and do more research!
Digging a bit further myself, I think I know why I misunderstood the sync/async point. Put concretely based on looking further at the ES6 spec, the Node.js implementation of CJS, and the code for Require.js and Alameda):
CJS executes imports as it finds them, blocking until they finish.
ESM waits to execute any code in a module until all of it's imports have been loaded and parsed, then does the binding/side-effects stuff in the relative order that they happen.
AMD also waits to run module code until it's dependencies are loaded and parsed, but it runs each dependency as it's loaded in the order in which they finish loading, instead of the order they're listed in the file.
So, in a way, we're kind of both right. The loading and parsing for ESM modules is indeed asynchronous, but the execution of the code in them is synchronous and serialized based on the order they are imported, while for AMD, even the execution of the code in the modules is asynchronous and based solely on the order they are loaded.
That actually explains why the web app I recently converted from uisng Alameda to ESM took an almost 80% hit to load times, the dependency tree happened to be such that that async execution provided by AMD modules actually significantly cut down on wait times.
“JS 模块化”发展史 (CJS / AMD ➡️ UMD ➡️ ESM )
参考文章:
JS 为什么需要模块化?
最早的 JS 并没有导入/导出模块的概念,所有功能均写在一个文件内。这就导致在开发大型项目时,无法并行开展团队协作和代码管理。虽然开发者可以将不同功能的模块抽离出来放入不同的 JS 文件,通过引用的方式构建项目,但这可能会引起命名冲突、污染作用域等一系列问题。为了解决这些问题,模块化的概念及实现方法应运而生 —— ”隔离并维护 JS 文件自身的作用域“。
CJS (CommonJS 规范)
❇️ 代码形式
🌈 特点
CJS imports module synchronously.
When CJS imports, it will give you a copy of the imported object.
CJS will not work in the browser. It will have to be transpiled and bundled.
❌ 不足
AMD (Asynchronous Module Definition 规范)
AMD:异步模块定义
=> 采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,模块加载完成之后会自动执行所挂载的回调函数。❇️ 代码形式
🌰 例子
🌈 特点
AMD imports modules asynchronously.
AMD is made for frontend (when it was proposed) (while CJS backend).
AMD 可以并行非阻塞加载模块 (得益于它的异步加载方式)。
❌ 不足
UMD (Universal Module Definition 规范)
UMD:通用模块定义
=> 鉴于AMD
(浏览器优先,异步加载)和CommonJS
(服务器优先,同步加载)的不同特点,为实现通用,UMD
对二者做了整合。其本质就是:先判断是否支持node
的模块,支持就使用node
;再判断是否支持AMD
,支持则使用AMD
的方式加载。❇️ 代码形式
🌈 特点
Works on front and back end.
Unlike CJS or AMD, UMD is more like a pattern to configure several module systems. Check here for more patterns.
UMD is usually used as a fallback module when using bundler like Rollup/ Webpack
UMD
目前通常作为ESM
不工作情况下的备用兜底方案。ESM (ECMAScript Module 规范)
ESM:ES 模块
=> JS 提出的标准模块系统实现。其设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。❇️ 代码形式
🌈 特点
编译时加载 (静态加载):
ES6
可以在编译时就完成模块加载。不同于CJS
和AMD
的运行时加载方式,ESM
本质上是在编译阶段加载、解析并运行模块,并将实际代码所引用的实体与预设接口名相互关联映射,代码运行中基于接口关联的引用路径,从各模块调用到对应的实体。其整体的运行效率要比CommonJS
模块的加载方式高。但编译时加载方法也导致了没法直接引用ES6
模块本身,因为它不是对象。便于静态分析以及
Tree-Shakeable
: 得益于ESM
在编译阶段就构建了模块间的引用依赖,这使得开发人员可以多模块依赖进行提前分析,同时可以发现不必要的模块引入,对其进行剔除。这些都是CJS
和AMD
等运行时加载的模块化方案所做不到的。Works in many modern browsers
It has the best of both worlds: CJS-like simple syntax and AMD's async
🔥 Is ESM asynchronous and high operating efficiency?
🧑💻A:
ES6 modules aren't asynchronous, at least not as they are in use right now most places. Put simply, unless you're using dynamic imports (see below), each import statement runs to completion before the next statement starts executing, and the process of fetching (and parsing) the module is part of that. They also don't do selective execution of module code like you seem to imply.
import {bar, baz} from './foo.js'
loads, parses, and runs all of './foo.js', then binds the exported entities named 'bar', and 'baz' to those names in the local scope, and only then does the next import statement get evaluated. They do, however, cache the results of this execution and do direct binding, so the above line called from separate files will produce multiple references to the single 'bar' and 'baz' entities.Now, there is a way to make them asynchronous called 'dynamic import'. In essence, you use
import
as a function in the global scope, which then returns a Promise that resolves to the module you're importing once it's fetched and parsed. However, dynamic import support is somewhat limited right now (IE will never support it, Edge is only going to get it when they finish the switch to Chromium under the hood, and UC Browser, Opera Mini, and a handful of others still don't have it either), so you can't really use them if you want to be truly portable (especially since static imports (the original ES6 import syntax) are only valid at the top level, so you can't conditionally use them if you don't happen to have dynamic import support).As a result of this, code built around ES6 modules is often slower than equivalent code built on AMD (or a good UMD syntax).
🧑💻B:
Hi A, thanks for the reply! I appreciate you taking a lot of time to write this.
There is a good chance I am wrong (still learning about JS modules) or interpreted what I read incorrectly (also likely, happened before), but based on what I've read, ESM is async and ESM in overall is faster because it removes unnecessary code. I really appreciate your comment - it forced me to look up more stuff and do more research!
🧑💻A:
Digging a bit further myself, I think I know why I misunderstood the sync/async point. Put concretely based on looking further at the ES6 spec, the Node.js implementation of CJS, and the code for Require.js and Alameda):
So, in a way, we're kind of both right. The loading and parsing for ESM modules is indeed asynchronous, but the execution of the code in them is synchronous and serialized based on the order they are imported, while for AMD, even the execution of the code in the modules is asynchronous and based solely on the order they are loaded.
That actually explains why the web app I recently converted from uisng Alameda to ESM took an almost 80% hit to load times, the dependency tree happened to be such that that async execution provided by AMD modules actually significantly cut down on wait times.
❇️ 核心讨论内容:ESM 是否是异步的 / 高效的?
综合上述两者的讨论,可以提取出以下几点关键信息:
ESM
在编译和解析阶段是异步执行的,但是在代码运行阶段依然是同步的。但ESM
提供了在运行时异步加载模块的API: import()
,它允许ESM
同AMD
类似,在运行到模块加载命令处再异步加载模块,并通过回调方式执行后续依赖于该模块的代码。ESM
高效主要体现在其Tree-shakeable
的特性上,依据静态结构剔除冗余的模块依赖引入,从而实现更快的加载。CJS
和AMD
而言是高效的。对比
AMD vs CJS
对于依赖的模块,
AMD
是 提前执行,CMD
是 延迟执行。AMD
推崇 依赖前置,CMD
推崇 依赖就近。AMD
的 API 默认是一个当多个用,CMD
的 API 严格区分,推崇职责单一。ESM vs CJS
CommonJS
模块输出的是一个 值的拷贝,ES6
模块输出的是 值的引用。CommonJS
模块是运行时加载,ES6
模块是编译时输出接口。CommonJS
模块的require()
是 同步加载 模块,ES6
模块的import
命令是 异步加载,有一个独立的模块依赖的解析阶段。