- 新增 RDEPUBTextStatisticsCoordinator:后台单线程按章渲染统计全书字符数,footer 显示精确阅读百分比;统计未就绪时(含 PageMap 未覆盖的 loading 页)显示"计算中",不再回退旧页码 - 统计结果按 bookID + renderSignature + 章节内容哈希做磁盘缓存,布局/字号变化自动失效;缓存命中不创建 renderer - 移除"0.8 秒翻页时间窗轮询让路"逻辑,后续由事件驱动的交互加载门替代(见方案文档 5.6 节) - 新增 Doc/FeatureSolution/TEXT_STATISTICS_PIPELINE_OPTIMIZATION.md:三条渲染流水线收敛为"交互 + 生产者"、交互加载门、全局后台渲染预算池、后台渲染登记表、两阶段近似百分比、自适应并发的完整优化方案与验收标准 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
24 KiB
阅读进度统计与后台渲染流水线优化方案
创建:2026-07-03 状态:方案评审中(两阶段近似百分比、自适应并发已确认纳入正式范围) 范围:textReflowable 阅读模式(章节级运行时)
1. 背景与问题
阅读百分比统计(RDEPUBTextStatisticsCoordinator)上线后,书籍打开初期(统计计算进行中)用户翻页明显卡顿。此前已做的缓解手段(统计降为单线程、缓存命中不创建 renderer)未根治问题。
根因不是统计线程本身,而是打开书后同时存在三条互不知晓的章节渲染流水线,它们做的是同一种重活(DTCoreText HTML → NSAttributedString 构建),同一章最多被渲染三遍,并在首屏后的前几秒集中抢占 CPU——恰好是用户开始翻页的时刻。
1.1 三条流水线现状
| 流水线 | 入口 | 队列 / 并发 | 产出 | 缓存 |
|---|---|---|---|---|
| 交互加载(导航/预览/预取) | RDEPUBChapterLoader |
chapterLoadQueue(userInitiated,串行) |
RuntimeChapter + ChapterSummary | RDEPUBChapterSummaryDiskCache |
| 窗口元数据解析 | RDEPUBMetadataParseWorker |
独立 OperationQueue(utility,并发 = 全部 CPU 核) | ChapterSummary + PageMap 增量刷新 | 同上(同 cacheKey) |
| 全书字符统计 | RDEPUBTextStatisticsWorker |
独立 OperationQueue(utility,串行) | 每章字符数 → 阅读百分比 | RDEPUBTextStatisticsDiskCache(独立,仅字符数) |
三者的缓存 key 均为同一个 RDEPUBChapterCacheKey(bookID + spineIndex + renderSignature + contentHash),但统计流水线不读 summary 缓存,summary 生产者也不通知统计。
1.2 打开书流程的具体问题
按 RDEPUBReaderPaginationCoordinator.paginatePublication 的执行顺序:
- 统计流水线最先启动(
paginatePublication内先于首章渲染调用startTextStatistics),从 spine 0 按顺序渲染全书,与首屏关键路径抢 CPU;渲染顺序与用户阅读位置无关。 - 首屏串行等待 1~3 章:utility 队列上信号量同步等 anchor 章渲染;anchor 章不足 2 页时再同步加载相邻章(
loadInitialInteractiveRuntimeChapters),之后才回主线程出首屏。 - 首屏一出即 CPU 风暴:出首屏的同一个
main.async块内连续触发窗口 prefetch、MetadataParseWorker(全核并发)启动;统计已在跑。metadata worker 仅在启动时等一次 0.8 秒交互冷却,之后章间不让路。 MetadataParseWorker在主线程初始化:init 中同步读取覆盖窗口内每章 HTML 并计算 sha256(contentHashBySpineIndex),主线程磁盘 I/O + 哈希,首帧后主线程可见占用。- 翻页卡顿的直接机制:翻到未加载章节时主线程经
loadChapterSynchronouslyForMigration信号量同步等chapterLoadQueue渲染,而 CPU 已被两条全书/窗口流水线占满,该次渲染耗时被放大 2~3 倍。 - 架构缺口:首次打开时
MetadataParseWorker只覆盖 anchor 附近窗口(metadataCoverageSpineIndices),全书 PageMap 靠翻页时 3 章一批扩展(extendPartialBookPageMapIfNeeded)——当前架构中不存在全书 summary 生产者,统计因此只能自己渲染全书,造成整书级别的重复劳动。
1.3 关键代码位置
Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swiftpaginatePublication:统计过早启动(约 L32)paginateTextPublication→loadInitialInteractiveRuntimeChapters:首屏串行多章(L264)metadataCoverageSpineIndices:窗口覆盖范围(L341)
Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBMetadataParseWorker.swift- init 主线程哈希(L95~105);
workerCount = activeProcessorCount(L92);启动时单次交互冷却(L187)
- init 主线程哈希(L95~105);
Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBTextStatisticsCoordinator.swift:独立渲染 + 独立缓存Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swiftloadChapterSynchronouslyForMigration:主线程信号量同步等待(L182)
Sources/RDReaderView/EPUBUI/RDEPUBReaderController+DataSource.swifttextProgressFooterText:百分比消费点(L142~153)
Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterRuntimeStore.swift:chapterLoadQueue、pending load 状态
2. 关键设计事实与约束
方案的取舍建立在以下三个事实上,评审时先对齐。
2.1 一个百分比数字由两条流水线各出一半
Footer 百分比的计算是 statistics.percent(spineIndex:, chapterOffset:),两个输入来源不同:
- 分子(当前位置):
page.contentRange.location + length,即当前页在本章排版后字符串中的结束偏移——由章节运行时(交互加载/预取)的分页产出; - 分母(全书总字符数)与本章绝对起点
absoluteCharacterStart:由统计流水线产出。
两者必须在同一坐标系:DTCoreText 渲染后 attributedString 的字符长度。这带来一条硬约束:精确统计不能用"HTML 去标签的文本长度"替代(坐标系与分子不一致);近似值只允许用于分母侧的估计(见阶段 4),分子永远来自真实分页。
2.2 预加载不可被统计扫描替代
两边虽然做同样的渲染动作,产物和时限完全不同:
- 预加载的产物是驻留内存的可绘制对象(
RDEPUBRuntimeChapter:排版串 + layouter + 分页),翻页瞬间直接取用;统计的产物只是一个数字,昂贵的排版结果随 autoreleasepool 丢弃。 - summary 缓存命中仍需一次完整 DTCoreText 渲染才能显示(
buildChapterFromCachedPageRanges只免分页、不免渲染)。真正保证翻页零等待的是 prefetch 把 RuntimeChapter 提前放进内存,这一角色无可替代。 - 预加载是带截止时间的需求预测(用户 30 秒后会翻到下一章);统计是无截止时间的全量扫描。
因此去重方向是统计不再渲染(消费预加载产物 + 补渲染远端章节),而不是砍掉预加载。
2.3 结构性瓶颈:分母需要全书每章齐全
百分比的分母是全书字符总和,一章不齐数字就出不来——"footer 从'计算中'变成百分比"的时刻恒等于全书扫描完成的时刻,与谁来渲染、开几个线程无关(提速只能压缩这个时刻,不能消除)。要突破这个语义,唯一路径是近似分母先行(阶段 4)。
3. 优化目标
- 全书每章最多渲染一次(同一 renderSignature 下)。
- 交互(翻页/跳转)绝对优先:任何后台渲染在交互加载进行期间让路,让路机制为事件驱动而非时间窗轮询。
- 阅读百分比首次可用时间压到亚秒级(近似值),精确值完成时间显著优于现状(消除重复渲染后后台总量减半)。
- 首屏时间不劣化,目标改善(去掉首屏路径上的非必要串行工作)。
4. 方案总览
将"三条流水线"收敛为"一条交互流水线 + 一条后台生产流水线":
交互流水线(不变,最高优先级,不受后台预算约束)
RDEPUBChapterLoader / chapterLoadQueue (userInitiated)
navigation / preview / prefetch
渲染完成 → RuntimeChapter + Summary 写盘
└──(新增)characterCount 上报统计聚合器
后台生产流水线(由统计流水线改造而来)
QoS .utility / 并发自适应(1~预算上限) / 章间事件驱动让路
排序:距当前阅读位置由近到远(复用 backgroundPriorityManager)
每章:Summary 缓存命中 → 直接取字符数(零渲染)
miss → 渲染 + 分页 → 写 Summary + 上报字符数
产出:① 全书 PageMap 增量收敛(总页数越来越准)
② 全书字符统计(阅读百分比,近似值先行、精确值收敛)
共享组件
交互加载门(5.6):navigation/preview 加载期间后台全体暂停
全局后台渲染预算池(5.7):所有后台渲染共享并发上限
后台渲染登记表(5.8):后台流水线间的飞行中去重
核心思想:统计流水线不再是"为了一个数字渲染全书"的消费者,而是升级为架构中缺失的"全书 summary 生产者"——一次渲染两个产品(翻页要分页结果,统计要字符数)。chapterLoader 后续翻页大概率命中 summary 缓存,翻页反而更快。
MetadataParseWorker 的窗口覆盖职责与新生产者重叠,分两步处理(见阶段 2):先治理(接入预算池与让路门 + init 出主线程),后评估合并移除。
5. 分阶段实施
5.1 阶段 1:统计流水线 → 全书章节索引生产者(核心)
改造 RDEPUBTextStatisticsCoordinator / RDEPUBTextStatisticsWorker(建议更名为 RDEPUBBookIndexingCoordinator / Worker,体现双职责):
- 读 summary 缓存优先:每章先查
RDEPUBChapterSummaryDiskCache(key 一致)。命中时字符数 = 末段 pageRange 的location + length,零渲染。不新增 summary 字段、不动 schemaVersion。边界兜底:该等式依赖"分页覆盖全文";读取时校验 pageRanges 非空、末段 end 单调且pageCount == pageRanges.count(sanitizedPageRanges的存在说明缓存出现过脏 range),校验不过视为 miss 重渲染,并加 debug 断言。 - miss 时渲染一次、产出两份:渲染(传入真实
pageSize,当前统计传 nil)+ 分页,写RDEPUBChapterSummary(复用RDEPUBChapterLoader.makeSummary的构造逻辑,抽成共享工具),同时上报字符数。CFI map 沿用"延迟构建"策略,不在生产者内做。 - 渲染顺序按阅读位置:废弃 spine 顺序,使用
backgroundPriorityManager.makeMetadataPriorityOrder(现成:当前章优先、warm anchor 次之、cold 游标兜底)。 - 章间让路 + 飞行中去重:每章开始前先过交互加载门(5.6),再向后台渲染登记表(5.8)登记本章;登记失败(他人在渲染)则跳过取下一章。取消(cancellationController)可随时打断等待。
- PageMap 增量喂给主线程:每产出 N 章(复用
pageMapRefreshInterval = 32或取更小值)将累计 summary 合并为 partial map,走现有refreshBookPageMapInPlace路径。 - 接收交互流水线的产出:
RDEPUBChapterLoader渲染完成回调中把(spineIndex, characterCount)推给统计聚合器——用户翻页产生的渲染同样贡献统计,生产者跳过这些章;用户读得越活跃,统计越快。 - 启动时机后移:从
paginatePublication开头移到首屏applyBookPageMap完成之后(与 prefetch、metadata worker 同一批,但受让路门约束)。 - 移除
RDEPUBTextStatisticsDiskCache:summary 缓存成为唯一数据源。发布前保留一个版本的只读兼容(读旧统计缓存作为 fallback)可选,建议直接移除以降低复杂度。
聚合器在阶段 1 保持"凑齐才发布"语义(与现状等价,footer 显示"计算中");增量/近似发布在阶段 4 引入。
5.2 阶段 2:MetadataParseWorker 治理(先治理,后合并)
2a. 立即治理(低风险)
- 并发接入全局预算池(5.7),不再自行按全核计算;
RDEPUBReaderConfiguration.metadataParsingConcurrency语义改为"预算上限的用户侧覆盖",默认跟随预算池。 - init 中的
contentHashBySpineIndex全量读文件 + sha256 移出主线程:移入start()的后台块,或 init 改为在调用方后台线程构造。 - 章间让路 + 飞行中去重:每个 chapter operation 执行块开头接入交互加载门(5.6)与后台渲染登记表(5.8),替代现有"仅启动时等 0.8 秒"的一次性冷却。共存期(阶段 2b 之前)生产者与 metadata worker 的目标章节高度重叠(都从 anchor 附近开始),登记表是保证"每章只渲染一次"的必要条件,不可省略。
2b. 后续合并(独立评审)
阶段 1 落地后,MetadataParseWorker 的价值收缩为"打开时快速凑齐 anchor 窗口的 PageMap"。评估将其移除,窗口覆盖由生产者的优先级排序天然承担(anchor 窗口排最前)。涉及 backgroundCoverageStore、restoreBookPageMapIfPossible、retry 逻辑的迁移,单独出 RFC,不与阶段 1 捆绑。
5.3 阶段 3:首屏路径优化(独立可做)
loadInitialInteractiveRuntimeChapters中相邻章的同步补载改为异步:首屏只等 anchor 章;anchor 章不足 2 页时先出首屏,相邻章以.preview优先级异步补,完成后走现有queueForwardAppendedPageMap扩展。paginateTextPublication的编排队列由.utility提升为.userInitiated(首屏关键路径)。
有意的 trade-off(需评审确认):现状同步补载的目的正是保证首屏出来时至少 2 页可翻。改异步后存在一个短暂窗口——单页首章 + 用户立刻翻页 → 命中 loading 页。缓解:补载走 .preview 优先级(userInitiated 队列,通常数百毫秒内就绪)、loading 页 footer 已有"计算中"占位处理;换来的是所有多页首章场景的首屏时间稳定改善。若评审认为该窗口不可接受,退化方案为"仅当 anchor 章 ≥ 2 页时走纯异步"。
5.4 阶段 4:两阶段近似百分比(正式纳入)
解决 2.3 的结构性瓶颈:footer 首个百分比时刻从"全书渲染扫完"提前到打开书后亚秒级。
- 快速近似扫描:打开书后在后台对全书每章做"HTML 去标签纯文本长度"统计(轻量 XML/正则 strip,不走渲染管线),全书量级为几十~几百毫秒,产出近似分母与近似
absoluteCharacterStart。 - 混合分母收敛:聚合器为每章维护
精确值 | 近似值标记;分母 = 精确值之和 + 未精确章节的近似值之和。精确值(来自 summary 命中、交互渲染上报、生产者渲染)逐章替换近似值,全部精确后与阶段 1 语义完全一致。 - 坐标系约束(见 2.1):近似值只进入分母与前序章节的起点估计;分子
chapterOffset永远来自当前章真实分页(当前章正在显示,必然已渲染,其精确字符数同时可用)。误差来源仅剩前序章节近似与总量近似,典型偏差 <1% 且随阅读单调收敛(误差分级标准见第 8-3 节)。 - UI 语义:近似分母就绪即显示百分比,不再显示"计算中"(近似/精确不做视觉区分);"计算中"仅保留给近似扫描也未完成的极短窗口与 PageMap 未覆盖的 loading 页。
- 实现要点:
RDEPUBBookTextStatistics由一次性不可变构建改为支持逐章替换的聚合模型(内部仍可每次替换后重建不可变快照发布,避免并发可变状态);主线程刷新节流见 5.5-4。 - 持久化:近似值不落盘(计算便宜,冷启动重算);summary/精确值沿用磁盘缓存。
5.5 阶段 5:自适应并发渲染(默认启用)
前提:阶段 1(去重)已落地——否则多线程只是把重复劳动乘以 N。
- 并发预算:来自全局预算池(5.7),上限
min(3, max(1, activeProcessorCount - 2))。 - 自适应降速(在"取下一章"调度点做准入,不动态改 OperationQueue 配置):
- 活跃翻页期(
secondsSinceLastUserNavigation()小于阈值,建议 2 秒,与 LocationCoordinator 的 idle 判定一致)→ 收缩为 1; ProcessInfo.thermalState >= .serious或低电量模式 → 收缩为 1;- 收到内存警告 → 收缩为 1,并配合现有 evict;
- 静止阅读/空闲 → 放开至预算上限。
- 活跃翻页期(
- QoS 保持
.utility(P+E 核架构下低 QoS 线程主要落能效核,与主线程/chapterLoadQueue 的性能核天然隔离;不选.background以免 I/O 被系统节流拖长扫描)。 - UI 刷新节流:多线程下章节乱序完成(聚合已是加锁模型,天然支持);主线程刷新(footer/
reloadPageCountOnly)合并为"每完成 8~16 章或每 0.5 秒"一次,禁止每章一刷。 - 收益预期:DTCoreText 存在 libxml/字体缓存等进程级锁,3 线程实际加速 2.2~2.5 倍,继续加线程边际收益趋零——这是预算上限取 3 的依据之一。
5.6 共享组件:交互加载门
新增 RDEPUBInteractiveLoadGate(NSCondition 封装),所有后台渲染流水线共用:
- raise:
RDEPUBChapterLoader在 navigation / preview 优先级的 pending load 注册时计数 +1(prefetch 不计,prefetch 本身就是后台工作)。 - lower:对应 load resolve(成功/失败)时计数 -1,归零时 broadcast 唤醒全部等待者。
- wait:后台流水线章间调用
waitUntilClear(cancellation:);cancellation 触发时立即返回。 - 只在章间边界等待,不打断进行中的单章渲染(单章通常数百毫秒,可接受;避免渲染中途状态管理复杂度)。多线程下语义不变:翻页瞬间后台从 N 并发降到 0,翻完集体恢复。
该门是此前已移除的"0.8 秒翻页时间窗轮询"的正确形态:无时间参数、无轮询、翻页结束即恢复。
5.7 共享组件:全局后台渲染预算池
动机:现状各流水线各自决定并发(metadata worker 按全核、统计 1、chapterLoadQueue 1),局部合理、总和超载。并发决策必须收敛到一处。
- 形式:计数信号量,
后台渲染总并发 = min(3, max(1, activeProcessorCount - 2))。所有后台流水线(生产者、metadata worker)每次章节渲染前取票、完成归还;交互流水线(navigation/preview)不受预算约束,永远直接执行。 - 为什么留 2 核而不是 1 核:翻页关键路径上同时有两条热线程——主线程(手势/转场/当前页绘制)与
chapterLoadQueue(未缓存章节的那次渲染,主线程可能正同步等它,其完成时间即翻页延迟)。只留 1 核时两者在最坏时刻互挤,卡顿复现。 - "留核"的真实含义:iOS 无核绑定 API,可控的只有"后台线程数 × QoS";预算池控制线程数,QoS 控制调度倾向,内存带宽与进程级锁的共享是并发上限取 3 的另一依据。
- 与交互加载门正交:门管"交互期间全体暂停",池管"平时最多同时渲染几章"。
5.8 共享组件:后台渲染登记表(飞行中去重)
summary 缓存只能去重"已完成"的渲染;同一章同时在两条后台流水线中飞行时(谁都还没写盘),缓存查询拦不住双渲染。共存期(阶段 2b 前)生产者与 metadata worker 的目标章节高度重叠,该场景必然发生,不是边角情况。
- 形式:全局
Set<spineIndex>+ 锁(参照现有beginPendingChapterLoad模式),按 renderSignature 命名空间隔离。 - 规则:后台流水线(生产者、metadata worker)取章渲染前
begin(spineIndex:)登记,失败(他人在渲染)即跳过取下一章;完成/失败/取消时注销。 - 交互流水线不受约束:navigation/preview 加载不查表、不等表——让 userInitiated 的翻页渲染去等 utility 的后台渲染是优先级反转。交互与后台撞车导致的偶发双渲染予以接受,记
duplicateRender日志计数(纳入第 8-1 节验证)。 - 生产者取章时同时检查
store.chapterData/hasPendingChapterLoad,已被交互体系覆盖的章节直接等其上报,不自行渲染。
6. 性能预期(按场景)
| 场景 | 现状 | 方案后 | 依据 |
|---|---|---|---|
| 冷启动大书,footer 首个百分比 | 全书渲染扫完(几十秒~分钟级) | 亚秒级(近似值),精确值后台收敛 | 阶段 4 |
| 冷启动大书,精确统计完成时间 | 单线程 × 与 metadata worker 全程抢核 | 持平或更快:去重(窗口章节免渲染)+ 无重复流水线竞争 + 空闲期 2~3 并发;单章成本 +约 30%(增加分页)被上述收益覆盖 | 阶段 1 + 5 |
| 二次打开 / 读过一部分 | 统计缓存独立,预加载渲染过的章节仍是 miss,需重渲染 | 快一个量级:读过/预加载过的章节直接读 summary(毫秒级/章) | 阶段 1 |
| 统计期间翻页 | 主线程同步渲染被后台放大 2~3 倍 | 后台交互期间为 0 并发(让路门),平时不超预算;翻页目标章大概率 summary 命中(免分页) | 5.6 + 5.7 |
| 让路对统计的代价 | — | 总完成时间仅增加"用户实际交互占用的时长",无空转等待 | 5.6 |
7. 风险与对策
| 风险 | 对策 |
|---|---|
| 布局变化(转屏、字号)导致 renderSignature 失效,生产者产出作废 | 沿用现有 token + cancellationController 机制:设置变更时 cancel 并以新 snapshot 重启;summary 缓存按 signature 命名空间隔离,旧产出不污染。近似分母与 renderSignature 无关,无需重算 |
| 生产者渲染中用户跳转,让路门只拦章间 | 单章渲染粒度可接受;跳转走 navigation 优先级,chapterLoadQueue(userInitiated)调度上压过生产者(utility),且交互不受预算池约束 |
| 让路门计数泄漏导致后台永久挂起 | raise/lower 严格配对在 registerPendingLoad / resolvePendingLoad(已有单点出入口);增加 debug 断言 + 兜底超时日志(不自动放行,只报警) |
| 预算池取票/归还不配对导致后台饿死 | 取票/归还封装为 scoped helper(defer 归还);debug 断言校验余额 |
| 生产者写 summary 覆盖 loader 已写入的更完整 summary(loader 侧经延迟构建后含 cfiMap,生产者侧不含,后写会把 cfiMap 抹掉导致重复构建) | 生产者写盘前 re-check:该 key 已存在 summary 即跳过写入(只取字符数);写路径本身沿用 RDEPUBChapterSummaryDiskCache 串行队列 + 原子替换。配合 5.8 登记表,正常情况下不会走到同章双写 |
| 近似百分比与精确值偏差被用户感知 | 近似仅用于分母(2.1 坐标系约束),偏差单调收敛;分级标准与抽样验证见第 8-3 节 |
| PageMap 增量刷新引起当前页码跳动 | 复用现有 refreshBookPageMapInPlace / reconciliation 路径,不新增刷新语义;刷新间隔沿用 32 章 |
| 多线程渲染内存峰值(每 worker 持有整章排版串 + DTCoreText 中间 DOM) | 每章 autoreleasepool;不持有 RuntimeChapter,写盘后即释放;内存警告时并发收缩为 1;3 并发峰值约为单线程 3 倍,预算上限已封顶 |
| 多线程撞 DTCoreText/CoreText 进程级锁,加速比不达预期 | 预期管理:2.2~2.5 倍为目标;若实测低于 1.5 倍,回退默认并发 1(自适应策略保留,阈值可配) |
8. 验收与度量
上线前后对比(同一测试书目,冷缓存 / 热缓存各一组):
- 每章渲染次数 ≤ 1:扩展
[TextStatistics]日志为来源分布summaryCacheHit / rendered / fromInteractiveLoad / approximated,全书求和验证。 - footer 首个百分比出现时间:目标亚秒级(近似值);同时记录精确值收敛完成时间。
- 近似误差(统一分级标准):样本书目(不同语言、图文比例、章节数量级)上对比近似百分比与最终精确百分比——典型值预期 <1%;验收线为偏差分布 P95 < 1.5%;任一单本书 > 3% 触发个案分析(定位该书目类型的系统性偏差来源,如实体密集、大量脚注)。
- 翻页流畅度:
loadChapterSynchronouslyForMigration耗时日志 P50/P95;统计计算期间连续翻页 20 次的主线程 hang 时长(Instruments Hangs)。 - 统计完成时间:
lastTextStatisticsWallClockMs,目标冷缓存下降 ≥ 40%(消除重复渲染 + CPU 竞争;空闲期自适应并发另计)。 - 多线程加速比:空闲场景 1 / 2 / 3 并发的完成时间对比,验证 ≥ 2 倍再默认放开;同时记录热状态触发降档的频率。
- 首屏时间(阶段 3):打开到首帧渲染完成,目标不劣化、多页首章场景改善。
- PageMap 收敛速度:总页数从"计算中"到稳定值的耗时,目标不劣于现状(生产者兼职覆盖全书,应优于现状)。
9. 实施顺序建议
- 交互加载门(5.6)+ 预算池(5.7)+ 登记表(5.8)+ 阶段 2a(治理 metadata worker)——小改动,立即缓解翻页卡顿。
- 阶段 1(生产者化)——核心重构,消除重复渲染。
- 阶段 4(两阶段近似百分比)——直接兑现"首个百分比亚秒级"的产品收益;依赖阶段 1 的聚合器改造,不依赖阶段 5。
- 阶段 3(首屏)——独立优化,可与 3 并行。
- 阶段 5(自适应并发,默认启用)——依赖阶段 1 去重与 5.7 预算池;按第 8-6 节实测加速比决定是否默认放开到 3。
- 阶段 2b(合并 metadata worker)——后续 RFC。