先记住这个答案
静态 import 用于始终需要的依赖。它写在文件顶部,编译期就确定了模块之间的依赖关系,打包工具可以据此做 tree-shaking、类型检查、静态分析。import() 用于运行时才决定要不要加载的模块。它返回 Promise,可以出现在函数里、if 里、事件回调里,什么时候执行到就什么时候加载。
- 静态
import编译期确定依赖,支持 tree-shaking import()运行时加载,可以放在if或函数里- 路由懒加载是最常见的
import()使用场景 - 首屏不需要的重型库用
import()延迟加载
路由懒加载:最典型的 import() 场景
单页应用里,用户进首页时不需要加载"设置页"的代码。如果全部静态 import,所有页面的代码会被打进一个大的 bundle,首屏加载时间变长。
import() 实现懒加载JavaScript// 每个路由组件用 import() 加载
// 打包工具会把每个 import() 的目标拆成单独的 chunk
const Home = lazy(() => import('./pages/Home'))
const Settings = lazy(() => import('./pages/Settings'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
// 用户点击"设置"时才下载 Settings 的 chunk
// 首屏只下载 Home 的代码React 的 lazy() 接受一个返回 import() 的函数,组件首次渲染时才触发加载。Vue Router 也是同样的思路——component: () => import('./views/About.vue')。这个写法已经是前端项目的标配了。
条件加载和环境判断
有些模块只在特定条件下才需要用到。比如只有管理员需要的权限管理模块,或者只在移动端需要的手势库。静态 import 没办法做到"满足条件才加载",因为它在编译期就确定了。
async function initDashboard(user) {
if (user.role === 'admin') {
// 管理员才加载后台管理模块
const { AdminPanel } = await import('./AdminPanel')
renderAdmin(AdminPanel)
} else {
// 普通用户加载精简版
const { UserPanel } = await import('./UserPanel')
renderUser(UserPanel)
}
}这样普通用户永远不会下载管理员模块的代码,既减小了下载量,也避免了前端代码泄露管理功能的逻辑。
另一个常见场景是根据浏览器能力加载不同的 polyfill。比如检测到不支持 IntersectionObserver 时再动态加载 polyfill,支持的浏览器就省下这部分体积。
重型库延迟到用户操作时再加载
有些库体积很大,但只在用户触发特定操作时才用到。比如图表库只在用户点击"查看报表"时才需要,代码编辑器只在用户进入编辑页面时才需要。把这些库放在静态 import 里会拖慢首屏。
用 import() 可以在用户操作时才加载。虽然用户第一次点击时会有一点延迟(网络请求的时间),但首屏速度的提升通常远大于这个代价。可以配合 <link rel="modulepreload"> 或者在空闲时间用 requestIdleCallback 预加载来减少等待感。
容易答错的地方
- 觉得所有
import都可以换成import() import()是异步的,用起来比静态import复杂——要处理 Promise、要考虑加载失败、要考虑加载期间的 UI 状态。而且import()加载的模块不能被 tree-shaking 分析得那么彻底。能用静态import就用静态的,只有真正需要按需加载的场景才用import()。- 以为
import()只能用在浏览器环境 - Node.js 同样支持
import()。在 CommonJS 文件里import()是加载 ESM 模块的唯一方式。Node.js 里虽然没有"首屏加载"的概念,但import()在写插件系统、按配置加载模块这些场景也很有用。
面试官还会怎么问?
import() 会影响 tree-shaking 吗?
会。静态 import 让打包工具在编译期就知道模块的哪些导出被用到了,没用到的可以删掉。import() 加载的模块,打包工具通常会把它的所有导出都保留,因为无法在编译期确定运行时会用到哪些。不过如果对 import() 的结果做了解构,有些打包工具能做一定程度的优化。
用 import() 加载的模块什么时候会被卸载?
不会被卸载。一旦模块被加载并执行,它就一直在内存里,下次 import() 同一个路径时直接从缓存拿。如果需要释放内存,需要确保没有任何引用指向该模块的导出对象,等待垃圾回收器处理——但模块本身在模块缓存里不会被清除。
import() 能和顶层 await 一起用吗?
可以。在支持顶层 await 的 ESM 模块里,可以直接写 const mod = await import('./a.js'),不需要包在 async 函数里。但要注意顶层 await 会阻塞依赖该模块的其他模块的执行,用在入口文件问题不大,用在被多处导入的工具模块里就要谨慎了。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。