Flutter 的跨平台一致性,到了浏览器中会遇到什么变化?郭树煜在这场分享里把问题拆成两部分:产物怎样加载,以及界面最终怎样绘制。前者影响首次打开的成本,后者解释为什么看起来相似的组件,可能生成不同结构,甚至出现不同的文本效果。
这段分享来自深圳“前端的挑战与机遇”录播第 4 段,所属稿件发布于 2022 年 5 月 9 日。以下讨论针对分享时的 Flutter Web 实现,特别是当时的 HTML 渲染器,不是当前版本的配置指南。
相同框架,进入浏览器后仍有不同实现
郭树煜先回顾 Flutter 与 Web 技术的渊源,再与先有 Web 框架、后扩展客户端支持的跨端方案作比较。Flutter 先在移动端形成框架与渲染体系,再进入浏览器,因此不是把现成网页组件简单搬到手机的反向过程。
共用框架 API 带来了复用可能,但宿主能力并不一致,绘图等接口尤其需要适配。片中介绍当时的两条路径:HTML 渲染器更多借助浏览器元素及接口,CanvasKit 则使用 WebAssembly 与 Skia 相关能力,追求更接近移动端的绘制一致性。前者与 Web 能力耦合更深,后者需要承担额外资源体积与加载成本。
他特别提醒,Flutter Web 和移动端共用大量 framework API,不等于引擎实现完全一样。编译到 Web 时,一部分引擎代码会由 Web 对应实现承接,Canvas 等接口尤其如此。HTML 路径能利用浏览器已有能力,渲染效果与 Web 平台贴得更近;CanvasKit 路径更接近 Flutter 自己的绘制方式,却要下载额外 Wasm 等资源。演示时默认配置可能把两种路径的资源都带上,再依目标环境选择,因此优化之前先看实际构建内容。
先看产物,而不是笼统地说包大
他以自己的项目展示当时的默认构建:除了主 JavaScript 文件,还可能加载渲染器资源和图标字体。不同平台的默认选择以及是否同时包含多种渲染路径,都会改变产物构成。
现场项目的初始清单里,主 JavaScript 约 2.3 MB、CanvasKit 相关 Wasm 约 2.8 MB,Material 图标字体约 1.5 MB,还有较小的另一套图标字体。数字属于他的项目和当时版本,但这个拆法很有用:不能看到加载慢就笼统责怪“Flutter Web 包大”,先要分清是业务脚本、渲染器、字体还是传输配置占了预算。
因此,第一步是确认项目实际需要哪些资源。在分享的方案中,他移除未使用的图标依赖,明确选择一种渲染器,再逐项检查剩余体积。选择 HTML 路径是其当时对可控性与优化空间的判断,并不意味着所有应用都应作相同选择。
图标裁剪、延迟加载与传输压缩分别解决什么
图标字体通常包含比应用实际使用更多的内容。郭树煜介绍过构建时移除未用图标的方式,也记录了当时 Web 构建遇到裁剪问题、借助另一平台构建结果处理字体的临时做法。这是历史版本问题的应对,不能当作今天应复制的固定步骤。
他先删掉项目没有使用的图标库,并指定 HTML 渲染器,避免在正式产物里同时携带两套引擎。对仍会使用的 Material 图标,则尝试只保留代码实际引用的字形;演示里该字体经处理降到约 3.2 KB。分享时 Web 构建直接裁剪会报错,所以他用另一平台的构建结果绕过。这说明真正有效的优化有时还受特定工具版本限制,必须验证产物而非机械复制命令。
主 JavaScript 则通过 Dart 的延迟导入思路拆分:将不必立即使用的内容延后,在需要时加载对应代码。片中展示了 deferred as 与 loadLibrary() 的使用方向,以及编译后出现的分块文件。它把一部分下载从首屏移到后续使用时,并没有让那部分功能永远不占资源。
他把一部分页面或组件改成延迟导入,让编译器生成独立的 part.js,到用户需要该功能时才调用加载。演示中主脚本由约 2.2 MB 降到约 1.6 MB;这减少的是首次必须下载和执行的内容,不代表应用总代码凭空消失。接着他用 source map 看主要体积来自哪里,认为剩余框架与引擎部分继续手改的收益有限。
随后,他用 source map 分析产物,区分业务部分与框架、引擎相关部分。不是所有字节都值得通过修改框架来消除,还应考虑优化收益和维护成本。最后再通过 Gzip 减少传输体积。演示中从兆字节降到数百 KB 的数据,涉及不同处理阶段与传输口径,不是所有项目都能取得的固定比例。
对约 1.6 MB 的主脚本启用 Gzip 后,传输体积在他的示例里降至四百多 KB。这里要把“构建文件的体积”和“网络传输的体积”分清:Gzip 没有减少浏览器最终要解析的逻辑,也没有替代图标裁剪与代码拆分。郭树煜把指定渲染器、清理字体、延迟加载与传输压缩排在同一条链上,分别解决不同来源的首开成本。
HTML 渲染器不等于所有内容都用普通标签
后半场进入具体实现。Flutter 框架中的 Canvas 接口由平台对应实现承接;在当时 Web 路径中,郭树煜主要分析 SurfaceCanvas,并继续区分 DomCanvas 与 BitmapCanvas。
他先观察一个滚动列表:页面中出现以 flt- 开头的自定义元素,离开可见区域的项目与可见项目内部结构不同。更值得注意的是,文本不一定表现为普通的段落标签,也可能画在 Canvas 上。不能因为选择了 HTML 渲染器,就以为页面结构会与手写网页相同。
列表滚动时,可见项与离开可视区域的项不是一整套静态 DOM 永远原封不动地留在那里。引擎会控制或替换相应元素。郭树煜放慢演示观察结构,再问一个更具体的问题:同样是 Flutter 的 Text,什么时候在 Canvas 里成为像素,什么时候变成浏览器中的段落与行内标签?
在他的分析中,DomCanvas 主要通过 HTML 元素承接绘制;BitmapCanvas 的路径也不是始终只画 Canvas,还会因具体能力与兼容条件转向元素。理解分支,比只记住渲染器名字更能解释实际输出。
他的调用链从 framework 的 Canvas 接口走到 Web 端的 SurfaceCanvas,再区分 DomCanvas 与 BitmapCanvas。前者走元素路径;后者优先处理位图绘制,却仍可能在某些内容上生成元素。尤其是文字还会受周围绘制状态影响,因此渲染器叫作“HTML”,也不能推断所有内容都是普通 HTML 标签。
同一段文字,加背景后为什么换了画法
第一个连续示例从简单文本开始:当时实现把它绘制到 Canvas。增加红色背景后,结果却变成了段落和行内元素。
郭树煜沿绘制矩形、再绘制文本的路径解释这种变化。背景先触发了元素绘制,当前绘制状态又影响后续段落的处理,最终让文本也进入元素路径。也就是说,文本怎样显示不仅取决于 Text 自己的属性,还可能与同一绘制过程中的其他操作有关。
他在源码中追到绘制矩形时的条件判断,再看段落绘制如何读取此前的状态。没有背景时,简单文字走 Canvas;加了红底,背景进入元素绘制路径,后续文字随之采用 p 与 span。这不是“红色文字一律用 DOM”的规则,而是同一段操作次序改变了当前画布的判断条件。
这一点对排查很重要:遇到文字清晰度或输出结构变化时,仅比较字符串和字体参数,可能还没有覆盖真正变化的条件。
阴影、滤镜与矩阵,继续改变分支
在文字和红色背景上加阴影后,演示中的输出重新回到 Canvas 路径。再加入颜色滤镜,又转为元素绘制。郭树煜把原因追到过滤参数与宿主兼容性:当某些效果不能可靠地通过目标浏览器的 Canvas 路径呈现时,引擎选择其他方式承接。
他以当时 Safari 等浏览器对过滤效果的兼容为例,解释为什么引擎会优先采用元素表达某些滤镜或阴影。一个例子从“只有文字”到“文字加背景”,再到“背景加阴影”“再加颜色滤镜”,输出在 Canvas 与元素之间反复切换。只看最终截图,几乎看不出是哪一次状态改变触发了分支;逐项添加效果和检查输出,才是他演示的排查方法。
复杂矩阵变换也有相似情况。片中用带立体效果的变换展示,输出不再只按最初的简单绘制逻辑处理。这些例子的共同点是:引擎需要在 Flutter 的效果表达与浏览器实际能力之间协调,分支反映的是当时的实现和兼容条件。
背景走 Canvas,文本仍然可能走 DOM
接着,郭树煜撤掉前面的滤镜与复杂变换,在带阴影的场景给文本加装饰线。结果是背景仍由 Canvas 绘制,文本却变成段落与行内元素。
他进一步提到字形特性等条件:某些文字功能利用 HTML 与 CSS 更便于表达,因此文本自己的判断也会改变最终路径。父级走 Canvas,并不能推出所有子内容一定走 Canvas。
装饰线是视频中的具体例子;他还讨论过字体特性与字形处理。对这些文本能力,使用浏览器标签和 CSS 可能更容易得到所需结果,所以段落有独立于背景图形的判断。看见背景是 Canvas 只能说明那一层怎样画,不能替文字这一层作结论。
去掉文本,再看简单图形与图层
最后几个示例把文本移除,只保留简单矩形及相关样式,以观察何时进入 DomCanvas。郭树煜随后把前面的分支重新串起来:绘制操作、滤镜、变换和文本特性一起决定走向,不能用单个属性概括所有情况。
他又把页面中的 flt- 自定义元素与图层操作关联起来。例如添加 picture 会留下相应层级,层内部再选择 Canvas 或元素完成具体呈现。理解这层关系后,就能从浏览器看到的结构追到渲染过程,而不只是面对陌生标签猜测。
简单矩形没有段落等操作时,走向可能与有文字时不同。他最后把这些规则收束成判断顺序:先看过滤器、复杂变换与绘制状态,再看当前图形是否需要元素承接,最后单独检查段落的文字特性。浏览器里看到的 flt-picture 等层级,对应框架添加的图层;在图层内部,依然可能混用 Canvas 与元素。
分享最后回到实用目的:构建优化改善加载,理解绘制路径则帮助解释表现差异,尤其是特定分辨率或条件下的文本清晰度。不同平台与版本可能作不同选择,排查应结合实际输出和实现,而不是认为所有端天然完全一致。本分段以总结结束,未收录现场问答。
