You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose LazyColumn初始滚动卡顿不流畅问题排查

LazyColumn初始滑动前10项卡顿问题定位

问题现象

使用LazyColumn加载帖子列表时,初始启动滚动阶段卡顿明显、响应缓慢、流畅度极差,滑动经过列表约第10个条目后,滚动变得十分顺滑无卡顿。已尝试切换release构建模式、应用官方推荐的常规优化方案,问题仍未解决。
对应实现代码如下:

@Composable
fun LazyListState.OnBottomReached(
    onEndReachedThreshold: Int = 0,
    onLoadMore: suspend () -> Unit,
) {
    require(onEndReachedThreshold >= 0) { "buffer cannot be negative, but was $onEndReachedThreshold" }

    LaunchedEffect(Unit) {
        snapshotFlow derivedStateOf@{
            val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
                ?: return@derivedStateOf true
            lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - onEndReachedThreshold
        }
            .collect {
                if (it) {
                    onLoadMore()
                }
            }
    }
}

@Composable
fun TileListView(
    tiles: List<Tile>,
    loadMore: suspend () -> Unit,
    onEndReachedThreshold: Int = 0,
) {

    val listState = rememberLazyListState()
    val tileViewModel: TileViewModel = hiltViewModel()
    LazyColumn(
        state = listState,
        verticalArrangement = Arrangement.spacedBy(5.dp),
        modifier = Modifier.background(Style.colors.appBackground),
    ) {
        items(tiles, key = {
            it.id
        }) { tile ->
            tileViewModel.tileViewFactory.ConstructTileView(tile)
        }
    }

    listState.OnBottomReached(onEndReachedThreshold) {
        loadMore()
    }
}

根因分析

该现象是LazyColumn缓存机制与代码缺陷共同导致的:

  • 核心原因是首次item构造开销阻塞滚动帧:LazyColumn默认仅在item即将进入视口时才启动composition、测量、布局、绘制全流程。初始滑动阶段,前10个左右的item均为首次渲染,tileViewModel.tileViewFactory.ConstructTileView(tile)内的所有逻辑(复杂布局嵌套、图片加载初始化、富文本解析、状态首次计算)都会直接运行在UI线程的滚动帧中,单帧耗时超过16ms(60fps标准)就会掉帧卡顿。当滑过10个左右item后,LazyColumn内部复用池已经缓存了对应类型item的Composable节点与布局测量结果,后续新进入视口的item直接走复用流程,不需要执行完整首次初始化,渲染开销大幅下降,滑动自然恢复顺滑。
  • 分页加载逻辑存在缺陷:OnBottomReached中LaunchedEffect绑定key为Unit,首次进入composition就会触发判断,初始状态下visibleItemsInfo为空时会直接返回true触发onLoadMore,若第一页数据未加载完成就触发下一页分页请求,IO/UI线程资源被抢占,会进一步放大初始阶段卡顿。同时未加distinctUntilChanged会导致滑动过程中重复触发加载,产生不必要开销。
  • 列表缓存配置缺失:items调用未声明contentType,LazyColumn默认将所有item视为同类型处理,不同布局类型的item无法被精准复用,额外增加首次测量开销;LazyColumn默认beyondBoundsPageCount值为0,不会提前缓存视口外的item,滑动时新进入视口的item只能在滚动帧内临时构造。

修复方案

  • 调整列表缓存配置,提前构造视口外item:给LazyColumn设置beyondBoundsPageCount = 2,让列表在空闲帧提前构造视口外2屏高度的item,避免滚动帧内临时产生构造开销。同时给items声明contentType,提升复用效率:
LazyColumn(
    state = listState,
    verticalArrangement = Arrangement.spacedBy(5.dp),
    modifier = Modifier.background(Style.colors.appBackground),
    beyondBoundsPageCount = 2
) {
    items(
        items = tiles,
        key = { it.id },
        contentType = { it.tileType } // 为不同布局类型的item设置唯一标识
    ) { tile ->
        tileViewModel.tileViewFactory.ConstructTileView(tile)
    }
}
  • 重构item渲染逻辑,剥离重型初始化任务:将ConstructTileView中不依赖Compose重组的纯计算逻辑(富文本解析、图片尺寸预计算、装饰元素预创建)提前下沉到数据层,列表拿到的Tile数据直接携带计算完成的渲染参数,不要在item渲染路径中临时计算;图片加载配置预解码、预取策略,避免item首次挂载时才触发图片加载解码。
  • 修复分页加载逻辑缺陷:调整OnBottomReached实现,修正空列表误触发问题,增加防抖判断,绑定正确的效应key:
@Composable
fun LazyListState.OnBottomReached(
    onEndReachedThreshold: Int = 0,
    onLoadMore: suspend () -> Unit,
) {
    require(onEndReachedThreshold >= 0) { "buffer cannot be negative, but was $onEndReachedThreshold" }

    LaunchedEffect(this, onEndReachedThreshold) {
        snapshotFlow {
            val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
                ?: return@snapshotFlow false
            lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - onEndReachedThreshold
        }
            .distinctUntilChanged()
            .collect { reachEnd ->
                if (reachEnd) onLoadMore()
            }
    }
}
  • 定位具体耗时点:使用Compose Tracing配合Perfetto、Layout Inspector的Compose重组统计功能,查看ConstructTileView中每个子项的composition耗时,将单帧耗时超过5ms的逻辑移出UI线程主线程路径。

内容的提问来源于stack exchange,提问作者Jalson1982

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 08:09:15