☰
知乎++Markdown渲染性能优化实战:滚动延迟341ms降到27ms,LaTeX公式与延迟布局全解
2026/9/26 3:52:06 网站建设 项目流程

知乎++Markdown渲染性能优化实战:滚动延迟341ms降到27ms,LaTeX公式与延迟布局全解

【免费下载链接】zhihu-plus-plusZhihu++ | 知乎++: Ad-free, low cost, AI powered zhihu android 3rd-party client. 去广告、占用低、AI大模型的新时代知乎安卓端体验项目地址: https://gitcode.com/gh_mirrors/zh/zhihu-plus-plus

知乎++(Zhihu++)是一个去广告、低占用、支持 AI 大模型的知乎第三方客户端。其中最能体现工程实力的,是它的 Markdown 渲染性能优化:在一篇包含 80 条最长公式的知乎真实压力文章中,首次向前滚动的中位延迟从 341.7ms 降到 27.7ms(-91.9%),普通 16 类场景的中位数全部低于 30ms。本文将带你完整看懂这套「LaTeX 公式异步准备 + 视口延迟布局」方案的思路与成果。

📄 核心资料:docs/markdown-renderer-performance-report.md 与 docs/markdown-issue-495-performance-report.md 记录了全部测试口径与数据。

优化前的问题:滚动为什么会卡?

卡顿的直接感受是:打开一篇公式密集的长回答,往下滚一下,页面「顿」一下才跟手。

根因排查后发现,问题不在解析,而在布局:

  1. LaTeX 组件只在后台解析,主线程仍同步测量公式。长文滚动时,每个新进入视口的公式都会触发一次主线程布局,滚出视口再进入还可能重复计算。
  2. 大量公式同时启动后台任务,占满 CPU,与输入、Compose 的 layout 和 draw 争抢调度。
  3. 全量 eager 布局一篇真实知乎回答(406 个 AST 节点、148 个公式节点),首帧耗时高达6,310ms。

其中独立公式块约占首帧时间的 49.6%,是最大的单项成本,但只优化公式仍会剩下约 3.18 秒——所以最终方案是「所有屏外块统一延迟布局」,公式自然被覆盖。

核心方案一:视口延迟布局(Deferred Layout)

这是本次优化的灵魂。核心实现位于 MarkdownLayouts.kt:

  • 完整 AST 不裁剪:HTML 一次性转为完整文档结构,首帧即可用于脚注跳转、图片预览和标题导航,数据完整性不打折(AST 构建实测仅约 2.4ms~9.3ms,根本不是瓶颈)。
  • 只有视口内的块才真实布局:基于SubcomposeLayout,屏外块只生成一个「估高占位」,进入视口上下各1.5 个视口(普通正文为 0.5 个)的预取范围后,才调用真实渲染。
  • 滚出预取范围即回收:块离开后恢复占位;已经真实布局过的块会记住实测高度,不再退回粗略估算,滚动位置和进度条不会跳动。
  • 递归覆盖容器子块:一个 300 项的列表或 200 行的表格,子项也逐条延迟物化,不会出现「单个超长块被整体布局」。

一个巧妙细节:高度预估不靠猜。段落按中文字符宽度和换行估算行数,公式块按分式、根号、求和等高结构加成,代码块按行数估算。实测首帧预测的全文滚动范围 38,301px,与实际布局后的 37,955px 仅差0.91%,进度条从第一帧就准。

核心方案二:LaTeX 公式的后台准备与缓存

针对公式这个最大单项成本,latex-parser 与 latex-renderer 做了三层改造:

  1. 单一受限后台通道:解析和完整布局都放到一个受限制的后台 lane 完成,在解析与布局边界检查取消并主动让出,避免后台任务挤占 CPU。
  2. 页面级 LRU 缓存:每个 Markdown 文档持有可缓存活跃公式的 LRU,公式离屏回收后,滚回来时直接复用不可变的准备结果,不重复计算。
  3. 行内公式保守占位:先按保守尺寸占位,准备完成后只刷新该公式的真实尺寸,彻底删除了主线程的同步预测量。

