CommonJS模块加载本身不阻塞,但require()内部调用fs.readFileSync读取文件,首次加载时同步执行顶层代码,若模块过大或含耗时操作仍会导致主线程阻塞;ESM则基于Promise链异步加载。

CommonJS 本身不处理大文件同步读取——它只是 Node.js 的模块系统,而阻塞风险来自 fs.readFileSync 这类同步 I/O 操作,不是 CommonJS 规范的问题。
同步读取才是真正的阻塞源
Node.js 中用 require() 加载 JavaScript 模块时,内部确实调用 fs.readFileSync 读取文件并执行。但这个过程只在模块首次加载时发生,且仅限于 .js/.json/.node 文件;一旦缓存(require.cache),后续 require 就不再读磁盘。
真正高危的是开发者主动调用:
-
fs.readFileSync('huge.log')—— 直接把整个文件读进内存,主线程完全卡死 -
require('./data.json')—— 如果 data.json 是 100MB 的巨型 JSON,解析过程同样同步、单线程、不可中断
CommonJS 模块加载的隐含限制
即使你没写 readFileSync,只要模块内容过大或初始化逻辑过重,也会拖慢启动:
立即学习“Java免费学习笔记(深入)”;
- 模块顶层代码(top-level code)会同步执行,包括大量计算、正则构建、嵌套对象初始化等
- V8 对 >256KB 的对象会分配到 LargeObjectSpace,GC 回收代价显著升高
- require 链过深(A → B → C → ...)会累积解析和执行时间,形成“冷启动延迟”
避开阻塞的实用做法
不是禁用 CommonJS,而是绕开同步瓶颈:
- 大资源不走 require:把 JSON/CSV/配置数据转为异步加载(
fs.promises.readFile+JSON.parse),或预编译为 JS 模块导出函数而非数据 - 拆分巨型模块:将一个 50MB 的 data.js 拆成多个按需加载的子模块,用
import()或动态require()控制时机 - 用 Web Worker 处理解析:把文件读取和结构化逻辑移到 Worker 中,主线程只负责调度和渲染
- 服务端配合流式响应:对超大配置或数据集,改用 HTTP 流接口 + ReadableStream,前端边收边处理,不落地全量文件
对比 ESM 的差异点
ESM(import)在 Node.js 中默认是异步加载机制(即使静态 import 也走 Promise 链),但顶层 await 和动态 import() 才真正释放控制权。CommonJS 的 require 始终是同步语义——这不是缺陷,而是设计选择。关键在于:别让 require 成为大文件入口,更不要在模块顶层做耗时同步操作。


















