perf: 延迟 YearGridView 文本测量到第二帧,补充性能分析文档

YearGridView 首帧 168ms 中约 24ms 来自 remember 同步文本测量。
将 dayLayouts/titleLayouts/weekdayLayouts 改为 produceState,
首帧 Canvas 渲染为空(sharedElement 结构不变),第二帧填充内容。

DEVELOPMENT.md 补充已知性能瓶颈分析和 Baseline Profile 覆盖建议。
This commit is contained in:
meyou 2026-05-25 23:19:58 +08:00
parent ce84c614de
commit 6fac313fdf
No known key found for this signature in database
2 changed files with 49 additions and 21 deletions

View File

@ -27,8 +27,36 @@
trace 中包含自定义标记: trace 中包含自定义标记:
- `MonthView:Compose` — 月视图重组 - `MonthView:Compose` — 月视图重组
- `YearView:Compose` — 年视图重组 - `YearView:Compose` — 年视图重组
- `YearGridView:*` — 年视图网格组合(首帧耗时关键指标)
- `generateMiniMonthDays:*` — 月份网格计算
- `VM:collapseProgress` — 折叠动画 - `VM:collapseProgress` — 折叠动画
- `getMonthDays:*` — 月份网格计算
### 已知性能瓶颈
#### AnimatedGif 持续解码
`AnimatedGif` 中的 250×250 WebP 动画在 `repeatCount` 未限制时会以 11-14 FPS 无限循环解码,
持续消耗 CPU/GPU。已通过 `ImageOptions { repeatCount(2) }` 限制播放 3 次后停止。
#### YearGridView 首帧 168ms
切换到年视图时,`YearGridView` 首次组合需要:
- 创建 12 个 MiniMonth composable`sharedElement` + `clickable` + `Canvas` 等 modifier 节点)
- 124 次文本测量93 日期 + 24 标题 + 7 星期)
- `SharedTransitionLayout` 注册 12 个共享元素
文本测量已通过 `produceState` 延迟到第二帧执行,首帧 Canvas 渲染为空。
剩余 ~140ms 是 12 个 composable 节点创建 + layout 的 Compose 运行时开销,
需要通过 **Baseline Profile** 预编译相关类来优化。
确保 `macrobenchmark``BaselineProfileGenerator` 覆盖以下路径:
- 冷启动 → FAB → 年视图(触发 YearGridView 首次组合)
- 年视图 → 点击任意月份返回月视图(触发 sharedElement 转场)
#### CalendarPager 预加载
`beyondViewportPageCount` 已设为 0避免翻页时预加载相邻月页导致的帧丢失。
如需恢复预加载,注意 `compose:lazy:prefetch` 可能产生 400-700ms 卡顿。
## Baseline Profile ## Baseline Profile

View File

