| name | kuikly-recomposition-analyzer |
| description | 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. |
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:
- 用户直接提供路径 → Read 工具读取
- 检查当前目录约定路径:
./profiler_logs/、./profiler_report.json
- 自动从设备拉取 → 读
references/log-retrieval.md 执行对应平台命令
- 均失败 → 输出引导:
未找到 profiler 日志。请先采集数据:
- 在代码中调用
RecompositionProfiler.start() 开始录制,操作完成后调用 stop()
- 或在 Profiler Overlay 面板点击「开始」录制,操作完成后点击「停止」,再点击「获取报告」
- 采集完成后告诉我文件路径,或提供 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)。
帧级检查:
对每个耗时超标帧,按以下流程处理:
-
先判断是否正常:
- 对照
scroll_context:若该帧紧跟滚动事件,且帧内事件以 noScope(首次组合)为主 → 归为正常渲染开销,在报告中简短说明原因,不进入后续分析
- 若帧内事件数很多但绝大多数是 noScope → 同上,属于列表滑入时的正常批量首次组合
-
确认是真实问题后,做帧内根因分析:
- 找出帧内耗时最长的 composable 事件(
durationMs 最大的几个)
- 检查这些组件是否被同一个 State 级联触发(
triggerStates 相同)
- 对耗时最高的组件执行完整的链式推理(Step 1-5,同嫌疑项流程)
-
报告中每个真实问题帧必须包含:
- 帧耗时 + 帧内事件数
- 判断结论(正常 / 有问题)及理由
- 若有问题:耗时最高的 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 高频情况):
- 读源码找到参数的调用侧——是谁在传这个参数?
- 传入的是新建对象(
copy()、listOf()、lambda)还是稳定引用(单例、remember 缓存)?
- 如果是新建对象:检查是否有必要每次新建,还是可以用
remember 缓存
- 如果是稳定引用但还是判定为变化:才考虑类型稳定性(是否有
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 注解前,必须通过以下两项检查,任一不满足则不推荐:
- 类的属性是否满足注解的承诺(
@Immutable = 构造后永不变;@Stable = 变化只通过 MutableState 通知)?若含 var 直接赋值,skip 仍会发生(注解让编译器信任),但界面不会更新(Compose 不知道值变了),会产生界面 bug
- 调用方是否会复用实例或传相同引用?若每次都
copy()/new/listOf() 创建新对象传入,注解无法让 skip 发生
如果两项检查通不过,不要推荐加注解,而是从调用方如何传参或数据模型如何设计角度给出方案。
如果遇到分析受限的情况(无法定位源码、参数索引无法映射等),读取 references/known-limitations.md 确认是否属于已知限制,按限制说明处理。
Phase 4 — 输出报告
读 references/report-template.md,生成:
- 对话摘要:数据概览 + TOP 3 问题
- Markdown 报告:
recomp-analysis-YYYYMMDD-HHmm.md,含数据概览、正常重组清单、问题诊断(按严重度降序)、过滤配置声明。严重度评级和排序规则见 references/detection-rules.md:总耗时(重组次数 × 平均单次耗时)为第一排序维度,次数多但单次耗时极低的问题排在真正耗时高的问题之后。
报告写作规范(必须遵守):
- 数据概览的帧统计:只写帧数,不写占比。例如「慢帧:14 帧」,不写「14 帧(占 7.4%)」。
- 问题描述禁止使用内部术语:不得在问题描述中出现
RULE-A、RULE-B、RULE-C、RULE-SCOPE 等字眼。用用户能理解的语言描述,例如「每次滚动都触发该组件重渲染」而不是「命中 RULE-B」。规则标识只允许出现在过滤配置声明段。
- 上下文描述要区分触发来源:描述重组次数时必须明确是「一次点击触发 N 次重组」还是「N 帧滚动累计触发 M 次重组」,两者不可混用。若是跨多帧的累计,说明「在 X 帧滚动过程中,该组件共重组 N 次」。
- 每个问题必须包含两个关键数据:① 同一 scope 触发的重组次数(或总重组次数);② 平均单次耗时(avgDurationMs)。缺少任一数据时标注「数据不足,无法评估严重程度」。
- 问题分析必须有深度:根因分析要说明「谁在写这个 State、在什么时机写、为什么频繁触发」,不能只说「State 变化导致重组」。优化建议必须提供修改前后的代码对比,并解释为什么这个改法有效,不能只给出结论。
- 原因不明时直接告知,并给出排查引导:如果某个问题(如单次耗时异常)通过现有日志和源码无法定位根因,不要猜测或给出模糊结论。直接写:「当前日志不足以确定根因,建议进一步排查」,并给出具体的排查建议,例如:
- 在该组件函数体内增加耗时打点(
measureTimeMillis)定位慢在哪个子操作
- 或直接说「可以告诉我,我来帮你做更深入的分析」
- 重组次数高但总耗时低的组件不能省略:所有命中检测规则的组件都必须出现在报告中,不能因为总耗时低就跳过。对于重组次数明显偏高(如 >50 次)但单次耗时极低(<0.5ms)的组件:
- 仍然列入报告,标注严重度为「低」
- 说明重组次数和平均耗时
- 如果未做深入分析,明确注明「单次耗时极低,暂未深入分析,但重组次数偏高,建议关注」
- 对于次数极高的(如 >100 次),即使耗时低也应做简要根因分析(至少说明是什么 State 在驱动、是否可以减少重组次数)
- 尊重已有的
@Stable/@Immutable 注解,不质疑其准确性:
- 类已标注
@Stable 或 @Immutable → 编译器信任它是稳定的,不要说「标注不准确」「标注是无效的」
- 类含
Map/List/lambda 属性但已标注 @Immutable → 注解覆盖了编译器推断,这是开发者的有意设计,不是错误
- Strong Skipping 已开启时,
@Stable 类含 lambda 属性 → lambda 自动 remember,引用稳定,不要说「lambda 引用稳定性取决于调用方」
- 如果加了注解的类参数仍然高频变化,问题在调用方每次传入新实例,不是注解有问题。分析方向是调用方如何传参,而不是质疑注解
- 建议使用
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 报告模板