# 在 PWA 中用动态 import() 做代码拆分

> 动态 import() 如何拆分打包体、让启动加载更少 JavaScript，它对 INP 与 LCP 的影响，以及 PWA 中哪些地方不能用。

**一句话：** 代码拆分把一个 JavaScript 打包体拆成若干片段，使浏览器在启动时只下载、
解析、执行页面真正需要的部分；`import()`——一个返回 Promise 的动态表达式，不同于静态
`import` 语句——正是按需加载其余部分的机制。

## 动态 `import()`

`import()` 虽然看起来像函数调用，但其实不是——MDN 指出"`import` 本身是关键字，而非
函数"，因此不能像 `const myImport = import` 这样为它取别名。它返回一个 Promise，成功
时"兑现为一个模块命名空间对象：一个包含 `moduleName` 所有导出的对象"。与静态 `import`
声明不同——后者"是静态的，总会导致被导入的模块在加载时被求值"——动态 `import()` 调用
可以出现在代码的任意位置，按条件或按需加载模块，例如仅在用户提交表单之后才加载。

## 为什么拆分打包体很重要

web.dev 直接点明了目标："代码拆分是一种旨在最小化启动时间的技术"，做法是在启动时发送
更少的 JavaScript——"把你的打包体拆成多个片段，只在最开始发送必要的部分"。被动态导入
的代码"不会被包含进初始打包体，而是被懒加载"，因此只在真正需要时才被获取。

更小的启动包意味着更少的主线程解析/编译/执行工作，这"将有助于改善……INP 时间"，因为
释放出的主线程能更快响应用户输入。对于客户端渲染的页面，削减负责渲染标记的
JavaScript 也能改善 LCP，尤其是当主线程太忙、无法及时渲染最大内容元素时。

## 路由级、组件级与第三方依赖拆分

除了手动延迟单个函数之外，web.dev 还指出了更高层的策略："在使用客户端框架时，按路由
或组件级别拆分，是懒加载应用不同部分的一种更简单的方式"，构建在 webpack 等打包工具之
上的框架通常为此提供内置的抽象。第三方依赖的处理方式通常不同——"被拆分进一个可缓存的
独立 vendor 包，因为它们不常更新"（例如借助 webpack 的 `SplitChunksPlugin`）——而不是
像路由或组件代码那样按需懒加载。支持以动态 `import()` 实现这一目的的打包工具包括
webpack、Parcel 和 Rollup。

## 动态 `import()` 无法运行的地方

`import()` "可以在主线程、共享 worker 或专用 worker 中使用，但如果在 Service Worker
或 worklet 中调用则会抛出错误"。规划拆分方案时应针对运行在主线程或专用/共享 worker
中的代码——它不能用于懒加载 Service Worker 内部的逻辑。

## 缓存行为

每个规范化的模块说明符通常会解析为同一个被缓存的模块命名空间对象——MDN 指出这"通常
成立"，但记录了一个例外：如果被导入的模块导出了一个名为 `then` 的函数，该函数会在动
态导入的 Promise 兑现过程中被调用，MDN 提醒不要依赖这一点。"这种积极的缓存确保一段
JavaScript 代码即便被多次导入，也绝不会被执行超过一次。之后的导入甚至不会产生 HTTP
请求或磁盘访问。" 这种缓存适用于已成功加载并链接的模块；"只有求值失败会被缓存"，因此
加载或链接失败的模块，下一次导入时可能会被重新尝试。

## 实践清单

- [ ] 启动时只发送某个路由或视图真正需要的内容；其余部分放到 `import()` 之后按需加载。
- [ ] 在使用客户端框架时按路由或组件边界拆分，而不是手工拆分单个函数。
- [ ] 把第三方依赖归入一个独立、可长期缓存的 vendor 包，而不是按交互逐个懒加载。
- [ ] 不要在 Service Worker 或 worklet 中调用动态 `import()`——那里会抛出错误。
- [ ] 提前预加载你明确知道很快会用到的片段，避免拆分把启动成本换成了之后可见的延迟。