# SKUOptionsSelection 性能优化报告

日期：2026-08-29  
范围：`pro/skuDetailModal → plus/skuOptionsSelection → pro/Selector`  
验证商品：62028、479897

## 1. 最终结论

客户反馈的“低端设备点击 SKU 后约 0.3–0.5 秒才反映到 UI”，主因不是选项数量、`cloneElement`、linkage、value 转换或 `effectiveMax` 计算，而是同一次 SKU 操作同步穿透了多层状态：

```text
用户点击
  → Selector.Group 提交本地选择
  → SKUOptionsSelection 转换并上报业务 value
  → SelectionFlow 同步更新权威 Draft
  → skuDetailModal 把同一 Draft 复制到多组本地 state
  → Modal 父树、Flow 和 SKU 子树在同一事件中重新渲染
  → React commit 完成后浏览器才能绘制选中态
```

React 18 会批处理同一事件内的外部 store 通知和父组件 state 更新。虽然 Selector 的局部状态很早已经变化，但浏览器仍需等待整个同步任务和 React commit，导致低端设备上出现明显点击延迟。

最终只保留两项直接针对根因的修改：

1. SKUOptionsSelection 以“Selector 投影是否变化”判断是否需要重新协调。上层原样回传同一选择时，不再重复进入 SKU 内容树。
2. SelectionFlow 继续同步持有权威 Draft；只有 SKU-only 事务产生的 Modal 重复镜像投影进入 transition，不再阻塞选中态首帧。

此前实现的 SelectionEngine、Selector 细粒度订阅、跨组 revision 等扩展性优化已经全部撤回，不属于本次客户问题的必要修复。

## 2. 诊断过程

### 2.1 商品规模不足以解释延迟

62028 的实际规模是：

- 1 个 variant group、2 个 variant items；
- 5 个 option groups、17 个 option items；
- 无 bundle；
- 无嵌套 options；
- 合计 19 个可选项。

这是典型的中小 SKU 场景，不具备仅凭节点数量制造 300–500ms 长任务的条件。

### 2.2 计算热点排除

使用真实商品数据进行离线分段：

| 分段 | 单次耗时 |
| --- | ---: |
| Selector value → 业务 SKU value | 约 0.0008–0.0009ms |
| 业务 SKU value → Selector value | 约 0.0017ms |
| SKU 业务选择签名 | 约 0.0004ms |
| capacity 重新解析 | 约 0.0009–0.0010ms |
| SKUOptionsSelection 独立受控点击 | 约 4–7ms |
| SKUOptionsSelection React 主更新 | 约 2–3ms |

这些操作不能单独解释 300–500ms 延迟。

### 2.3 Selector 扩展性优化不是主因修复

诊断阶段曾重新实现 Selector 内核，将 40 个普通选项的一次点击从 120 次重型卡片执行降至 1 次，将 22 个共享 max 的套餐项从 66 次降至 1 次。

这证明旧 Selector 存在数据量扩展性问题，但真实 62028 在完成该重构后，20× CPU slowdown 下点击到首帧仍为 315–350ms，与客户反馈一致。因此它不是本次延迟的主导因素。

### 2.4 浏览器探针确认传播链路

临时探针覆盖：

- NormalCard 点击入口；
- Selector action 返回；
- SKUOptionsSelection value 转换和业务回调；
- 受控 value 投影比较；
- SelectionFlow commit；
- selected card commit；
- 首个 requestAnimationFrame；
- Modal 镜像投影 commit。

探针显示，业务转换和回调很快完成，主要时间消耗发生在 Flow/Modal 父树同步提交。受控 value 回传还会再次穿透 SKU 树，形成额外协调。

## 3. 根本原因

### 3.1 P0：Modal 重复镜像阻塞交互

SelectionFlow 已经持有完整权威 Draft，但 skuDetailModal 又把同一 Draft 拆分复制到：

- `latestSelectionFlowDraft`；
- `productData`；
- `skuValue`；
- `quantity`；
- `note`；
- `discounts`；
- `priceOverride`；
- `weighingData`。

SKU 点击因此同步驱动整个 Modal 父树及价格摘要、预约派生状态等外围消费者。20× 基线中，Modal 单次 render/commit 可持续约 229ms，是最主要的阻塞来源。

### 3.2 P1：受控原样回传再次穿透 SKU 树

Selector.Group 已经提交本地选择后，SKUOptionsSelection 将其转换成业务 value 上报。上层保存后又把等价 value 回传。

旧协调方式无法区分：

