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

Paging3滚动加载缓慢问题排查与性能优化咨询

使用Paging3实现滚动加载时的性能问题咨询

问题背景

我在使用Paging3实现滚动加载时遇到加载缓慢的问题,根源是需要发起多次网络请求。相关代码如下:

原始PagingSource与Repository代码

class RepoResultPagingSource(
private val repository: Repository,
private val query: String
) : PagingSource<Int, Result>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Result> {
    try {
        val page = params.key ?: 1
        // 网络请求获取仓库列表
        val repos = repository.getRepositories(query, page, PAGE_SIZE)

        val results = repos.map { repo ->
            // 为每个仓库请求其顶级贡献者
            val contributor = repository.getTopContributor(repo.owner.username, repo.reponame)
            Result(repo.owner.url, repo.reponame, contributor.username?: "")
        }
        return LoadResult.Page(
            data = repoResults,
            prevKey = if (page == 1) null else page-1,
            nextKey = page + 1
        )
    } catch (e: Exception) {
        return LoadResult.Error(e)
    }
}

class Repository(private val api: RepositoryApi,
             private val dispatcher: CoroutineDispatcher = Dispatchers.IO) {
    suspend fun getRepositories(query: String, page: Int, perPage: Int): List<Repository> 
    {
       return withContext(dispatcher) {
           api.search(query, page, perPage).repositories
       }
    }

    suspend fun getTopContributor(ownerName: String, repoName: String): Contributor {
        return withContext(dispatcher) {
            api.getContributors(ownerName, repoName, 1).first()
        }
    }
}

我发现load方法处于阻塞状态,因为Paging库通过runBlocking调用它,相关源码片段如下:

@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public fun <K : Any, T : Any> create(
        pagingSource: PagingSource<K, T>,
        initialPage: PagingSource.LoadResult.Page<K, T>?,
        coroutineScope: CoroutineScope,
        notifyDispatcher: CoroutineDispatcher,
        fetchDispatcher: CoroutineDispatcher,
        boundaryCallback: BoundaryCallback<T>?,
        config: Config,
        key: K?
    ): PagedList<T> {
        val resolvedInitialPage = when (initialPage) {
            null -> {
                // 兼容路径 - 立即执行初始加载,因为调用方未执行此操作。这里会阻塞,但仅用于旧路径。
                val params = PagingSource.LoadParams.Refresh(
                    key,
                    config.initialLoadSizeHint,
                    config.enablePlaceholders,
                )
                runBlocking {
                    val initialResult = pagingSource.load(params)
                    when (initialResult) {
                        is PagingSource.LoadResult.Page -> initialResult
                        is PagingSource.LoadResult.Error -> throw initialResult.throwable
                        is PagingSource.LoadResult.Invalid ->
                            throw IllegalStateException(
                                "Failed to create PagedList. The provided PagingSource " +
                                    "returned LoadResult.Invalid, but a LoadResult.Page was " +
                                    "expected. To use a PagingSource which supports " +
                                    "invalidation, use a PagedList builder that accepts a " +
                                    "factory method for PagingSource or DataSource.Factory, " +
                                    "such as LivePagedList."
                            )
                    }
                }
            }
            else -> initialPage
        }
        return ContiguousPagedList(
            pagingSource,
            coroutineScope,
            notifyDispatcher,
            fetchDispatcher,
            boundaryCallback,
            config,
            resolvedInitialPage,
            key
        )
    }

于是我尝试注入viewModelScope启动非阻塞协程,修改后的load方法代码如下:

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Result> {
    try {
        return scope.async {
            val page = params.key ?: 1
            // 网络请求获取仓库列表
            val repos = repository.getRepositories(query, page, 10)

            val results = repos.map { repo ->
                // 为每个仓库请求其顶级贡献者
                val contributor = repository.getTopContributor(repo.owner.username, 
repo.reponame)
                Result(repo.owner.url, repo.reponame, contributor.username?: "")
            }
            return@async LoadResult.Page(
                data = repoResults,
                prevKey = if (page == 1) null else page-1,
                nextKey = page + 1
            )
       }.await() as LoadResult<Int, Result>
    } catch (e: Exception) {
        return LoadResult.Error(e)
    }
}

