美团外卖商家端基于 FlutterWeb 的技术探索已久,目前在多个业务中落地了App、PC、H5的多端复用,有效提升了产研的整体效率。在这过程中,性能问题是我们面临的最大挑战,本文结合实际业务场景进行思考,介绍美团外卖商家端在 FlutterWeb 性能优化上所进行的探索和实践,希望对大家能有所帮助或启发。
一、背景
1.1 关于FlutterWeb
1.2 业务现状
二、挑战
三、整体设计
四、设计与实践
4.1 精简 SDK
4.2 JS 分片
4.3 预加载方案
4.4 分平台打包
4.5 图标字体精简
五、总结与展望
时间回拨到 2018 年,Google 首次公开 FlutterWeb Beta 版,表露出要实现一份代码、多端运行的愿景。经过无数工程师两年多的努力,在今年年初(2021 年 3 月份),Flutter 2.0 正式对外发布,它将 FlutterWeb 功能并入了 Stable Channel,意味着 Google 更加坚定了多端复用的决心。
图1 FlutterWeb历史
当然 Google 的“野心”不是没有底气的,主要体现在它强大的跨端能力上,我们看一下 Flutter 的跨端能力在 Web 侧是如何体现的:
图2 Flutter跨端能力
上图分别是 FlutterNative 和 FlutterWeb 的架构图。通过对比可以看出,应用层 Framework 是公用的,意味着在 FlutterWeb 中我们也可以直接使用 Widgets、Gestures 等组件来实现逻辑跨端。而关于渲染跨端,FlutterWeb 提供了两种模式来对齐 Engine 层的渲染能力:Canvaskit Render 和 HTML Render,下方表格对两者的区别进行了对比:
图3 模式对比
Canvaskit Render 模式:底层基于 Skia 的 WebAssembly 版本,而上层使用 WebGL 进行渲染,因此能较好地保证一致性和滚动性能,但糟糕的兼容性(WebAssembly 从 Chrome 57 版本才开始支持)是我们需要面对的问题。此外 Skia 的 WebAssembly 文件大小达到了 2.5M,且 Skia 自绘引擎需要字体库支持,这意味着需要依赖超大的中文字体文件,对页面加载性能影响较大,因此目前并不推荐在 Web 中直接使用 Canvaskit Render(官方也建议将 Canvaskit Render 模式用于桌面应用)。
HTML Render 模式:利用 HTML + Canvas 对齐了 Engine 层的渲染能力,因此兼容性表现优秀。另外,MTFlutterWeb 对滚动性能已有过探索和实践,目前能够应对大部分业务场景。而关于加载性能,此模式下的初始包为 1.2M,是 Canvaskit Render 模式产物体积的 1/2,且我们可对编译流程进行干预,控制输出产物,因此优化空间较大。
基于以上原因,美团外卖技术团队选择在 HTML Render 模式下对 FlutterWeb 页面的性能进行优化探索。
美团外卖商家端以 App、PC 等多元化的形态为商家提供了订单管理、商品维护、顾客评价、外卖课堂等一系列服务,且 App、PC 双端业务功能基本对齐。此外,我们还在 PC 上特供了针对连锁商家的多店管理功能。同时,为满足平台运营诉求,部分业务具有外投 H5 场景,例如美团外卖商家课堂,它是一个以文章、视频等形式帮助商家学习外卖运营知识、了解行业发展和跟进经营策略的内容平台,具有较强的传播属性,因此我们提供了站外分享的能力。
图4 业务形态
为了实现多端(App、PC、H5)复用,提升研发效率,我们于 2021 年年初开始着手 [MTFlutterWeb] 研发体系的建设。目前,我们基于 MTFlutterWeb 完成提效的业务超过了 9 个,在 App 中,能够基于 FlutterNative 提供高性能的服务;在 PC 端和 Mobile 浏览器中,利用 FlutterWeb 做到了低成本适配,提升了产研的整体效率。
然而,加载性能问题是 MTFlutterWeb 应用推广的最大障碍。这里依然以美团外卖商家课堂业务为例,在项目之初页面完全加载时间 TP90 线达到了 6s 左右,距离我们的指标基线值(页面完全加载时间 TP90 线不高于 3s,基线值主要依据美团外卖商家端的业务场景、用户画像等来确定)有些差距,用户访问体验有很大的提升空间,因此 FlutterWeb 页面加载性能优化,是我们亟需解决的问题。
不过,想要突破 FlutterWeb 页面加载的性能瓶颈,我们面临的挑战也是巨大的。这主要体现在 FlutterWeb 缺失静态资源的优化策略,以及复杂的架构设计和编译流程。下图展示了 Flutter 业务代码被转换成 Web 平台产物的流程,我们来具体进行分析:
图5 FlutterWeb 编译流程
可以看出,要完成对 FlutterWeb 编译产物的优化,就需要干预 FlutterWeb 的众多编译模块。而为了提升整体的编译效率,大部分模块都被提前编译成了 snapshot 文件( 一种 Dart 的编译产物,可被 Dart VM 所运行,用于提升执行效率),例如:flutter_tools.snapshot、frontend_server.snapshot、dart2js.snapshot 等,这又加大了对 FlutterWeb 编译流程进行干预的难度。
如前文所述,为了实现逻辑、渲染跨平台,Flutter 的架构设计及编译流程都具有一定的复杂性。但由于各平台(Android、iOS、Web)的具体实现是解耦的,因此我们的思路是定位各模块(Dart-SDK、Framework、Flutter_Web_SDK、flutter_tools)的 Web 平台实现并寻求优化,整体设计图如下所示:
图6 整体设计
工欲善其事,必先利其器,在开始做体积裁剪之前,我们需要一套类似于 [webpack-bundle-analyzer] 的包体积分析工具,便于直观地比较各个模块的体积占比,为优化性能提供帮助。
Dart2JS 官方提供了 [--dump-info] 命令选项来分析 JS 产物,但其表现差强人意,它并不能很好地分析各个模块的体积占比。这里更推荐使用 [source-map-explorer] ,它的原理是通过 sourcemap 文件进行反解,能清晰地反映出每个模块的占用大小,为 SDK 的精简提供了指引。下图展示了 FlutterWeb JS 产物的反解信息(截图仅包含 Framework 和 Flutter_Web_SDK):
图7 反解信息
FlutterWeb 依赖的 SDK 主要包括 Dart-SDK、Framework 和 Flutter_Web_SDK,这些 SDK 对包体积的影响是巨大的,几乎贡献了初始化包的所有大小。虽然在 Release 模式下的编译流程中,Dart Compiler 会利用 [Tree-Shaking] 来剔除那些引入但未使用的 packages、classes、functions 等,很大程度上减少了包体积。但这些 SDK 中仍然存在一些能被进一步优化的代码。
以 Flutter Framework 为例,由于它是全平台公用的模块,因此不可避免地存在各平台的兼容逻辑(通常以 if-else、switch 等条件判断形式出现),而这部分代码是不能被 Tree-Shaking 剔除的,我们观察如下的代码:
// FileName: flutter/lib/src/rendering/editable.dart
void _handleKeyEvent(RawKeyEvent keyEvent) {
if (kIsWeb) {
// On web platform, we should ignore the key.
return;
}
// Other codes ...
}
上述代码选自 Framework 中的 RenderEditable 类,当 kIsWeb 变量为真,表示当前应用运行在 Web 平台。受限于 Tree-Shaking 的机制原理,上述代码中,其它平台的兼容逻辑即注释 Other codes 的部分是无法被剔除的,但这部分代码,对 Web 平台来说却是 Dead Code(永远不可能被执行到的代码),是可以被进一步优化的。
图8 部分功能构成
上图展示了 SDK 的一部分功能构成,从图中可以看出,FlutterWeb 依赖的这些 SDK 中包含了一些使用频率较低的功能,例如:蓝牙、USB、WebRTC、陀螺仪等功能的支持。为此,我们提供了对这些长尾功能的定制能力(这些功能默认不开启,但业务可配置),将未被启用长尾的功能进行裁剪。
通过上述分析可得,我们的思路就是对 Dead Code 进行二次剔除,以及对这些长尾功能做裁剪。基于这样的思路,我们深入 Dart-SDK、Framework 和 Flutter_Web_SDK 各个击破,最终将 JS Bundle 产物体积由 1.2M 精简至 0.7M,为 FlutterWeb 页面性能优化打下了坚实的基础。
图9 精简成果
为了提升构建效率,我们将 FlutterWeb 依赖的环境定制为 Docker 镜像,集成入 CI/CD(持续集成与部署)系统。SDK 裁剪后,我们需要更新 Docker 镜像,整个过程耗时较长且不够灵活。因此,我们将 Dart-SDK、Framework、Flutter_Web_SDK 按版本打包传至云端,在编译开始前读取 CI/CD 环境变量:sdk_version(SDK 版本号),远程拉取相应版本的 SDK 包,并替换当前 Docker 环境中的对应模块,基于以此方案实现 SDK 的灵活发布,具体流程图如下图所示:
图10 集成CI/CD
FlutterWeb 编译之后默认会生成 main.dart.js 文件,它囊括了 SDK 代码以及业务逻辑,这样会引起以下问题:
针对文件 Hash 化和 CDN 加载的支持,我们在 flutter_tools 编译流程中对静态资源进行二次处理:遍历静态资源产物,增加文件 Hash (文件内容 MD5 值),并更新资源的引用;同时通过定制 Dart-SDK,修改了 main.dart.js、字体等静态资源的加载逻辑,使其支持 CDN 资源加载。
更详细的方案设计请参考[《Flutter Web在美团外卖的实践》]一文。下面我们重点介绍 main.dart.js 分片相关的一些优化策略。
Flutter 官方提供 deferred as
关键字来实现 Widget 的懒加载,而 dart2js 在编译过程中可以将懒加载的 Widget 进行按需打包,这样的拆包机制叫做 Lazy Loading。借助 Lazy Loading,我们可以在路由表中使用 deferred 引入各个路由(页面),以此来达到业务代码拆离的目的,具体使用方法和效果如下所示:
// 使用方式
import 'pages/index/index.dart' deferred as IndexPageDefer;
{
'/index': (context) => FutureBuilder(
future: IndexPageDefer.loadLibrary(),
builder: (context, snapshot) => IndexPageDefer.Demo(),
)
... ...
}
图11 效果演示
使用 Lazy Loading 后,业务页面的代码会被拆分到了多个 PartJS(对应图中 xxx.part.js 文件) 中。这样看似解决了业务代码与 SDK 耦合的问题,但在实际操作过程中,我们发现每次业务代码的变动,仍然会导致编译后的 main.dart.js 随之发生变化(文件 Hash 值变化)。经过定位与跟踪,我们发现这个变化的部分是 PartJS 的加载逻辑和映射关系,我们称之为 Runtime Manifest。因此,需要设计一套方案对 Runtime Manifest 进行抽离,来保证业务代码的修改对 main.dart.js 的影响达到最低。
通过对业务代码的抽离,此时 main.dart.js 文件的构成主要包含 SDK 和 Runtime Manifest:
图12 main.dart.js构成
那如何能将 Runtime Manifest 进行抽离呢?对比常规 Web 项目,我们的处理方式是把 SDK、Utils、三方包等基础依赖,利用 Webpack、Rollup 等打包工具进行抽离并赋予一个稳定的 Hash 值。同时,将 Runtime Manifest (分片文件的加载逻辑和映射关系)注入到 HTML 文件中,这样保证了业务代码的变动不会影响到公共包。借助常规 Web 项目的编译思路,我们深入分析了 FlutterWeb 中 Runtime Manifest 的生成逻辑和 PartJS 的加载逻辑,定制出如下的解决方案:
图13 Runtime Manifest抽离
在上图中,Runtime Manifest 的生成逻辑位于 Dart2JS Compiler 模块,在该生成逻辑中,我们对 Runtime Manifest 代码块进行了标记,之后在 flutter_tools 中将标记的 Runtime Manifest 代码块抽离并写入 HTML 文件中(以 JS 常量形式存在)。而在 PartJS 的加载流程中,我们将 manifest 信息的读取方式改为了 JS 常量的获取。按照这样的拆分方式,业务代码的变更只会改变 Runtime Manifest 信息 ,而不会影响到 main.dart.js 公共包。
经过以上引入 Lazy Loading、Runtime Manifest 抽离,main.dart.js 文件的体积稳定在 0.7M 左右,浏览器对大体积单文件的加载,会有很沉重的网络负担,所以我们设计了切片方案,充分地利用浏览器对多文件并行加载的特性,提升文件的加载效率。
具体实现方案为:将 main.dart.js 在 flutter_tools 编译过程拆分成多份纯文本文件,前端通过 XHR 的方式并行加载并按顺序拼接成 JavaScript 代码置于 <script>
标签中,从而实现切片文件的并行加载。
图14 并行加载
如上一节所述,虽然我们做了很多工作来稳定 main.dart.js 的内容,但在 Flutter Tree-Shaking 的运行机制下,各个项目引用不同的 Framework Widget,就会导致每个项目生成的 main.dart.js 内容不一致。随着接入 FlutterWeb 的项目越来越多,每个业务的页面互访概率也越来越高,我们的期望是当访问 A 业务时,可以预先缓存 B 业务引用的 main.dart.js,这样当用户真正进入 B 业务时就可以节省加载资源的时间,下面为详细的技术方案。
我们把整体的技术方案分为编译、监听、运行三个阶段。
下图为预缓存的整体方案设计:
图15 预缓存方案设计
编译阶段
编译阶段会扩展现有的发布流水线,在 flutter build 之后增加 prefetch build 作业,这样 build 之后就可以对产物目录进行遍历和筛选,得到我们所需资源进而生成云端 JSON,为运行阶段提供数据基础。下面的流程图为编译阶段的详细方案设计:
图16 预缓存编译阶段
编译阶段分为三部分:
通过对流水线编译期的整合,我们可以生成新的云端 JSON 并上传到云端,为运行阶段的下发提供数据基础。
监听阶段我们知道,浏览器对文件请求的并发数量是有限制的,为了保证浏览器对当前页面的渲染处于高优先级,同时还能完成预缓存的功能,我们设计了一套对缓存文件的加载策略,在不影响当前页面加载的情况下,实现对缓存文件的加载操作。以下为详细的技术方案:
图17 预缓存监听阶段
在页面 DOMContentLoaded 之后,我们会监听三部分的的变化。
通过上述步骤,我们就可以得到一个首屏渲染完成的时机,之后就可以实现预缓存功能了。以下为预缓存功能的实现。
运行阶段
预缓存的整体流程为:下载编译阶段生成的云端 JSON,解析出需要进行预缓存资源的 CDN 路径,最后通过 HTTP XHR 进行缓存资源进行请求,利用浏览器本身的缓存策略,把其他业务的资源文件写入。当用户访问已命中缓存的页面时,资源已被提前加载,这样可以有效地减少首屏的加载时间。下图为运行阶段的详细方案设计:
图18 预缓存运行阶段
在监听阶段,我们可以获取到页面的首屏渲染完成的时机,会获取到云端 JSON,首先判断该项目的缓存是否为启用状态。当该项目可用时,会根据全局变量 PROJECT_ID 进行资源数组的匹配,再以 HTTP XHR 方式进行预访问,把缓存文件写入浏览器缓存池中。至此,资源预缓存已执行完毕。
当有页面间互访问命中预缓存时,浏览器会以 200(Disk Cache)的方式返回数据,这样就节省了大量资源加载的时间,下图为命中缓存后资源加载情况:
图19 预缓存效果展示
目前,美团外卖商家端业务已有 10+ 个页面接入了预缓存功能,资源加载 90 线平均值由 400ms 下降到 350ms,降低了 12.5%;50 线平均值由 114ms 下降到 100ms,降低了 12%。随着项目接入接入越来越多,预缓存的效果也会越发的明显。
图20 预缓存数据展示
如前文所述,美团外卖商家业务大部分都是双端对齐的。为了实现提效的最大化,我们对 FlutterWeb 的多平台适配能力进行加强,实现了 FlutterWeb 在 PC 侧的复用。
在 PC 适配过程中,我们不可避免地需要书写双端的兼容代码,如:为了实现在列表页面中对卡片组件的复用。为此我们开发了一个适配工具 ResponsiveSystem,分别传入 PC 和 App 的各端实现,内部会区分平台完成适配:
// ResponsiveSystem 使用举例
Container(
child: ResponsiveSystem(
app: AppWidget(),
pc: PCWidget(),
),
)
上述代码能较方便的实现 PC 和 App 适配,但 AppWidget 或 PCWidget 在编译过程中都将无法被 Tree-Shaking 去除,因此会影响包体积大小。对此,我们将编译流程进行优化,设计分平台打包方案:
图21 分平台打包
通过这样的方式,我们去除了各自平台的无用代码,避免了 PC 适配过程中引起的包体积问题。依然以美团外卖商家课堂业务(6 个页面)为例,接入分平台打包后,单平台代码体积减小 100KB 左右。
图22 效果展示
当访问 FlutterWeb 页面时,即使在业务代码中并未使用 Icon 图标,也会加载一个 920KB 的图标字体文件:MaterialIcons-Regular.woff。通过探究,我们发现是 Flutter Framework 中一些系统 UI 组件(如:CalendarDatePicker、PaginatedDataTable、PopupMenuButton 等)使用到了 Icon 图标导致,且 Flutter 为了便于开发者使用,提供了全量的 Icon 图标字体文件。
Flutter 官方提供的 --tree-shake-icons
命令选项是将业务使用到的 Icon 与 Flutter 内部维护的一个缩小版字体文件(大约 690KB)进行合并,能一定程度上减小字体文件大小。而我们需要的是只打包业务使用的 Icon,所以我们对官方 tree-shake-icons
进行了优化,设计了 Icon 的按需打包方案:
图23 图标字体精简
通过以上的方案,我们解决了字体文件过大带来的包体积问题,以美团外卖课堂业务(业务代码中使用了 5 个 Icon)为例,字体文件从 920KB 精简为 11.6kB。
图24 效果展示
综上所述,我们基于 HTML Render 模式对 FlutterWeb 性能优化进行了探索和实践,主要包括 SDK(Dart-SDK、Framework、Flutter_Web_SDK)的精简,静态资源产物优化(例如:JS 分片、文件 Hash、字体图标文件精简、分平台打包等)和前端资源加载优化(预加载与按需请求)。最终使得 JS 产物由 1.2M 减少至 0.7M(**非业务代码**),页面完全加载时间 TP90 线由 6s 降到了 3s,这样的结果已能满足美团外卖商家端的大部分业务要求。而未来的规划将聚焦于以下3个方向:
美团外卖技术团队正在基于 FlutterWeb 做更多的探索和尝试。如果你对这方面的技术也比较感兴趣,可以在文末留言,跟我们一起讨论。也欢迎大家给提出一些建议,非常感谢。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/fE11vs2qnRIHAWYv1af-gA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。