@ -25,7 +25,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -109,29 +112,24 @@ fun YearGridView(
val textMeasurer = rememberTextMeasurer() val textMeasurer = rememberTextMeasurer()
val dayTextStyle = remember { TextStyle(fontSize = 8.sp, lineHeight = 12.sp) } val dayTextStyle = remember { TextStyle(fontSize = 8.sp, lineHeight = 12.sp) }
// P0-D: 预测量 1..31 × 3 种颜色 = 93 个 TextLayoutResult // 延迟文本测量到下一帧,避免首帧阻塞
val dayLayouts = remember(textMeasurer, dayTextStyle, colors) { val dayLayouts by produceState<Map<Pair<Int, Color>, androidx.compose.ui.text.TextLayoutResult>?>(null, textMeasurer, dayTextStyle, colors) {
val days = 1..31 val days = 1..31
val colorList = listOf(colors.day, colors.todayText, colors.otherMonth) val colorList = listOf(colors.day, colors.todayText, colors.otherMonth)
days.flatMap { d -> value = days.flatMap { d ->
colorList.map { c -> colorList.map { c ->
(d to c) to textMeasurer.measure(d.toString(), dayTextStyle.copy(color = c)) (d to c) to textMeasurer.measure(d.toString(), dayTextStyle.copy(color = c))
} }
}.toMap() }.toMap()
} }
// P0-H: 预测量月份标题(选中/非选中两种颜色) val titleLayouts by produceState<Map<Pair<Int, Boolean>, androidx.compose.ui.text.TextLayoutResult>?>(null, textMeasurer, colors) {
val titleLayouts = remember(textMeasurer, colors) { value = (1..12).flatMap { month ->
(1..12).flatMap { month ->
val text = "${month}" val text = "${month}"
listOf( listOf(
(month to true) to textMeasurer.measure( (month to true) to textMeasurer.measure(
text, text,
TextStyle( TextStyle(fontSize = 10.sp, color = colors.titleSelected, fontWeight = FontWeight.Bold)
fontSize = 10.sp,
color = colors.titleSelected,
fontWeight = FontWeight.Bold
)
), ),
(month to false) to textMeasurer.measure( (month to false) to textMeasurer.measure(
text, text,
@ -141,9 +139,8 @@ fun YearGridView(
}.toMap() }.toMap()
} }
// P0-H: 预测量星期标签 val weekdayLayouts by produceState<Map<String, androidx.compose.ui.text.TextLayoutResult>?>(null, textMeasurer, colors) {
val weekdayLayouts = remember(textMeasurer, colors) { value = WEEKDAY_LABELS.associateWith { label ->
WEEKDAY_LABELS.associateWith { label ->
textMeasurer.measure(label, TextStyle(fontSize = 8.sp, color = colors.weekday)) textMeasurer.measure(label, TextStyle(fontSize = 8.sp, color = colors.weekday))
} }
} }
@ -213,9 +210,9 @@ private fun MiniMonth(
today: LocalDate, today: LocalDate,
days: List<MiniDayData>, days: List<MiniDayData>,
colors: MiniMonthColors, colors: MiniMonthColors,
dayLayouts: Map<Pair<Int, Color>, androidx.compose.ui.text.TextLayoutResult>, dayLayouts: Map<Pair<Int, Color>, androidx.compose.ui.text.TextLayoutResult>?,
titleLayouts: Map<Pair<Int, Boolean>, androidx.compose.ui.text.TextLayoutResult>, titleLayouts: Map<Pair<Int, Boolean>, androidx.compose.ui.text.TextLayoutResult>?,
weekdayLayouts: Map<String, androidx.compose.ui.text.TextLayoutResult>, weekdayLayouts: Map<String, androidx.compose.ui.text.TextLayoutResult>?,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@ -240,10 +237,13 @@ private fun MiniMonth(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Canvas(modifier = Modifier.fillMaxWidth().height(totalHeight)) { Canvas(modifier = Modifier.fillMaxWidth().height(totalHeight)) {
val dl = dayLayouts ?: return@Canvas
val tl = titleLayouts ?: return@Canvas
val wl = weekdayLayouts ?: return@Canvas
val cellWidth = size.width / 7f val cellWidth = size.width / 7f
// 1. 绘制标题 // 1. 绘制标题
val titleLayout = titleLayouts[month to isSelected]!! val titleLayout = tl[month to isSelected]!!
drawText( drawText(
textLayoutResult = titleLayout, textLayoutResult = titleLayout,
topLeft = Offset( topLeft = Offset(
@ -255,7 +255,7 @@ private fun MiniMonth(
// 2. 绘制星期行 // 2. 绘制星期行
val weekdayY = titleHeightPx + titleToWeekdayGapPx val weekdayY = titleHeightPx + titleToWeekdayGapPx
WEEKDAY_LABELS.forEachIndexed { i, label -> WEEKDAY_LABELS.forEachIndexed { i, label ->
val layout = weekdayLayouts[label]!! val layout = wl[label]!!
drawText( drawText(
textLayoutResult = layout, textLayoutResult = layout,
topLeft = Offset( topLeft = Offset(
@ -292,7 +292,7 @@ private fun MiniMonth(
} }
if (dayNum > 0) { if (dayNum > 0) {
dayLayouts[dayNum to textColor]?.let { layoutResult -> dl[dayNum to textColor]?.let { layoutResult ->
drawText( drawText(
textLayoutResult = layoutResult, textLayoutResult = layoutResult,
topLeft = Offset( topLeft = Offset(