但滚动缓慢的问题依然存在,特此咨询两个问题:

  1. 参考相关问题,即使getRepositories和getTopContributor切换到Dispatchers.IO,load方法默认是否仍以阻塞方式运行?
  2. 每页需要发起1次仓库列表请求,再为每个仓库发起1次贡献者请求,共page_size+1次请求,如何优化滚动加载性能?我的Pager配置如下:
val pagingData: Flow<PagingData<RepoResult>> = Pager(
    config = PagingConfig(pageSize = PAGE_SIZE, prefetchDistance = 4 * PAGE_SIZE),
    pagingSourceFactory = { RepoResultPagingSource(repo, QUERY, viewModelScope) }
).flow.cachedIn(viewModelScope)

解答

问题1解答

首先明确:load方法本身是挂起函数,它的执行是否阻塞取决于调用方的上下文。你看到的runBlocking调用属于Paging的旧版兼容路径,而在Paging3的标准用法(Pager+Flow<PagingData>)中,load方法是在Paging内部指定的fetchDispatcher(默认是Dispatchers.IO)中执行的,并不会被runBlocking阻塞。

你修改后的代码里用scope.async { ... }.await()其实毫无意义——async会在指定协程上下文启动任务,但await会立即挂起当前协程直到任务完成,本质上和直接执行代码没有区别,反而多了一层协程调度的开销。

问题2解答

核心性能瓶颈在于串行发起page_size次贡献者请求,单页加载时间被大幅拉长。可以从以下几个方向优化:

1. 并行发起贡献者请求

利用协程并发能力,将串行的map改为并行执行,减少总请求耗时:

// 在load方法中替换原有的results生成逻辑
val results = coroutineScope {
    repos.map { repo ->
        async {
            val contributor = repository.getTopContributor(repo.owner.username, repo.reponame)
            Result(repo.owner.url, repo.reponame, contributor.username ?: "")
        }
    }.awaitAll()
}

这里用coroutineScope管理所有子协程,确保任一子协程出错时取消其他协程;awaitAll()会等待所有并发请求完成,总耗时近似于单个请求的最长耗时,而非串行的时间总和。

2. 后端接口优化(最优解)

协调后端开发,新增接口或在现有仓库列表接口中批量返回每个仓库的顶级贡献者信息,把page_size+1次请求压缩为1次,这是性能提升最显著的方案。

3. 本地缓存优化

使用Room等本地数据库做缓存:

  • 首次加载仓库列表时,并行请求贡献者数据并缓存到本地
  • 后续加载或分页时,先从本地缓存读取贡献者数据,同时后台发起网络请求更新缓存
  • 用户滚动时能快速看到缓存数据,大幅提升感知性能

4. 调整Pager预加载配置

你当前的prefetchDistance = 4 * PAGE_SIZE已经较大,可结合实际情况微调:

  • 若单页加载耗时过长,适当增大预加载距离,让Paging提前发起下一页请求
  • 调整initialLoadSize,让初始加载更多数据,减少用户早期滚动时的等待次数

5. 避免不必要的协程调度

如果你的RepositoryApi是用Retrofit定义的,其挂起函数默认会在后台线程执行,此时可以去掉Repository中的withContext(Dispatchers.IO),避免重复的线程切换开销:

class Repository(private val api: RepositoryApi) {
    suspend fun getRepositories(query: String, page: Int, perPage: Int): List<Repository> {
       return api.search(query, page, perPage).repositories
    }

    suspend fun getTopContributor(ownerName: String, repoName: String): Contributor {
        return api.getContributors(ownerName, repoName, 1).first()
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 08:51:06