42-RDB查询拖慢首屏-用索引分页和快照缓存拆开 第42篇RDB 查询拖慢首屏用索引分页和快照缓存拆开摘要本地 RDB 数据多了以后首屏卡顿经常不是 UI 写得慢而是页面一出现就做了全表排序、统计和复杂过滤。更稳的做法是把首屏快照、分页查询和统计重算拆开首屏先读轻量快照列表分页加载统计在写入后维护或后台刷新。一个记录类应用首屏从 300ms 变成 2 秒表面看是列表项太多实际是首页 aboutToAppear 里同时查全部记录、按时间排序、计算月统计、再渲染卡片。数据少时没问题半年后本地记录多了首屏就被 RDB 查询拖住。修复时不能只优化组件得把查询路径拆开。这篇文章解决四个实际问题区分首屏快照、列表分页、统计重算三类读请求。给常用排序和筛选字段补索引不在页面里全量排序。写入后更新轻量摘要首屏先用摘要展示。用耗时日志证明慢在查询还是渲染。先把首页查询拆成三类首屏慢时不要只看列表组件。先列出首页到底读了哪些数据每一类数据是否都必须阻塞首帧。位置现象风险处理方向今日摘要只需要总数和最近一条全表扫描太重读 SummaryStore记录列表需要分页一次查全部卡住limit/offset 或游标月统计计算量大不该阻塞首屏后台刷新筛选排序按时间和类型查无索引导致慢迁移补索引这一步的价值是先把问题放回运行链路。只要能确认问题停在哪个位置后面就不用靠猜测改页面。页面不能自己拼复杂查询页面应该向 Service 要 HomeSnapshot 和 PageResult而不是直接拿 repository 全量数据后自己排序过滤。模块应该负责不应该负责HomePage快照、分页结果、加载态SQL 条件HomeService组合首屏数据RDB 连接细节RecordRepository分页和筛选查询卡片展示SummaryStore轻量摘要缓存业务按钮Migration索引和版本升级页面生命周期边界定清后代码就不会在页面、服务和回调之间来回复制同一段判断。后续新增场景也能先判断应该落在哪一层。首屏快照只放轻量字段exportinterfaceHomeSnapshot{todayCount:numberlatestTitle:stringlatestTime:numbermonthTotal:numbergeneratedAt:number}exportinterfacePageQuery{cursorTime:numberpageSize:numbertypeFilter:string}exportinterfacePageResultT{items:T[]nextCursorTime:numberhasMore:boolean}首屏快照是为快速展示设计的不放完整列表。分页查询用 cursor 表达下一页位置避免每次都从头读到尾。这类模型最好放在models或common中页面、Service 和仓储都使用同一份类型避免各层用字符串互相猜。Repository 只提供分页读exportclassRecordRepository{asyncqueryPage(query:PageQuery):PromisePageResultRecordItem{constrowsawaitthis.queryRows({beforeTime:query.cursorTime,limit:query.pageSize1,typeFilter:query.typeFilter})constitemsrows.slice(0,query.pageSize)constlastitems[items.length-1]return{items,nextCursorTime:last?.createdAt??0,hasMore:rows.lengthquery.pageSize}}}Repository 不返回全量列表。多查一条用来判断hasMore页面只关心下一页 cursor不需要知道底层查询细节。Service 的目标不是把所有逻辑都塞进去而是把跨页面、跨生命周期或需要持久化的判断集中起来。页面只表达用户动作。首页先显示快照再加载列表Componentstruct RecordHomePage{Statesnapshot:HomeSnapshotcreateEmptyHomeSnapshot()Staterecords:RecordItem[][]StateloadingMore:booleanfalseprivateservice:HomeRecordServicecreateHomeRecordService()asyncaboutToAppear():Promisevoid{this.snapshotawaitthis.service.loadHomeSnapshot()awaitthis.loadFirstPage()}privateasyncloadFirstPage():Promisevoid{constpageawaitthis.service.loadRecords({cursorTime:Date.now(),pageSize:20,typeFilter:})this.recordspage.items}}首屏不会等月统计重新计算完成才显示。快照先给用户一个稳定首页列表再分页补齐。页面层要控制展示和交互节奏但不应该拥有底层事实。这样页面重建、横竖屏变化或返回前台时都能重新从 Service 拿到可信结果。写入后同步更新摘要exportclassHomeRecordService{constructor(privatereadonlyrepository:RecordRepository,privatereadonlysummaryStore:SummaryStore){}asyncsaveRecord(input:SaveRecordInput):Promisevoid{constrecordnormalizeRecord(input)awaitthis.repository.save(record)awaitthis.summaryStore.updateAfterWrite(record)}asyncloadHomeSnapshot():PromiseHomeSnapshot{returnthis.summaryStore.loadSnapshot()}}摘要不是页面临时算出来的而是在写入路径维护。这样下一次打开首页时能直接读快照不用全表重算。实际项目里很多问题不是第一次进入页面暴露而是在冷启动、返回前台、页面重建或回调晚到时暴露。把这段生命周期补上修复才算闭环。慢查询要留下耗时证据情况页面表现工程处理首屏快照慢记录 SummaryStore 耗时检查是否退回全表统计分页慢记录 cursor 和 pageSize检查索引和过滤字段写入后摘要错对比原始记录重算修 updateAfterWrite列表渲染慢查询快但首帧慢再看组件和图片兜底路径不是可有可无。用户遇到异常时页面至少要给出当前状态、可执行动作和可排查线索。排查时找全量查询和排序rg-nqueryAll|findAll|ORDER BY|sort\(|filter\(entry features common rg-nlimit|offset|cursor|pageSize|SummaryStore|HomeSnapshotentry features common rg-naboutToAppear|onShown|loadHome|loadRecordsentry features common第一组命令找旧路径第二组命令找新边界第三组命令看生命周期或回调位置。命中后不要只看字段名要判断它是否仍在直接改页面状态。索引和摘要要跟版本迁移一起走很多首屏慢的问题不是某一次查询写错而是版本演进时没有同步维护本地结构。新增筛选条件后页面开始按type、createdAt、updatedAt查但迁移里没有补索引新增首页统计后写入路径没有更新摘要只能在首页临时全量重算。变更需要同步处理不处理的后果新增时间排序给时间字段补索引数据多后排序拖慢首屏新增类型筛选给类型和时间组合查询设计路径筛选页每次全表过滤新增首页统计写入后更新摘要首页打开时全量重算新增导入功能导入结束后重建摘要导入后统计和列表不一致所以 RDB 优化不是最后才做的性能补丁。每次本地数据结构变化都应该顺手问一句首屏读什么、列表怎么分页、摘要在哪里更新、旧版本升级后这些结构是否存在。验证清单验证项预期结果千条记录冷启动先出现快照再分页出列表新增记录首页摘要立即更新加载更多每次只追加一页筛选切换查询耗时可记录无记录空状态不触发全量统计循环这些验证最好在真机或模拟器里按顺序走一遍。只看源码很难发现冷启动、重启和回调晚到这类问题。常见问题和处理方式现象常见原因处理方式首屏 2 秒全量查加统计阻塞快照和分页拆开越用越慢无索引且全表排序迁移补索引统计不准页面手动改计数写入后统一更新摘要列表卡顿一次渲染全部记录分页加载如果现象没有出现在表里仍然建议按“输入来源 - 中间状态 - 持久化或页面展示 - 失败兜底”的顺序排查。小结首屏不要背负全量数据RDB 首屏慢要先拆读路径。首页读快照列表分页统计在写入或后台维护慢查询有日志。这样数据增长后首屏仍然是一个可控链路而不是所有本地数据的总入口。|| 列表卡顿 | 一次渲染全部记录 | 分页加载 |如果现象没有出现在表里仍然建议按“输入来源 - 中间状态 - 持久化或页面展示 - 失败兜底”的顺序排查。小结首屏不要背负全量数据RDB 首屏慢要先拆读路径。首页读快照列表分页统计在写入或后台维护慢查询有日志。这样数据增长后首屏仍然是一个可控链路而不是所有本地数据的总入口。