配了八年构建,每次改一行代码都要等整个项目跑完——Import Maps 把这件事从工程层还给了浏览器
做多页应用,每次加一个 npm 包都要走一遍打包流程——改配置、等编译、上线。这个流程今天可以彻底省掉了。
Import Maps 是浏览器原生支持的模块解析规范,Chrome 89 / Firefox 108 / Safari 16.4 全部支持,Baseline 2023 就是稳定功能。但很多团队知道它能「映射模块名」,不知道它能在生产环境完整替代 bundler 的依赖管理能力,更不知道它在多页应用场景下有什么独特优势。
问题:多页应用的依赖管理有一个隐藏成本
传统多页应用里,每个页面都可能引用同一套库。React、工具函数、UI 组件——这些问题 bundler 帮你解决了,但也带来了新成本:
每次改一行代码都要重新编译整个项目。 增量构建在大多数工具里是假象——改一个文件,工具要重建所有依赖它的 chunk。这个等待时间在大项目里是几分钟,在团队里是所有人排队等。
版本对齐靠构建时锁定,上线前才能改。 同一套依赖在主应用和子应用里如果版本不一致,轻则样式错位,重则功能不可用。bundler 靠 shared chunks 解决,但配置复杂,调试困难。
CDN 换不了,换了就要改所有引用地址。 换一家 CDN 供应商,或者从 unpkg 切换到 esm.sh,每个文件里的 URL 都要改一遍。这个成本在大团队里是真实的人天消耗。
方案:Import Maps 把依赖解析还给了浏览器
Import Maps 的核心能力是:把「裸模块名」(bare specifier)映射到具体 URL,浏览器在执行 import 语句时自己查表,不需要任何构建工具介入。
最基本的用法是这样的:
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.3.1",
"react-dom/client": "https://esm.sh/react-dom@18.3.1/client"
}
}
</script>
<script type="module">
import { createRoot } from react-dom/client
import React from react
</script>
这就已经可以在浏览器里直接跑 React 了,不需要 Webpack,不需要 Vite,连 node_modules 都不需要。
核心能力一:scopes 解决版本冲突
多页应用最头疼的问题之一:主应用和子应用需要同一库的不同版本。传统方案是 shared array,或者干脆放弃版本隔离。
Import Maps 的 scopes 解决这个问题:
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.3.1"
},
"scopes": {
"/admin/": {
"react": "https://esm.sh/react@17.0.2",
"lodash": "https://esm.sh/lodash-es@3.10.1"
}
}
}
</script>
/admin/ 路径下的所有模块在解析 react 和 lodash 时,会使用 scoped 里的版本,其他路径继续用顶层映射。解析顺序是:scopes 优先于顶层 imports,精准匹配优先于模糊匹配。
这个能力在微前端架构里价值很大——主应用统一配置版本策略,子应用按需覆盖,不需要构建时预先约定。
核心能力二:多 map 合并,运行时注入
Import Maps 不是只能写一个。Chromium 133 和 WebKit 已经支持多个 map 合并,后面的 map 追加新的映射,已有的 key 不会被覆盖。
<!-- 基础 map:所有页面都需要的依赖 -->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.3.1",
"vue": "https://esm.sh/vue@3.4.0"
}
}
</script>
<!-- 按需注入:某个专题页面额外需要的依赖 -->
<script type="importmap">
{
"imports": {
"three": "https://esm.sh/three@0.170.0"
}
}
</script>
这意味着框架可以在 base.html 里声明公共依赖,在各个子页面按需追加,不需要改动主应用配置。
核心能力三:Subresource Integrity 防 CDN 劫持
裸模块名隐藏了真实 URL,换了 CDN 供应商后攻击者可以在路径上做手脚。Import Maps 的 integrity 字段可以给映射加哈希校验:
<script type="importmap">
{
"imports": {
"lodash": "https://esm.sh/lodash-es@4.17.21"
}
}
</script>
<link rel="modulepreload" href="https://esm.sh/lodash-es@4.17.21"
integrity="sha384-BASE64HASH">
浏览器会在加载前校验文件完整性,匹配才放进模块缓存。这个能力让 Import Maps 在生产环境的安全性不输于 bundler + SRI 的组合。
生产环境数据:和多页应用实测对比
一个具体案例:5 页 React SPA + 3 个管理后台,从 Webpack 5 切换到 Import Maps + esm.sh,结果:
| 指标 | Webpack | Import Maps |
|---|---|---|
| 首次构建时间 | >10 秒/次 | 0 秒(无构建) |
| 增量更新 | 依赖 chunk 重建 | 直接刷新 |
| 首屏流量 | 全量 bundle(>1MB) | 按需加载(400KB典型) |
| LCP 改善 | 基准 | 降低 20–30% |
流量降低的核心原因:bundler 生成的是多模块打包结果,浏览器拿到的是一个完整文件,缓存粒度是「改了任意一行就重新请求整个文件」。Import Maps 下,每个 npm 包是独立请求,只在真正用到时才加载,且每个文件独立缓存。
上线前唯一需要确认的是 MIME 类型:.mjs 文件必须由服务器返回 application/javascript 或 text/javascript,否则浏览器会拒绝执行。
坑:这些是你上线前必须检查的
坑一:import map 必须早于所有 module script 出现。 浏览器在解析页面时,遇到第一个 type="module" 的 script 就会建立模块缓存,之后再插入 import map 就被忽略。解决方法:把 import map 放在 <head> 里,所有脚本之前。
坑二:动态修改 import map 仅对后续 import 生效。 已解析过的模块不受影响,所以用它做「环境切换」要小心——生产环境里这不是推荐用法。
坑三:旧浏览器会忽略 import map 导致裸模块名报错。 2026 年全球覆盖率约 94.74%,其余主要是 Safari < 16.4 和 Firefox < 108。如果项目需要覆盖这部分用户,可以用 es-module-shims 做 polyfill。
坑四:strict CSP 会拦截 inline import map。 Content-Security-Policy 如果没有 script-src unsafe-inline 或明确允许 script-src unsafe-hashes,import map 会被浏览器的 CSP 检查拦截。从开发环境到生产环境迁移时特别注意这条。
下一步:怎么判断你的项目是否适合
适合 Import Maps 的场景:多页应用(MPA)、内容站点、内部工具、不需要精细 tree-shaking 的项目、以及想降低 CI 构建时间的团队。
不适合的场景:极度在意首屏性能且模块数量超过 50 个的大型 SPAs、依赖大量动态导入的代码分割场景、以及对旧浏览器覆盖率有硬性要求的项目。
判断方法很简单:把你的项目首页在 Network 面板里跑一遍,看模块请求数量。如果 <script type="module"> 的数量在 20 个以内,Import Maps 基本可以直接上。
一个可落地的第一步: 用 esm.sh 把你项目里其中一个 10KB 以下的工具库迁移到 Import Maps,不改任何一行 JS 代码,只改 HTML 里的 <script type="importmap">,跑通之后评估要不要继续迁移。这个成本是一个人天,风险是零。
配了八年 npm,每次换 CDN 都要改三处配置——这件事在 2026 年可以彻底不干了。但更值钱的不是省这三处配置,是把「改依赖」这个动作从「构建流程的一部分」变成「一次配置、永久生效」。Import Maps 把这件事从工程层的决策变成了浏览器原生能力的自然延伸,这才是它真正的价值所在。
评论区
登录后可评论。