Template System 进阶:Core / Extra 分离与“钥匙带大门”——把 React 栈的加载时间榨干
Template System 进阶(二):Core / Extra 分离与“钥匙带大门”——把 React 栈的加载时间榨干
承接上篇《Template System 收官构建:多主题矩阵、Vendor 抽离、Manifest 联合、Cache 治理》(图一)。这次把 vendor 从“一块砖”演化为 Core / Extra 两段式(图二),并把整条加载链路收敛为 Theme-Loader SDK 的三颗齿轮:
getManifests、buildHeadTags、buildImportMap。本文只谈思路,不贴代码。
0. 我到底在优化什么?
一句话:把首屏一定会用到的 React 栈,以更少的往返(RTT)更早地塞进浏览器缓存,同时保持与主题资源的彻底解耦。
很多人只会在 <head> 里预抓 react-dom。问题是:客户端 Hydration、createRoot/hydrateRoot、以及部分能力(如 portals)会在运行时触发 react-dom/client。如果你只预抓了 react-dom,等代码真正跑到 react-dom/client 时,还要再走一次网络来拿“钥匙”——白丢一次 RTT。
更聪明的做法是:预抓 “上游入口” react-dom/client。浏览器在解析这个小入口时,会沿着它的静态依赖把 react 和 react-dom 一并并行拉进缓存。随后业务代码再导入 react-dom,直接命中缓存,不再多走那一步。这就是本文的核心策略:“钥匙带大门”。
1. 从图一到图二:为什么要 Core / Extra 两段式?
为了 可控的首屏带宽预算 与 缓存收益最大化,我把 vendor 分成两段、两种摇树策略:
Core(底层 vendor,放“实现”)
- 
内容: react、react-dom(生产版实现)。
- 
策略:激进摇树 —— treeshake: { moduleSideEffects: false }
 这等价于告诉构建器“所有模块都没有顶层副作用”,只要没有被导出/使用,就可以大胆裁掉。放在“底层实现”尤其合适:越干净,缓存越值钱。
Extra(上层入口与薄壳,放“引用”)
- 
内容: react-dom/client、react/jsx-runtime(小入口 / 薄壳)。
- 
策略:常规摇树 —— treeshake: true
 目的不是压到极致,而是保留入口文件里的 import 语句。
 另外在这一段显式 external:react、react-dom
 如此一来,react-dom/client产物里会保留 对这两者的“裸导入”。这正是“钥匙带大门”的落地:预抓 client 时,浏览器顺着裸导入并行拉下 react 与 react-dom;运行时再次导入时,命中同一份缓存。
结论:Core 放“实现”,能狠摇树;Extra 放“入口”,要保留 import。
这不是语义洁癖,而是让预抓阶段与运行时解析出的 URL 严丝合缝,从而稳定命中缓存。
2. 为何不直接预抓 react-dom?
- 
只预抓 react-dom:你把“大门”抬来了,但当运行时首次需要react-dom/client时,仍要再发起一个小请求来找“钥匙”。多一次 RTT,首帧更慢一点点。
- 
预抓 react-dom/client:拿到“钥匙”的同时,浏览器会顺着依赖把“大门”也搬来;两者都在缓存里等你,少一次发现-再请求的延迟。
- 
react/jsx-runtime不会被react-dom/client引用,但你在新 JSX 运行时会用到,所以它单独做一次预抓更稳。
3. 产物布局的“契约化”
- 
Core 产物: react-<ver>.*.js、react-dom-<ver>.*.js
 —— 路径携带版本号,长缓存(一年immutable),可灰度、可回滚。
- 
Extra 产物: react-dom-client-<ver>.*.js、react-jsx-runtime-<ver>.*.js
 —— 轻壳、保留裸导入、走同一份 import map。
- 
Manifests: manifest.vendor.json+manifest.<theme>.json
 —— vendor 清单与主题清单各司其职,import map 与<head>标签都从这里计算。
4. Theme-Loader SDK:三颗齿轮把复杂度吃掉
为了让主程序 RootLayout 不再写“手工艺活”,我把流程沉淀为 三个 API。它们只做各自的职责,不泄漏底层细节。
① getManifests(themeName, version)
- 
读取并解析两个 manifest(vendor / theme)。 
- 
统一做三层缓存:CDN 协商缓存 + SSR revalidate + 进程级热缓存。 
- 
输出结构化数据:四个关键 vendor 键、主题 CSS、各场景入口。 
② buildImportMap(vendorManifest)
- 
由 vendor manifest 生成 import map 的 imports。
- 
至少包含这四个键: react、react-dom、react-dom/client、react/jsx-runtime。
- 
要求:这段 import map 必须先于任何 <link>注入,让预抓阶段和运行时解析都走同一张“路由表”。
③ buildHeadTags({ vendor, theme, scenes, currentScene, ... })
- 
生成 <head>所需的<link>标签列表:- 
modulepreload:react-dom/client(钥匙带大门)、react/jsx-runtime。
- 
CSS:首屏主题样式直接 <link rel="stylesheet">(必要时提高优先级),避免多余的双写 preload。
- 
场景:当前场景入口用 modulepreload,其他场景按保守策略用prefetch(低优先级、暖顶层 URL,不抢带宽)。
- 
字体等第三方域:按需 preconnect。
 
- 
- 
输出是字符串化的 <link>列表;RootLayout 只需塞进去,不再关心路径、哈希、版本。
5. 运行时“丝滑”的四条底线
- 
import map 先于所有 <link>注入:预抓解析依赖时也要用到它。
- 
URL 一致性:预抓阶段与运行时导入解析出的 URL 必须字节一致(域名、路径、hash、query 都一样),否则缓存不命中。 
- 
缓存策略:vendor 长缓存;manifest no-cache + ETag。
- 
CORS / SRI / CSP:跨域加 anonymous,CDN 放开Access-Control-Allow-Origin;可选加 SRI 与 CSP 白名单,提升供应链安全感。
6. 自测清单(按这个跑一遍就能上线)
- 
仅预抓 react-dom/client时,Network 面板是否自动并行请求react与react-dom。
- 
运行时首次导入 react-dom时是否显示来自内存/磁盘缓存。
- 
import map 是否包含四个键,且注入顺序在任何 <link>之前。
- 
切 version是否能即刻灰度/回滚,且 vendor 命中长缓存、manifest 协商更新。
- 
非当前场景是否仅 prefetch顶层 URL,避免和首屏抢带宽。
7. 一句话收束
- 
Core 放实现,Extra 放入口; 
- 
激进摇树砍 Core,常规摇树保 Extra 的 import; 
- 
external 掉 react与react-dom,让react-dom/client以“裸导入”把它们提前并行拉下;
- 
Theme-Loader SDK 把这一切编排成三颗齿轮,RootLayout 只管塞 import map 与 <link>。

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号