- 当前事务的受控确认；
- variant 自动补全；
- 外部主动覆盖；
- 受控宿主拒绝用户值。

因此原样回传也会重新进入 SKU 内容树，造成一次额外协调和 commit。

### 3.3 P2：Selector 整组渲染是独立扩展性问题

旧 Selector 将根状态与整组 `renderItem` 绑定，几十到上百个重型卡片时会放大工作量。

但 62028 只有 19 个选项，实测证明该问题被更上层的固定传播成本覆盖。本次不修改公共 Selector，避免扩大外部项目兼容风险。

### 3.4 非核心因素

以下因素均不是本次主因：

- `cloneElement`；
- linkage/linkageRules；
- value 双向转换；
- SKU 签名计算；
- capacity 解析；
- `effectiveMax` 及共享 max 规则；
- 少量调试日志；
- CSS transition。

CSS transition 影响视觉动画完成时间，但不能解释 selected class 提交前的同步长任务。

## 4. 修复方案

### 4.1 SKUOptionsSelection：比较投影，不判断“是否回声”

SKUOptionsSelection 维护一个稳定的运行时协调对象，记录 Selector.Group 已提交的完整 Selector 投影。

上层业务 value 回传时重新解码：

- 投影相同：确认当前事务，跳过 SKU 内容树重新渲染；
- 投影不同：执行真正的外部协调；
- variant 自动补全：投影不同，继续回填其它 group；
- 外部 override：投影不同，正常应用；
- 受控宿主拒绝：外部投影不同，回滚本地选择。

这种判断基于最终 Selector 状态，而不是粗粒度的 `isControlledEcho` 标记，不会把真实业务修正误判为回声。

同时，SKUOptionsSelection 不再把业务 value、Selector value、Selector dataSource 和实例 store 都作为根渲染状态源。实例 store 仅作为现有卡片 hooks 的兼容上下文。

### 4.2 skuDetailModal：权威 Draft 与外围投影分离

SelectionFlow 的 Draft/ref 保持同步更新，模块依赖、校验和确认始终读取权威 Flow 状态。

只有满足以下条件时，Modal 的重复镜像投影进入 transition：

- SKU value 实际变化；
- productData 未变化；
- quantity、note、discounts、priceOverride、bookingPrice 未变化；
- booking 除 capacity 外未变化。

以下操作仍保持同步优先级：

- 数量；
- 预约日期、时间、资源；
- 备注；
- 折扣；
- 手动改价；
- weighing；
- 商品切换。

SKU 变化导致 capacity 失效属于同一 SKU 事务，可以随外围投影处理。

### 4.3 API 兼容策略

没有删除或修改公开 API：

- Selector 和 Selector.Group 已恢复仓库 HEAD 实现；
- SKUOptionsSelection 的 props、业务 value、onChange、ref 方法不变；
- skuDetailModal 的 open、confirm、validation 契约不变；
- legacy `renderItem` 和 `cloneElement` 路径不变；
- variant 自动补全仍由既有业务层完成；
- linkageRules 行为未修改；
- 套餐共享 max、达到总上限后 disabled 的逻辑未修改。

## 5. 前后性能对比

### 5.1 根因定位阶段

20× CPU slowdown：

| 版本 | click→first rAF |
| --- | ---: |
| 原同步传播基线 | 315–350ms |
| 仅增加 SKU 受控投影边界 | 254–313ms |
| 再隔离 Modal 镜像投影 | 131–178ms |

这说明 SKU 投影边界有效，但 Modal 同步镜像仍是最大阻塞；两项修复组合后才能稳定进入 200ms 以内。

### 5.2 当前最小修复复测

撤回所有非必要重构后，使用同一页面分别采集正常、13×、20× 各 2 次：

| CPU | Adapter 返回 | selected card commit | Flow commit | click→first rAF | Modal 镜像 commit |
| --- | ---: | ---: | ---: | ---: | ---: |
| 正常 | 0.9–1.3ms | 3.9–5.0ms | 3.9–5.1ms | 6.1–8.4ms | 12.3–31.6ms |
| 13× | 6.9–8.7ms | 43.8–47.1ms | 44.2–49.3ms | 75.5–83.8ms | 372.7–474.0ms |
| 20× | 8.2–12.0ms | 61.0–64.7ms | 61.1–64.8ms | 107.8–119.0ms | 最新投影 573.1ms |

结论：

