Flutter for Web(FFW)从 2021 年发布至今,在国内外互联网公司已经得到较多的应用。作为 Flutter 技术在 Web 领域的有力扩充,FFW 可以让熟悉 Flutter 的客户端同学直接上手写 H5,复用 App 端代码高效支撑业务需求;在 App 侧 FFW 也可作为 Flutter 动态下发的兜底方案。总的来说在业务和技术上 FFW 都具有相当的价值。
然而在使用 FFW 时有一个明显的问题:其编译产物 main.dart.js
较大,初始的 Hello world 工程编译后产物 js 大小为 1.2 MB,添加业务代码后 js 的大小还会继续增加。在阿里卖家的内容外投业务中,3 个页面的工程 js 大小为 2.0 MB,js 文件过大直接的影响就是页面首次首屏加载的速度。针对 js 的大小有较多优化方法,本文主要记录 main.dart.js
分片优化方案的实现。
图 1. FFW js 分片示意
页面 js 加载速度提升一般从两个角度考虑:
对应到 js 分片方案,主要通过如下两点提升加载速度:
按需加载:在工程中存在多个页面时,不论打开哪个页面都需要加载完整的main.dart.js
,而这里包含了很多不需要的页面代码。如果将各个页面的代码拆分只加载当前页面所需要的代码,则可减少 js 文件体积,而且当其他页面越多逻辑越复杂时,其提升的效果越明显。
并行加载:将 js 分片后会生成多个大小不一的 js 文件,在带宽充足的情况下如果使用并行加载则可以节省较小的分片加载时间。
注:js 文件压缩在线上部署的时候会自动处理,这里不做处理。
通过按需和并行加载提升加载速度,首先需要完成 js 的分片。分片和按需加载操作通常是绑定的,如在前端 Vue 开发中,可使用 webpack 的 code splitting[1] 工具在定义好各类库的使用关系后实现文件分割和按需加载,类似的在 flutter 中则可使用 延迟加载组件[2] 功能。
Flutter 为 App 设计的延迟组件加载功能同样适用于 FFW。在 dart 代码中通过关键字 deffered as
引入相关代码库并在使用时加载即可实现延迟加载功能。在官方的示例中可以通过如下的方式实现 box.dart
的延迟加载。
// box.dart
import 'package:flutter/material.dart';
/// 一个正常方式编写的 widget,后面会被延迟加载
class DeferredBox extends StatelessWidget {
const DeferredBox({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 30,
width: 30,
color: Colors.blue,
);
}
}
在需要使用 box.dart
的地方通过 deferred as
关键字引入 box.dart
/// some_widget.dart
import 'package:flutter/material.dart';
/// 1. deferred as 引入
import 'box.dart' deferred as box;
class SomeWidget extends StatefulWidget {
const SomeWidget({Key? key}) : super(key: key);
@override
State<SomeWidget> createState() => _SomeWidgetState();
}
之后调用延迟加载库的加载方法,加载完成后使用即可
/// some_widget.dart
class _SomeWidgetState extends State<SomeWidget> {
late Future<void> _libraryFuture;
@override
void initState() {
/// 2. 使用时加载延迟加载库
_libraryFuture = box.loadLibrary();
super.initState();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: _libraryFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {return Text('Error: ${snapshot.error}');}
/// 3. 延迟加载库加载完成后使用
return box.DeferredBox();
}
return const CircularProgressIndicator();
},
);
}
}
经过上述操作后,在 FFW 中编译后可生成类似如下的两个 js 文件:
├── [1.2M] main.dart.js /// FFW 引擎和主工程内容
├── [616B] main.dart.js_1.part.js /// 存放 box.dart 对应的内容
在多页面的工程中使用延迟组件加载即可完成多页面的分片,可进行接下来的改造工作。
在阿里卖家 FFW 工程中,为了尽可能的做到只加载必须内容,我们从路由跳转位置将各页面改造为延迟加载方式。
/// main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AliSupplier Headline',
debugShowCheckedModeBanner: false,
onGenerateRoute: RouteConfiguration.onGenerateRoute,
onGenerateInitialRoutes: (settings) {
return [RouteConfiguration.onGenerateRoute(RouteSettings(name: settings))];
},
);
}
}
/// routes.dart
import 'package:alisupplier_content/business/distribution/page/sellerapp_page.dart';
import 'package:alisupplier_content/business/webmain/page/web_news_detail_page.dart';
import 'package:alisupplier_content/debug/page/debug_main_page.dart';
/// 路由和页面 builder 的 map
static Map<String, RouteWidgetBuilder?> builders = {
'/debug': (context, params) {
return DebugMainPage(title: 'Debug');
},
'/web_news_detail': (context, params) {
return WebNewsDetailPage(
courseCode: params?['courseCode'] ?? params?['c'] ?? '',
sourceId: params?['sourceId'] ?? params?['s'] ?? '',
);
},
'/sellerapp': (context, params) {
return SellerAppPage(
url: params?['url'] ?? '',
sourceId: params?['sourceId'] ?? params?['s'] ?? '',
);
},
};
/// routes.dart
class RouteConfiguration {
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
return NoAnimationMaterialPageRoute(
settings: settings,
builder: (context) {
var uri = Uri.parse(settings.name ?? '');
/// 根据 path 找页面的 builder
var route = builders[uri.path];
if (route != null) {
return route(context, uri.queryParameters);
} else {
/// 404 页面
return CommonPageNotFound(routeSettings: settings);
}
},
);
}
}
创建 DeferredLoaderWidget
执行各页面加载操作
/// routes.dart
class RouteConfiguration {
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
return NoAnimationMaterialPageRoute(
settings: settings,
builder: (context) {
/// 承担路由和加载工作
return DeferredLoaderWidget(
settings: settings,
);
},
);
}
}
在 DeferredLoaderWidget
中将各页面通过 deferred as
方式引入
/// deferred_loader_widget.dart, 新添加的文件
import '../../business/distribution/page/sellerapp_page.dart' deferred as sellerapp;
import '../../business/webmain/page/web_news_detail_page.dart' deferred as web_news_detail;
import '../../debug/page/debug_main_page.dart' deferred as debug;
import '../../ability/common/page/common_page_not_found.dart' deferred as pageNotFound;
import 'package:flutter/material.dart';
typedef WidgetConstructer = Widget Function(Map? params);
/// 分包加载: library 加载 map
/// <页面地址,library加载方法>
var _loadLibraryMap = {
'/sellerapp': sellerapp.loadLibrary,
'/web_news_detail': web_news_detail.loadLibrary,
'/debug': debug.loadLibrary,
};
/// 分包加载: 页面 widget 创建方法 map
/// <页面地址,widget 创建方法>
var _constructorMap = {
'/sellerapp': () => sellerapp.widgetConstructor,
'/web_news_detail': () => web_news_detail.widgetConstructor,
'/debug': () => debug.widgetConstructor,
};
之后在需要的时候对页面进行加载,在 _DeferredLoaderWidgetState.initState
中执行加载操作:
/// deferred_loader_widget.dart
@override
void initState() {
super.initState();
/// 路由解析
Uri uri = Uri.parse(widget.settings.name ?? '');
path = uri.path;
params = uri.queryParameters;
/// 根据 path 找到 libraryLoad 方法
Future Function()? loadLibrary = _loadLibraryMap[path];
/// 未找到时使用 404 页面 loadLibrary
if (loadLibrary == null) {
loadLibrary = pageNotFound.loadLibrary;
params = {'settings': widget.settings};
}
loadFuture = loadLibrary.call();
}
DeferredLoaderWidgetState.build
中进行 widget 的创建:
/// deferred_loader_widget.dart
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: loadFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text('页面加载失败,请重试');
}
var constructor = _constructorMap[path];
if (constructor == null) {
/// 页面未找到
constructor = () => pageNotFound.widgetConstructor;
}
return constructor().call(params);
} else {
return Container();
}
},
);
}
其中对于每个页面在其头部定义构造统一的构造方法,以 sellerapp
为例:
/// sellerapp_page.dart
/// 页面构造方法
WidgetConstructer widgetConstructor = (params) {
return SellerAppPage(
url: params?['url'] ?? '',
sourceId: params?['sourceId'] ?? params?['s'] ?? '',
);
};
在进行延迟加载改造时有两个需要注意的点:
deferred as
命名引用到widgetConstructor
需要在相应的 library load 之后才能实际调用,在此之前引用的值会在使用时无效,如将 deferred_loader_widget
中 _constructorMap
进行如下修改:
图 2. widgetConstructor 错误使用方式说明
则运行时会得到如下的报错信息
图 3. widgetConstructor 错误使用方式报错信息
改造完成后即可进行编译调试,查看 js 分片和按需加载的效果。
产物对比
查看编译产物发现 main.dart.js
被拆分成了一个较小的 main.dart.js
和诸多小的 main.dart.js_xx.part.js
图 4. 分片前后编译产物对比
页面加载对比
在浏览器中查看页面 js 加载发现资讯页和下载页总的 js 大小均有减少,下载页因压缩问题传输 js 会比分包前稍大,但总大小有所减少,另外因为分包实现了部分的并行加载,总体耗时有所减少:
表1. 资讯页 js 加载情况对比
表2. 下载页 js 加载情况对比
在实验室环境经过多次测试后取平均时间,发现下载页耗时减少 15%,资讯页加载总加载耗时减少 9%。由于下载页 js 减少更多结果符合预期。
经过延迟加载改造后,产物 js 分成了多个包,相关页面加载耗时也有所减少,但是在加载中发现一个问题,main.dart.js
和其他分片的 js 不是同时加载的:
图 5. 分片后 js 加载时序
main.dart.js_xx.part.js
是在 main.dart.js
加载完成之后过了相当一段时间才开始加载,这浪费了很多的加载时间,如果所有的分片 js 都在 main.dart.js
加载时同时加载,则加载耗时基本只会和 main.dart.js
加载耗时相同。
为了让所有分片 js 同时加载,首先观察分片的加载过程。打开页面后检查页面发现情况如下,页面内被注入了分片 js 的加载代码:
图 6. FFW 自动注入的分片加载代码
在 main.dart.js
中查找相关分片的文件名,可发现如下内容:
图 7. 分片 main.dart.js 内的 js 加载信息
猜测 main.dart.js
内部包含的各页面所需 js 分片信息的相关字段含义如下:
考虑如果能将 main.dart.js
中注入分片的时间提前到 main.dart.js
加载时,则可实现理想的并行加载效果。由于 main.dart.js
还未加载相关注入的代码不可用,则只能在 index.html
中添加分片的加载代码。
有了实现的思路,接下来就是进行操作和验证。我们使用构建脚本中解析延迟组件信息,并将解析处理后的信息写入 index.html
中的方案来实现 js 分片的并行加载。
首先在 index.html
中增加加载 js 分片的代码:
<!-- ffw 分包并行加载,根据页面 path 并行加载相关的 part.js,不用等到 ffw 执行时自己去加载 -->
<script id="flutterJsPatchLoad">
// 使用脚本替换内容
var deferredLibraryParts = {};
// 使用脚本替换内容
var deferredPartUris = [];
// 使用脚本替换内容
var base = "";
// 根据页面路径加载所需 js 分片,为了方便要求 DeferredLoaderWidget 中 _loadLibraryMap key 的名称
// 和延迟组件的名称相同
var hash = window.location.hash.substring(2);
var path = hash.split('?')[0];
if (deferredLibraryParts[path]) {
for (var index in deferredLibraryParts[path]) {
loadScript(deferredPartUris[index])
}
}
function loadScript(url) {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = base + url;
document.body.appendChild(script);
}
</script>
之后在构建脚本中解析组件信息,并替换到 deferredLibraryParts
和 deferredPartUris
中,同时在线上发布时将分片 js 的 base 路径替换为实际的 cdn 地址:
# 从 main.dart.js 中获取 js 分包信息,写入 index.html 中预加载部分的变量中
def write_js_patch_info():
# 从 main.dart.js 获取两个参数:deferredLibraryParts、deferredPartUris
# 这个阶段在本地编译时执行
parts = reg_find_file_content('./build/web/main.dart.js', r'deferredLibraryParts:{(.*?)},')[0]
uris = reg_find_file_content('./build/web/main.dart.js', r'deferredPartUris:\[(.*?)\],')[0]
str_replace_file_content('./build/web/index.html', r'deferredLibraryParts = {}', r'deferredLibraryParts = {' + parts + r'}')
str_replace_file_content('./build/web/index.html', r'deferredPartUris = []', r'deferredPartUris = [{}]'.format(uris))
# 修改 index.html 中的 base 为实际的cdn地址
def change_base(version, publish_env):
str_replace_file_content('./build/web/index.html', r'base = ""', r'base = "{}"'.format(get_base(version, publish_env)))
构建过程中经过脚本的替换,index.html
内容更新如下:
<!-- ffw 分包并行加载,根据页面 path 并行加载相关的 part.js,不用等到 ffw 执行时自己去加载 -->
<script id="flutterJsPatchLoad">
// 使用脚本替换内容
var deferredLibraryParts = {sellerapp:[0,1,2,3],web_news_detail:[0,4,1,5,2,6],debug:[0,4,1,7,5,8],pageNotFound:[0,4,7,9]};
// 使用脚本替换内容
var deferredPartUris = ["main.dart.js_3.part.js","main.dart.js_9.part.js","main.dart.js_7.part.js","main.dart.js_6.part.js","main.dart.js_4.part.js","main.dart.js_11.part.js","main.dart.js_10.part.js","main.dart.js_2.part.js","main.dart.js_12.part.js","main.dart.js_1.part.js"];
// 使用脚本替换内容
var base = "https://g.alicdn.com/algernon/alisupplier_content_web/2.0.5/";
// 根据页面路径加载所需 js 分片,为了方便要求 DeferredLoaderWidget 中 _loadLibraryMap key 的名称
// 和延迟组件的名称相同
var hash = window.location.hash.substring(2);
var path = hash.split('?')[0];
if (deferredLibraryParts[path]) {
for (var index in deferredLibraryParts[path]) {
loadScript(deferredPartUris[index])
}
}
function loadScript(url) {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = base + url;
document.body.appendChild(script);
}
</script>
构建部署完成后测试加载过程如下,发现各分片 js 加载完成时间接近,基本与 main.dart.js
加载完成时间相同:
图 8. 并行加载改造后的 js 加载时序
同时检查页面发现,FFW 没有再额外注入分片 js 的加载代码,至此分片 js 并行加载达到了理想的效果。
图 9. 并行加载改造后 FFW 不再注入分片加载代码
在实际使用中发现 deferredLibraryParts
中包含的信息与实际所需分片可能不完全相同,如在 main.dart.js
中资讯页面的deferredLibraryParts
加载信息为 0,4,1,5,2,6
6 个分片,但在实际打开页面的时候发现还会加载 index 为 7
的分片:
图 10. FFW 额外需加载的 js 分片
简单的解析 deferredLibraryParts
不够精确,要做到更精确还需深入分析 main.dart.js
代码,这里目前采用人工修正的方式处理。
经过并行加载改造后,资讯页面总加载耗时进一步减少,加载耗时由 -9% 变为 -15%。下载页则提升不明显,考虑原因为下载页多图片资源占比稍大,IO资源在非并行的状态下已经得到了较为充分的使用。
由于当前阿里卖家 FFW 页面访问量不够大,同时线上性能数据为初次启动和非初次启动的混合数据不易区分,这里使用多次实验取平均数方式分析效果。
图 11. 资讯页下载页分片及并行改造结果对比
分析结论如下:
分片之后 main.dart.js
还有 1.3 MB 的体积,还有优化空间,另外延迟加载信息的解析还未做到完全精确。总体来说在加载提速上未来可做的事情还有:
main.dart.js
大小FFW 在生产环境使用的条件已经成熟,在当前开发人员存量的情况,FFW 是端技术同学的一大利器。FFW 当前与前端体系的分离是影响其在前端推广使用的一大阻力,如果能做好 FFW 和现有前端体系的融合,相信会更加的繁荣。
[1]code splitting: https://webpack.js.org/guides/code-splitting/
[2]延迟加载组件: https://flutter.cn/docs/perf/deferred-components
本文由哈喽比特于1年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/aeI_64dT9nHMrHajeQctAw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为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 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。