看到“Flutter WebView 滚动从 70 FPS 提升到 120 FPS”,很容易误以为只是把设备刷新率切到 120 Hz,或者修改了 FPS 统计上限。实际并非如此:这项优化对应已经合并的 flutter/flutter#195861,调整的是 Android TLHC 混合合成的任务时序,减少 WebView 离屏绘制与主窗口合成之间的竞争。资料中的测试场景因此由约 70 FPS 提升到 120 FPS。
核心原因也不是“WebView 绘制突然变轻了”,而是旧链路会让 WebView 的重型 DrawFrame 提前占用进程内共享的 RenderThread,主窗口同步只能等待。合并后的改动改变了排队关系;Pixel 9 Trace 中,主窗口同步耗时减少约 3.7 ms,而 120 Hz 屏幕每帧只有 8.33 ms,这段时间原本会直接挤压 Flutter 页面可用的帧预算。
70 FPS 升到 120 FPS,究竟改变了什么?
70 到 120 FPS 是资料所记录的 TLHC WebView 快速滚动场景中的观测结果,不应扩大解释成“所有 Flutter WebView 升级后都会增加 50 FPS”。它既不表示 WebView 自身突然获得了两倍算力,也不意味着所有设备、所有页面都能稳定跑满 120 FPS。
热点标题把模式写作“THLC”,而资料正文使用的是“TLHC”。本文统一写作 TLHC(Texture Layer Hybrid Composition,纹理层混合合成),不把这一拼写差异视为两种模式;最终仍应以 flutter/flutter#195861、所用引擎版本和运行时 Trace 为准。
资料支持以下三项结论及其边界:
| 结论 | 适用范围 | 不能据此推出 |
|---|---|---|
| TLHC WebView 滚动由约 70 FPS 提升至 120 FPS | 资料中的 Android 合成时序和测试场景 | 所有 Flutter WebView 都有相同增益 |
| 主窗口同步等待减少约 3.7 ms | Pixel 9 Trace | 单独增加 3.7 ms 就必然对应固定帧率增益 |
| 120 Hz 是值得追求的目标 | 设备、系统、合成路线和页面负载均满足条件 | 每个 WebView GPU 任务都低于 8.33 ms,或输入延迟完全消失 |
从工程角度看,70 到 120 FPS 是完整渲染链路改善后的结果,不能简单用帧率增量反推某个函数节省了多少时间。3.7 ms 是主窗口同步阶段的观测值,实际帧率还取决于 Flutter UI 线程、GPU 光栅化、WebView 内容负载、纹理帧交付时机以及设备调度。
为什么 WebView 一滚动就可能拖住整个页面?
旧版 TLHC 的关键绘制链路可以还原为五步。
- Android 主线程调用
PlatformViewWrapper.draw()。 - 通过
lockHardwareCanvas()取得离屏 Surface 的 Canvas。 - 调用
super.draw(),将 WebView 及其子 View 的内容绘制进去。 - 立即调用
unlockCanvasAndPost(),提交这次离屏绘制。 - 通知 Flutter 有新的纹理帧可以使用。
第 4 步中,unlockCanvasAndPost() 看起来只是提交 Canvas,但提交之后,Android RenderThread 还要消费这些绘制命令并执行相关 GPU 工作。也就是说,方法返回不代表 WebView 的离屏帧已经真正完成。
Android View 绘制大体可以分为两个阶段:主线程上的 View#draw 负责遍历 View、记录绘制命令,随后由 RenderThread 执行 DrawFrame。TLHC 又多了一层离屏 Surface,如果它在主窗口遍历尚未结束时就被提交,WebView 的重型绘制任务就会先进入 RenderThread 队列。
Android 并不会为每个 HardwareRenderer 都准备一条独立 RenderThread。WebView 离屏绘制和应用主窗口最终会争用进程内同一条 RenderThread,资源冲突如下:
旧时序:WebView 离屏 Surface 提前提交
Android 主线程 记录主窗口 → 提交 WebView 离屏 Surface → 主窗口同步并等待
RenderThread WebView DrawFrame 占用资源 → 主窗口绘制工作
当主窗口随后进入同步阶段时,RenderThread 可能仍在处理 WebView 的重任务。主窗口同步路径需要等待 RenderThread 响应,Android 主线程因此被连带阻塞,最终影响同一帧中的 Flutter 内容与 WebView 合成。
因此,WebView 离屏帧的提交顺序会改变整个窗口的合成节奏,不只影响 WebView 自身的滚动。
新合成时序怎样把主窗口同步耗时压下来?
这次优化针对的是任务排队关系,而不是通过降低 WebView 分辨率、关闭动画或限制帧率来换取流畅度。合并后的关键效果,是让 WebView 的重型 DrawFrame 不再抢先占住共享 RenderThread,从而减少主窗口同步阶段的额外等待。
旧、新时序的因果关系可概括为:
旧链路:
WebView DrawFrame 提前入队
↓
RenderThread 被占用
↓
主窗口同步等待,Android 主线程被拖住
优化后的链路:
WebView DrawFrame 不再提前抢占主窗口所需的工作
↓
主窗口同步少等待约 3.7 ms
↓
Flutter 页面获得更多帧预算
这只是逻辑时序,不是函数级实现说明。参考资料没有披露 flutter/flutter#195861 的完整 diff,因此不应根据标题推测具体新增了哪些 API、回调或调度方法。真正的实现细节应以 PR 为准,能够确认的结论只有两个:WebView 离屏绘制不再以相同方式提前阻塞主窗口同步,Pixel 9 Trace 中的同步等待减少了约 3.7 ms。
3.7 ms 在 120 Hz 环境中并不是可以忽略的时间。每帧总预算只有 8.33 ms,主窗口同步多消耗几毫秒,就可能挤掉 Flutter UI、图层合成以及纹理上屏的时间。它解释了为什么提帧收益会很明显,但不能据此认为 70 到 120 FPS 是由单个数值线性换算出来的。
同样,120 FPS 只描述最终输出帧率,不表示 WebView 的每一个 GPU 任务都控制在 8.33 ms 内,也不代表输入延迟、页面加载抖动和所有长帧都已经消失。帧率、平均耗时和交互响应需要分别观察。
HC、TLHC 和独立 Surface 合成应该怎样选?
Flutter 与 Android 原生 View 的合成路线不同,性能收益和兼容成本也不同。资料给出的三条路线可以按合成主体、同步位置和适用条件来比较,而不应只根据模式名称判断。
| 合成路线 | 内容如何参与合成 | 主要优势 | 主要代价 | 更适合的判断条件 |
|---|---|---|---|---|
| Android View 先绘制成纹理,再交给 Flutter 合成 | 原生内容进入纹理,由 Flutter 参与最终合成 | Flutter 侧性能较好,合成链路相对统一 | WebView 快速滚动曾是已知性能问题 | 页面内容较轻,且能够确认纹理合成路径没有成为瓶颈 |
| 传统 HC:保留 Android 原生 View 层级 | 原生 View 留在 Android View 层级,由 Android 合成 | 原生兼容性较好 | 存在 Flutter 线程合并开销 | 原生交互和兼容性优先,同时 Trace 证明 HC 路径可以接受 |
| Flutter 与原生内容分别使用 Surface,再由系统合成 | Flutter 和原生内容各自使用 Surface,由系统完成合成 | 可减少传统 HC 的同步开销 | 存在明确的设备和系统要求 | 目标设备支持相应能力,并且所用 Engine 包含相关优化 |
这三条路线并不存在脱离场景的绝对优劣。HC 的原生兼容性更好,但线程合并开销并没有因为 TLHC 的改进而自动消失;独立 Surface 路线减少了部分同步成本,其可用性取决于设备和系统能力;先绘制为纹理、再交由 Flutter 合成的路线在 Flutter 侧表现较好,但 WebView 快速滚动正是此次优化针对的历史问题。
因此,模式选择不能只问“哪个理论性能更高”,而要先确认:
- 目标设备实际上进入了哪条合成路线;
- 当前 Flutter Engine 是否包含
flutter/flutter#195861; - WebView 内容和原生交互是否给 HC 增加了不可接受的兼容成本;
- Trace 中主窗口同步、RenderThread 和纹理帧交付是否仍存在明显冲突。
这次提帧需要满足哪些设备和系统条件?
要复现这项改动,Flutter 引擎版本是首要条件。必须确认项目所用 Engine 已经包含 flutter/flutter#195861;参考资料没有给出对应的稳定版版本号,因此不能直接写成“某个 Flutter 版本及以上”。PR 已合并,也不等于所有历史稳定版或项目当前锁定的引擎都会自动包含它。
设备与系统能力决定独立 Surface 路线是否可用。资料明确提到该路线存在设备和系统要求,但没有提供完整的 Android 版本、GPU 或机型清单,因此更可靠的做法是查询目标机型支持情况,并在真机 Trace 中确认实际合成路径,而不是自行补一份并不存在的最低版本矩阵。
现有证据仅来自 Android:它依赖 Android 主线程、RenderThread、离屏 Surface 和 Pixel 9 Trace。这套结论不能直接外推到 iOS、桌面端或 Web 平台,因为这些平台的原生内容合成路径不同,需要单独分析。
WebView 内容负载仍然是变量。普通 HTML 与包含较重 GPU 绘制任务、WebGL 3D 或液体动画的页面,对 RenderThread 和 GPU 的压力并不相同;即使主窗口同步等待已经减少,内容本身过重仍可能留下长帧。
兼容性还受以下条件约束:
| 维度 | 有利条件 | 风险或未知项 | 验证方式 |
|---|---|---|---|
| Flutter/Engine 版本 | 已包含 flutter/flutter#195861 |
仍使用旧引擎或锁定版本未包含改动 | 查看 flutter --version 中的 Engine revision |
| 合成模式 | 实际走 TLHC/Surface 路线 | 实际回退到传统 HC,收益不能直接套用 | 在系统 Trace 中确认合成路径 |
| 设备与系统 | 支持相应 Surface 和系统合成能力 | 不支持、被厂商策略限制或行为存在差异 | 查目标机型支持情况,并在多台真机上抓 Trace |
| WebView 内容 | 普通页面或 GPU 负载可控 | WebGL、复杂动画、频繁纹理更新 | 使用固定脚本做前后对照压力测试 |
| Flutter UI 线程 | BUILD/LAYOUT/PAINT 耗时稳定 | Dart 逻辑过重,形成独立瓶颈 | 使用 DevTools Performance 检查 |
| 平台 | Android | Android 之外的合成机制未被这组资料覆盖 | 换平台后重新建立基线 |
这张矩阵的重点不是找到一个“最低配置”,而是避免把版本、模式和设备条件混在一起。只要其中一项没有确认,120 FPS 都只能算目标,不能算结果。
如何验证项目是否接近 120 FPS?
验证时首先需要固定基线,而不是直接比较两个不同项目里的平均 FPS。至少记录设备型号、系统版本、当前刷新率、Flutter 与 Engine revision、WebView 版本、页面内容和测试手势。版本信息可以先用下面的命令采集:
flutter --version
flutter devices
adb shell devices
adb shell dumpsys display
dumpsys display 的字段会随 Android 版本和厂商变化,需要在输出中确认设备支持的刷新率和当前显示模式,不要依赖某个固定字段名。
随后应使用目标真机,在系统允许的最高刷新率模式下测试。Profile 构建比 Debug 构建更接近实际渲染开销:
flutter run --profile
系统 Trace 用于确认这次优化是否生效。录制时只做固定范围内的重复滚动,并沿用 Pixel 9 Trace 的定位思路,重点检查以下事件:
- Android 主线程上的主窗口同步;
- RenderThread 上的 WebView DrawFrame;
- WebView 离屏 Surface 的提交与处理;
- 新的纹理帧到达 Flutter 的时间;
- 主窗口同步结束与整帧提交之间的关系。
如果优化前能看到“WebView 重型 DrawFrame 占用 RenderThread,主窗口随后进入同步并等待”,而优化后主窗口同步不再被这段工作额外阻塞,才能说明改动确实作用于目标路径。仅看到顶层 FPS 数字变化,无法排除测试环境、刷新率或页面内容变化带来的干扰。
DevTools 用于判断卡顿是否来自 Dart UI 线程。Flutter Performance 可以查看 CPU、GPU、build、layout、paint 等阶段耗时;资料中的 Flutter 调优案例也将 BUILD、LAYOUT 和 PAINT 作为 Dart 层排查重点。 如果系统 Trace 显示合成阻塞已经缓解,但 DevTools 中 UI 线程仍频繁出现长任务,就应转而检查 Flutter 状态更新、布局重建和自定义绘制,而不是继续调整 WebView 合成参数。
压力测试需要覆盖不同内容。除普通长页面外,可以加入 WebGL 3D、液体动画和快速连续滚动场景,观察优化前后是否仍存在 RenderThread 长任务;这些内容可用于检验 GPU 负载较重时的边界。
最后不要只看平均 FPS。至少同时记录单帧时间、卡顿发生位置、系统 Trace 和 DevTools 数据。60 Hz 每帧预算约为 16.6 ms,120 Hz 约为 8.3 ms,平均值可能掩盖少量但明显的交互长帧。 资料 中的 60 Hz、120 Hz 帧预算可用于理解时间尺度,但其中的 HiAppEvent、HiLog 和滑动丢帧上报流程属于 HarmonyOS DFX,不能直接当作 Android RenderThread 的诊断工具。
升级后仍然卡顿时,如何定位瓶颈?
先检查 Engine 是否包含合并改动。如果仍在使用旧引擎,先解决版本问题,否则后续的模式分析缺少前提。
随后确认实际合成模式。如果设备因为系统能力或厂商策略没有走 TLHC/Surface 路线,而是使用传统 HC,就不能把仍存在的卡顿简单归因于“#195861 优化无效”。需要先从 Trace 判断它究竟处于哪条路线。
最后将系统 Trace 与 DevTools 放在同一时间轴上判断:
| 观察结果 | 更可能的方向 | 后续动作 |
|---|---|---|
| Engine 不包含改动 | 版本问题 | 升级或替换 Engine 后重新测试 |
| 实际走 HC,Flutter 线程合并开销明显 | 合成路线问题 | 对比 HC 与可用 TLHC 路线的 Trace |
| RenderThread 持续被 WebView DrawFrame 或重型 GPU 工作占用 | WebView 内容或离屏绘制问题 | 缩小页面、减少复杂 GPU 内容和纹理更新压力 |
| DevTools 中 BUILD/LAYOUT/PAINT 占用明显 | Dart UI 线程问题 | 检查高频 setState、复杂布局和昂贵绘制 |
| 卡顿集中在首次加载或动态更新 | 加载与脚本处理问题 | 拆分静态内容和动态脚本 |
| 平均帧率尚可但交互仍不顺 | 输入、长帧分布或局部卡顿 | 补充单帧时间与卡顿位置数据 |
“Trace 忙而 DevTools 正常”通常意味着主要瓶颈不在 Flutter Dart 层。此时应继续从 WebView 页面负载、GPU 绘制和纹理帧交付成本入手,而不是把问题简单归因于 Widget 树。
反过来,如果 DevTools 显示 BUILD、LAYOUT 或 PAINT 忙碌,问题也可能完全独立于 WebView 合成。Flutter 页面滑动卡顿可以来自构建、布局或绘制耗时,不能把所有滚动长帧都归到 RenderThread。
加载和动态更新造成的卡顿需要单独排查。将静态 HTML 与脚本分开,并通过 evaluateJavascript 注入动态脚本,可以减少运行时 Base64 转换和重复编码的成本;但这属于加载优化,不能替代 RenderThread 时序修复。 同样,tree shaking 和延迟加载可以作为一般 Flutter 性能优化手段,但现有资料并未证明它们能够解决此次 WebView 共享 RenderThread 争用。
这次优化解决了什么,又留下了哪些边界?
这次合并解决的是一个因果链明确的问题:在特定 Android TLHC 路径中,WebView 离屏绘制提前抢占共享 RenderThread,继而拖慢主窗口同步;调整时序后,Pixel 9 Trace 中的主窗口同步等待减少了约 3.7 ms。
它没有消除所有 WebView 性能问题。Flutter/Engine 版本、实际合成路线、设备与系统能力、WebView GPU 负载以及 Dart UI 线程状态,都会决定最终能否稳定接近 120 FPS。
因此,验收时不应只问“有没有达到 120 FPS”,而应检查“版本—模式—设备—内容—Trace 数据”这五项是否一致。满足这组条件后,才能把 120 FPS 作为可验证的目标;条件未确认时,它只是一个需要实测的预期值,而不是 Flutter WebView 的普遍承诺。
[email protected]
。
我们承诺在核实信息后,将第一时间删除相关内容或断开链接。























暂无评论内容