Template System 进阶:Core / Extra 分离与“钥匙带大门”——把 React 栈的加载时间榨干

Template System 进阶(二):Core / Extra 分离与“钥匙带大门”——把 React 栈的加载时间榨干

承接上篇《Template System 收官构建:多主题矩阵、Vendor 抽离、Manifest 联合、Cache 治理》(图一)。这次把 vendor 从“一块砖”演化为 Core / Extra 两段式(图二),并把整条加载链路收敛为 Theme-Loader SDK 的三颗齿轮:getManifestsbuildHeadTagsbuildImportMap。本文只谈思路,不贴代码。


0. 我到底在优化什么?

一句话:把首屏一定会用到的 React 栈,以更少的往返(RTT)更早地塞进浏览器缓存,同时保持与主题资源的彻底解耦。

很多人只会在 <head> 里预抓 react-dom。问题是:客户端 HydrationcreateRoot/hydrateRoot、以及部分能力(如 portals)会在运行时触发 react-dom/client。如果你只预抓了 react-dom,等代码真正跑到 react-dom/client 时,还要再走一次网络来拿“钥匙”——白丢一次 RTT。

更聪明的做法是:预抓 “上游入口” react-dom/client。浏览器在解析这个小入口时,会沿着它的静态依赖把 reactreact-dom 一并并行拉进缓存。随后业务代码再导入 react-dom,直接命中缓存,不再多走那一步。这就是本文的核心策略:“钥匙带大门”


1. 从图一到图二:为什么要 Core / Extra 两段式?

为了 可控的首屏带宽预算缓存收益最大化,我把 vendor 分成两段、两种摇树策略:

Core(底层 vendor,放“实现”)

  • 内容reactreact-dom(生产版实现)。

  • 策略:激进摇树 —— treeshake: { moduleSideEffects: false }
    这等价于告诉构建器“所有模块都没有顶层副作用”,只要没有被导出/使用,就可以大胆裁掉。放在“底层实现”尤其合适:越干净,缓存越值钱。

Extra(上层入口与薄壳,放“引用”)

  • 内容react-dom/clientreact/jsx-runtime(小入口 / 薄壳)。

  • 策略:常规摇树 —— treeshake: true
    目的不是压到极致,而是保留入口文件里的 import 语句
    另外在这一段显式 externalreactreact-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>.*.jsreact-dom-<ver>.*.js
    —— 路径携带版本号,长缓存(一年 immutable),可灰度、可回滚。

  • Extra 产物react-dom-client-<ver>.*.jsreact-jsx-runtime-<ver>.*.js
    —— 轻壳、保留裸导入、走同一份 import map。

  • Manifestsmanifest.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

  • 至少包含这四个键:reactreact-domreact-dom/clientreact/jsx-runtime

  • 要求:这段 import map 必须先于任何 <link> 注入,让预抓阶段和运行时解析都走同一张“路由表”。

buildHeadTags({ vendor, theme, scenes, currentScene, ... })

  • 生成 <head> 所需的 <link> 标签列表:

    • modulepreloadreact-dom/client(钥匙带大门)、react/jsx-runtime

    • CSS:首屏主题样式直接 <link rel="stylesheet">(必要时提高优先级),避免多余的双写 preload。

    • 场景:当前场景入口用 modulepreload,其他场景按保守策略prefetch(低优先级、暖顶层 URL,不抢带宽)。

    • 字体等第三方域:按需 preconnect

  • 输出是字符串化的 <link> 列表;RootLayout 只需塞进去,不再关心路径、哈希、版本


5. 运行时“丝滑”的四条底线

  1. import map 先于所有 <link> 注入:预抓解析依赖时也要用到它。

  2. URL 一致性:预抓阶段与运行时导入解析出的 URL 必须字节一致(域名、路径、hash、query 都一样),否则缓存不命中。

  3. 缓存策略:vendor 长缓存;manifest no-cache + ETag

  4. CORS / SRI / CSP:跨域加 anonymous,CDN 放开 Access-Control-Allow-Origin;可选加 SRI 与 CSP 白名单,提升供应链安全感。


6. 自测清单(按这个跑一遍就能上线)

  • 仅预抓 react-dom/client 时,Network 面板是否自动并行请求 reactreact-dom

  • 运行时首次导入 react-dom 时是否显示来自内存/磁盘缓存。

  • import map 是否包含四个键,且注入顺序在任何 <link> 之前。

  • version 是否能即刻灰度/回滚,且 vendor 命中长缓存、manifest 协商更新。

  • 非当前场景是否仅 prefetch 顶层 URL,避免和首屏抢带宽。


7. 一句话收束

  • Core 放实现,Extra 放入口

  • 激进摇树砍 Core常规摇树保 Extra 的 import

  • external 掉 reactreact-dom,让 react-dom/client 以“裸导入”把它们提前并行拉下

  • Theme-Loader SDK 把这一切编排成三颗齿轮,RootLayout 只管塞 import map 与 <link>

posted @ 2025-08-14 11:20  PEAR2020  阅读(3)  评论(0)    收藏  举报