配合这两套方案,压力文章双向各 40 个滚动样本全部低于 100ms,其中 79/80 低于 50ms。

优化前后数据一览 📊

以下数据来自相同的 80 条最长公式文章与相同的 700px 分步滚动边界(摘自 docs/markdown-renderer-performance-report.md):

指标优化前优化后变化
首次向前滚动中位数341.7 ms27.7 ms-91.9%
首次向前滚动 P90523.1 ms41.2 ms-92.1%
返回滚动中位数81.6 ms22.6 ms-72.3%
300 项列表首屏中位数100.5 ms22.8 ms-77.3%
200 行表格首屏中位数99.3 ms25.6 ms-74.2%

另外两个关键数字:

  • issue #495 的 36,460 字符真实知乎 HTML,转完整 AST 中位数仅9.27ms——证明「先分阶段计时、找准瓶颈」的重要性;
  • 3,703 条真实知乎公式的 parser-only 中位总耗时20.67ms。

被拒绝的方案:踩过的坑同样值得学

项目复盘文档(docs/markdown-issue-495-performance-report.md)诚实地列出了被否决的方向:

  • ❌拆 HTML、逐段生成 AST:首帧看似降到 1.27s,但脚注表、图片预览、跨块结构都不完整,破坏数据契约;
  • ❌只延迟公式块:独立公式块虽是最大成本,单独处理仍剩约 3.18s 首帧;
  • ❌只保留已访问块、不回收:长文浏览越深,Compose 树越接近全量布局,内存和延迟随深度恶化;
  • ❌保留可关闭的「eager 逃生开关」:开关留着就有误退回全量布局的风险,最终被删除,静态与可选择正文始终延迟布局。

如何验证:固定回归门 🧪

性能优化的最大敌人是「悄悄退化」。知乎++把回归门槛写进了测试(MarkdownPerformanceTest.kt):

  • 全部稳定 JVM 样本低于 100ms,至少 70% 低于 50ms,全部场景中位数低于 30ms;
  • 压力文章直接读取真实公式语料(shared/src/jvmTest/resources/zhihu-formula-corpus/formulas.json),去重后按长度取最长 80 条,不使用缩短公式或预构造布局绕开瓶颈;
  • 功能回归覆盖公式真实像素、行内公式尺寸更新、缓存跨组合回收、脚注跳转与返回、300 项递归列表尾部物化等场景。

复现命令见 docs/markdown-renderer-performance-report.md 的「复现」章节,日常回归优先跑 Compose JVM 测试,真机 AVD 只做量级校准。

总结:三个可复用的性能思路

  1. 先分阶段计时,再动手:解析、组合、测量、布局、绘制各自归因,别优化错误阶段;
  2. 懒而不失完整:数据层完整 AST 首帧可用,渲染层按视口物化,兼顾功能与性能;
  3. 用实测数据自校准:占位高度只是起点,实测高度会持续回填预测,并让回收后的滚动体验保持丝滑。

这套方案让知乎++在保留文本选择、脚注跳转、真实公式绘制的前提下,把公式长文的滚动体验从「明显卡顿」提升到了「满帧跟手」。想深入了解实现细节,推荐直接阅读:

  • 渲染布局核心:MarkdownLayouts.kt
  • 应用侧渲染接入:RenderMarkdown.kt
  • 公式解析性能测试:LatexParserPerformanceTest.kt
  • 真实测试样本:issue-495-answer.html

【免费下载链接】zhihu-plus-plusZhihu++ | 知乎++: Ad-free, low cost, AI powered zhihu android 3rd-party client. 去广告、占用低、AI大模型的新时代知乎安卓端体验项目地址: https://gitcode.com/gh_mirrors/zh/zhihu-plus-plus

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询