# Kuikly Recomposition Analyzer

> Analyze KuiklyUI Compose DSL recomposition performance issues from Recomposition Profiler output. Use when the user mentions 重组分析、重组优化、卡顿分析、recomp 报告、recomposition analysis, or asks to analyze profiler_report.json / profiler_frames.jsonl log files generated by KuiklyUI Recomposition Profiler.

- Skill: `tencent-tds/kuikly-recomposition-analyzer` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds add tencent-tds/kuikly-recomposition-analyzer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tencent-tds/kuikly-recomposition-analyzer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tencent-tds (https://skillmd.com/u/tencent-tds)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/tencent-tds/kuikly-recomposition-analyzer

---


# Kuikly Recomposition Analyzer

三阶段漏斗分析 KuiklyUI Compose DSL 的重组性能问题：report 筛查 → frames 深挖 → 源码确认。

## 阈值配置

读取 `references/config.md` 获取默认阈值。用户在请求中指定参数可覆盖（如 `scopeCountThreshold=10`）。

## 工作流

### Phase 0 — 获取日志

读取 `references/log-format.md` 了解日志字段格式。按优先级获取 `profiler_report.json` 和 `profiler_frames.jsonl`：

1. 用户直接提供路径 → Read 工具读取
2. 检查当前目录约定路径：`./profiler_logs/`、`./profiler_report.json`
3. 自动从设备拉取 → 读 `references/log-retrieval.md` 执行对应平台命令
4. 均失败 → 输出引导：
   > 未找到 profiler 日志。请先采集数据：
   > 1. 在代码中调用 `RecompositionProfiler.start()` 开始录制，操作完成后调用 `stop()`
   > 2. 或在 Profiler Overlay 面板点击「开始」录制，操作完成后点击「停止」，再点击「获取报告」
   > 3. 采集完成后告诉我文件路径，或提供 App 包名让我来拉取

### Phase 1 — 数据健康检查

- `totalFrames < minFramesThreshold`（默认 30）→ 告警，询问是否继续
- `totalRecompositions == 0` → 提示无重组记录
- `filteredNames` 非空 → 报告中声明排除的组件

### Phase 2 — Report 筛查

**前置步骤（必须先执行）**：按 `recompositionCount` 降序排列所有非 noScope 组件，列出 TOP 20。**任何 `recompositionCount > 50` 的组件必须进入报告**，无论总耗时多低。这一步防止高频但低耗时的组件被后续按总耗时排序时遗漏。

读 `references/detection-rules.md`，遍历 `composables[]`：

| 条件 | 处理 |
|------|------|
| `noScopeRecompositions == recompositionCount` | 归入**正常重组清单**，跳过后续分析 |
| `maxDurationMs > singleRecompDurationThreshold`（默认 10ms） | **无论重组次数多少，必须输出到报告**，进入 Phase 3 深挖 |
| `scopeDistribution` 某 key 计数 > `scopeCountThreshold` | 标记嫌疑，进入 Phase 3 |
| `paramChangeFrequency["#N"] / recompositionCount > paramChangeRateThreshold` | 标记 **RULE-C** 嫌疑（需结合源码判断参数类型） |
| `triggerStates[i].readers.length > stateReadersThreshold` | 标记 **RULE-B** 嫌疑 |

### Phase 3 — Frames 深挖 + 源码确认

**注意**：`profiler_frames.jsonl` 是 JSONL 格式（每行一个独立 JSON 对象），不是单个 JSON 文件。必须逐行读取并 parse，不能整个文件当 JSON 解析。用 Read 工具读取后按行处理。

如果分析对象是 LazyList/LazyGrid/Pager 内的 item 组件，读取 `references/lazylist-rules.md` 了解 item 闭包重建与业务组件 skip 的区别。

逐行读 `profiler_frames.jsonl`，按 `type` 字段分流（`frame` / `touch_context` / `scroll_context`）。

**帧级检查：**

对每个耗时超标帧，按以下流程处理：

1. **先判断是否正常**：
   - 对照 `scroll_context`：若该帧紧跟滚动事件，且帧内事件以 `noScope`（首次组合）为主 → 归为**正常渲染开销**，在报告中简短说明原因，不进入后续分析
   - 若帧内事件数很多但绝大多数是 noScope → 同上，属于列表滑入时的正常批量首次组合

2. **确认是真实问题后，做帧内根因分析**：
   - 找出帧内耗时最长的 composable 事件（`durationMs` 最大的几个）
   - 检查这些组件是否被同一个 State 级联触发（`triggerStates` 相同）
   - 对耗时最高的组件执行完整的链式推理（Step 1-5，同嫌疑项流程）

3. **报告中每个真实问题帧必须包含**：
   - 帧耗时 + 帧内事件数
   - 判断结论（正常 / 有问题）及理由
   - 若有问题：耗时最高的 1-3 个组件的名称、耗时、触发 State
   - 根因分析（参照链式推理 Step 2-4）
   - 优化建议（有具体方向时给出，无法判断时说明需要补充什么信息）

- 单次 `composable_recomposed.durationMs > durationThreshold` → 进入链式推理
- 同帧多组件被同一 State 触发 → 级联嫌疑，分析该 State 的写入时机

**上下文辅助判断（touch/scroll 可用时）：**
- touchBegin~touchEnd 之间某 scope 重组 > 3 次 → 标注「一次点击触发 N 次重组，疑似可优化」
- scroll_context index 变化 + item 重组 ≈ 滑入数量 → 归入正常
- scroll_context index 未变 + item 重组 → 标注「非滚动导致的重组，需分析」

**源码确认（仅对确认嫌疑项）：**

对每个嫌疑项，按以下链式推理步骤深入分析（不得跳过）：

**Step 1 — 定位代码**
取 `sourceLocation`（格式 `FileName.kt:行号`），用 `Glob "**/<FileName>.kt"` 定位文件，读取函数声明及其周围 30 行代码。

**Step 2 — 理解数据信号**
回答：这个组件的 `scopeDistribution` 显示哪个 scope 被反复触发？`triggerStates` 显示是哪个 State 在驱动？`paramChangeFrequency` 中哪个参数每次都在变？把具体数值写出来（如「scope=223833166 被触发 61 次，平均耗时 0.75ms」）。

如果 `paramChangeFrequency` 显示某参数高频变化，**必须先判断变化的本质**：
- **业务数据确实在变**（如滚动时坐标每帧不同、翻页时列表内容更新）→ 根因是写入逻辑，不是类型稳定性问题
- **数据内容没变但引用变了**（每次传入新实例，值相同但 `===` 不等）→ 才是类型稳定性或对象创建问题

两者根因完全不同，不能混淆。

**Step 3 — 追溯根因**
结合代码，回答：这个 State 是谁写入的？在什么时机写入？为什么每次重组都会触发？找到真正的"写入者"（不是"读取者"）。如果 State 是在 `LaunchedEffect` / `onGloballyPositioned` / `snapshotFlow` 等副作用中写入，说明具体的触发时机。

**对于 CompositionLocal 子树重组，额外回答**：传入 `CompositionLocalProvider` 的值是新实例还是缓存实例？`CompositionLocalProvider` 用 `===` 引用比较，即使内容相同，每次传入新实例都会触发整个子树重组。根因可能是「每次重组都 `copy()`/新建对象」，不一定是类型不稳定。

**判断参数变化根因的通用流程**（适用于任何 paramChangeFrequency 高频情况）：
1. 读源码找到参数的调用侧——是谁在传这个参数？
2. 传入的是新建对象（`copy()`、`listOf()`、lambda）还是稳定引用（单例、`remember` 缓存）？
3. 如果是新建对象：检查是否有必要每次新建，还是可以用 `remember` 缓存
4. 如果是稳定引用但还是判定为变化：才考虑类型稳定性（是否有 `var`、`List`、跨模块类型）

**RULE-C 专项：命中 RULE-C 时额外执行**

读取 `references/stability-rules.md` 了解完整的稳定性判断规则，然后：
- 读源码，按声明顺序将 `#N` 对应到具体参数名和类型
- **先判断变化本质**：参数值每次确实不同（业务数据在变）？还是值相同但每次传入新实例（引用不等）？前者不是稳定性问题，后者才考虑类型稳定性
- **`@Stable`/`@Immutable` 注解会覆盖编译器推断**：加了注解的类，编译器信任其稳定，不会因 `var`/`List` 判为不稳定。若加了 `@Stable` 但参数仍 100% 变化，真正原因是「每次传入新实例」而非类型推断问题
- **注意 `@Stable` + var 直接赋值的 bug**：skip 会发生，但界面不更新（显示过时数据），比「不 skip」更危险
- 若确认是「相同值重复创建新实例」，再按类型选方案，详见 `references/optimization-patterns.md`
- **Strong Skipping 已开启时，禁止建议手写 `remember { { ... } }` 包裹 lambda**——手写是多余的。若 lambda 参数仍高频变化，问题在 lambda 捕获的变量稳定性

**Step 4 — 评估影响范围**
回答：这个 State 被几个组件订阅（readers）？这些组件是否都真的需要在每次 State 变化时重组？哪些是可以 skip 的？哪些是必须响应的？

**Step 5 — 提出方案并说明权衡**
读取 `references/optimization-patterns.md` 获取对应规则的优化方案。给出 1-2 个具体优化方案，每个方案必须：
- 提供修改前/后的代码对比
- 说明为什么这个改法能解决问题（从 Compose 运行时机制角度解释）
- 说明可能的副作用或注意事项
- 如果有多个方案，说明推荐哪个，以及在什么场景下选另一个

**推荐加 `@Stable`/`@Immutable` 注解前，必须通过以下两项检查，任一不满足则不推荐：**
1. 类的属性是否满足注解的承诺（`@Immutable` = 构造后永不变；`@Stable` = 变化只通过 MutableState 通知）？若含 `var` 直接赋值，skip 仍会发生（注解让编译器信任），但**界面不会更新**（Compose 不知道值变了），会产生界面 bug
2. 调用方是否会复用实例或传相同引用？若每次都 `copy()`/`new`/`listOf()` 创建新对象传入，注解无法让 skip 发生

如果两项检查通不过，不要推荐加注解，而是从**调用方如何传参**或**数据模型如何设计**角度给出方案。

如果遇到分析受限的情况（无法定位源码、参数索引无法映射等），读取 `references/known-limitations.md` 确认是否属于已知限制，按限制说明处理。

### Phase 4 — 输出报告

读 `references/report-template.md`，生成：

1. **对话摘要**：数据概览 + TOP 3 问题
2. **Markdown 报告**：`recomp-analysis-YYYYMMDD-HHmm.md`，含数据概览、正常重组清单、问题诊断（按严重度降序）、过滤配置声明。严重度评级和排序规则见 `references/detection-rules.md`：**总耗时**（重组次数 × 平均单次耗时）为第一排序维度，次数多但单次耗时极低的问题排在真正耗时高的问题之后。

**报告写作规范（必须遵守）：**

1. **数据概览的帧统计**：只写帧数，不写占比。例如「慢帧：14 帧」，不写「14 帧（占 7.4%）」。
2. **问题描述禁止使用内部术语**：不得在问题描述中出现 `RULE-A`、`RULE-B`、`RULE-C`、`RULE-SCOPE` 等字眼。用用户能理解的语言描述，例如「每次滚动都触发该组件重渲染」而不是「命中 RULE-B」。规则标识只允许出现在**过滤配置声明**段。
3. **上下文描述要区分触发来源**：描述重组次数时必须明确是「一次点击触发 N 次重组」还是「N 帧滚动累计触发 M 次重组」，两者不可混用。若是跨多帧的累计，说明「在 X 帧滚动过程中，该组件共重组 N 次」。
4. **每个问题必须包含两个关键数据**：① 同一 scope 触发的重组次数（或总重组次数）；② 平均单次耗时（avgDurationMs）。缺少任一数据时标注「数据不足，无法评估严重程度」。
5. **问题分析必须有深度**：根因分析要说明「谁在写这个 State、在什么时机写、为什么频繁触发」，不能只说「State 变化导致重组」。优化建议必须提供修改前后的代码对比，并解释为什么这个改法有效，不能只给出结论。
6. **原因不明时直接告知，并给出排查引导**：如果某个问题（如单次耗时异常）通过现有日志和源码无法定位根因，不要猜测或给出模糊结论。直接写：「当前日志不足以确定根因，建议进一步排查」，并给出具体的排查建议，例如：
   - 在该组件函数体内增加耗时打点（`measureTimeMillis`）定位慢在哪个子操作
   - 或直接说「可以告诉我，我来帮你做更深入的分析」
7. **重组次数高但总耗时低的组件不能省略**：所有命中检测规则的组件都必须出现在报告中，不能因为总耗时低就跳过。对于重组次数明显偏高（如 >50 次）但单次耗时极低（<0.5ms）的组件：
   - 仍然列入报告，标注严重度为「低」
   - 说明重组次数和平均耗时
   - 如果未做深入分析，明确注明「单次耗时极低，暂未深入分析，但重组次数偏高，建议关注」
   - 对于次数极高的（如 >100 次），即使耗时低也应做简要根因分析（至少说明是什么 State 在驱动、是否可以减少重组次数）
8. **尊重已有的 `@Stable`/`@Immutable` 注解，不质疑其准确性**：
   - 类已标注 `@Stable` 或 `@Immutable` → 编译器信任它是稳定的，**不要说「标注不准确」「标注是无效的」**
   - 类含 `Map`/`List`/`lambda` 属性但已标注 `@Immutable` → 注解覆盖了编译器推断，这是开发者的有意设计，不是错误
   - Strong Skipping 已开启时，`@Stable` 类含 lambda 属性 → lambda 自动 `remember`，引用稳定，**不要说「lambda 引用稳定性取决于调用方」**
   - 如果加了注解的类参数仍然高频变化，问题在**调用方每次传入新实例**，不是注解有问题。分析方向是调用方如何传参，而不是质疑注解
9. **建议使用 `remember` 缓存对象时，必须分析依赖项**：
   - **禁止**直接建议无 key 的 `remember {}`，除非已确认对象创建不依赖任何外部状态
   - 如果工厂函数内部可能读取 CompositionLocal（如主题色、字体大小、深色模式）→ `remember` 必须带正确的 key（如 `remember(isDarkTheme) { markdownColor() }`），否则主题切换后配置不更新，造成界面 bug
   - 如果**无法确认**工厂函数的内部依赖（没有读到源码）→ 不建议 `remember`，而是建议「检查该函数是否依赖主题等外部状态后再决定是否缓存」
   - 错误示例：`val colors = remember { markdownColor() }` — 如果 `markdownColor()` 读了深色模式，切换主题后颜色不更新
   - 正确示例：`val isDark = isAppInDarkTheme(); val colors = remember(isDark) { markdownColor() }`

### 形态 C：聚焦特定页面

用户说「我只想看 XX 页」时：
> 请在 profiler 面板点「重置」按钮，进入目标页面操作一遍，然后让我分析。

## References

- `references/config.md` — 可配置阈值默认值
- `references/log-format.md` — report.json / frames.jsonl 字段说明
- `references/log-retrieval.md` — 各平台（adb/xcrun/hdc）拉取命令
- `references/detection-rules.md` — 检测规则详细逻辑
- `references/lazylist-rules.md` — LazyList item 重组分析规则（闭包重建 vs 业务组件 skip、错误结论规避）
- `references/stability-rules.md` — Compose 稳定性规则（已实测验证）：编译器推断规则、skip 条件、注解有效/危险场景、Profiler 中的表现差异
- `references/optimization-patterns.md` — 每条规则对应的优化方案和代码样例
- `references/known-limitations.md` — 已知限制（paramChanges 索引无参数名、不稳定类型 scope 重建等）
- `references/report-template.md` — Markdown 报告模板