- 6 次事务的受控回传均被判定为相同 Selector 投影；
- 6 次 SKU-only Modal 投影均正确进入 transition；
- selected card 和 Flow 基本在同一次 commit 完成；
- 正常性能下首帧约 6–8ms；
- 13× 下首帧中位约 80ms；
- 20× 下首帧中位约 113ms；
- 相对原同步基线中位约 333ms，20× 探索性降低约 66%。

第二个 20× 点击发生在前一个 transition 完成前，React 合并了延迟更新，因此该档只记录到最新 Modal 投影提交，不能用来计算 Modal p50。

每档只有 2 个样本，以上结果用于验证因果和优化方向，不能替代正式 p50/p95。

## 6. 功能回归

必要回归覆盖：

1. 同一 Selector 投影的外部 value 回传不会重新进入 SKU 内容树。
2. 真实 479897 套餐共 47 项。
3. 某项达到共享总上限 99 后，其余 21 项 disabled 且 max 为 0。
4. 两组 variant 中选择一项后仍会自动补全完整可售 variant。
5. 嵌套 OptionsCard 的 remainingCount 保持实时。
6. 真正的外部 controlled override 正常生效。
7. 受控宿主拒绝用户值时，本地选择回滚。
8. SKU-only + capacity invalidation 可以降低 Modal 投影优先级。
9. quantity、booking、price 等变化不会被误判为 SKU-only。

测试结果：

- 必要定向回归：8/8 通过；
- skuOptionsSelection + skuDetailModal 相关目录：167 项中 166 项通过；
- 唯一失败为 Holder 的既有断言，同一测试在未修改 HEAD 上也以相同结果失败；
- `git diff --check` 通过；
- 未运行 webpack material build，符合工作区要求。

## 7. 风险与边界

### 7.1 外围 UI 延迟

SKU-only 操作后，Modal 外围价格摘要、booking price 展示和 POS 自动处理可能晚于选中态更新。

20× 探索样本中，Modal 投影约 373–573ms 后完成；折算正常 CPU 约几十毫秒。最终 Flow 数据、校验和确认不等待该镜像，但外围展示与副作用触发时序必须纳入上线回归。

### 7.2 transition 只移动工作，没有消除工作

当前修复解决的是“外围重复工作阻塞选择首帧”。Modal 的重复 state 仍然存在。

长期架构方向应是：

- Flow 保持唯一权威 Draft；
- Modal 外围消费者按所需字段订阅；
- 删除 `productData/skuValue/quantity/price` 等重复镜像；
- 最终减少或移除 SKU-only transition。

### 7.3 Selector 扩展性问题仍存在

撤回 SelectionEngine 后，旧 Selector 在 40/47 项重型卡片场景仍会整组渲染。

这不影响当前 62028 根因修复，但未来若出现上百选项商品，应作为独立项目处理，并单独验证外部 Selector 用户的完整 API 与渲染契约。

## 8. 正式验收标准

使用同一 production bundle、同一浏览器、同一商品 62028、相同 CPU slowdown，对 HEAD 与候选版本分别预热并至少采样 30 次。

必须记录：

- click→selected class commit；
- click→下一帧 paint；
- 事件长任务时长；
- React commit；
- p50/p95；
- 设备型号、浏览器版本和 slowdown 档位。

验收门槛：

- 20× slowdown 下 click→下一帧 paint p95 不高于 200ms；
- 相对 HEAD 的 p95 至少降低 50%；
- 正常 production 环境目标不高于 16.7ms；
- 目标低端设备确实跨帧时上限 33.3ms；
- variant、external override、controlled rejection、共享 max、booking price 与 POS 自动处理无行为回归；
- CSS 动画完成时间不得冒充状态提交时间。

## 9. 最终修改范围

运行时代码：

- `skuOptionsSelection/index.tsx`：受控 Selector 投影协调；
- `skuDetailModal/index.tsx`：Flow Draft 与 Modal 镜像优先级分离；
- `skuDetailModal/flow/isSkuOnlyDraftChange.ts`：SKU-only 事务判定。

回归与文档：

- `skuOptionsSelection/reconciliation.test.tsx`；
- `skuDetailModal/flow/isSkuOnlyDraftChange.test.ts`；
- 本报告。

已撤回：

- Selector/SelectorGroup 全量重构；
- SelectionEngine；
- option/view/control 细粒度订阅；
- `incrementalControls`；
- `observeAllValues` 和跨组 revision；
- NormalCard `actions.getOption`；
- usePresetProps 依赖修改；
- 日志清理与无关格式化；
- 仅服务于扩展性重构的测试和指标。
