一起漫部 是基于区块链技术创造的新型数字生活。

目录

  • 前言
  • 开发环境
  • 渲染模式
  • 首屏白屏
  • 优化方案
    • 启屏页优化
    • 包体积优化
      • 去除无用的icon
      • 裁剪字体文件
      • deferred延迟加载
      • 启用gzip压缩
    • 加载优化
      • 大文件分片下载
      • 资源文件hash化
      • 资源文件cdn化
  • 成果
  • 参考链接
  • 总结

前言

不久前,App 小组面临一场开发挑战,即『一起漫部』需要在 App 的基础上开发出一套 H5 版本。

由于一起漫部 App 版本是使用 Flutter 技术开发的,对于 H5 版本的技术选型,Flutter Web 成为我们的第一选择对象。 通过调研, 我们了解到在 Flutter 1.0发布会上由介绍如何让 Flutter 运行在Web 上而提出 Flutter Web 的概念, 到 Flutter1.5.4 版本推出 Flutter Web 的预览版,到 Flutter 2.0官方宣布 Flutter Web 现已进入稳定版, 再到如今 Flutter 对 Web 的不断更新,我们看到了 Flutter Web 的发展优势。同时,为了复用现有 App 版本的代码,我们团队决定尝试使用 Flutter Web 来完成一起漫部 H5 版本的开发。

经过 App 组小伙伴的共同努力,一起漫部在 Flutter Web 的支持下完成了 H5 端的复刻版本, 使 H5 端保持了和 App 同样的功能以及交互体验。 在项目实践过程中,Flutter Web 带来的整体验还不错,但依然存在较大的性能问题,主要体现在首屏渲染时间长,用户白屏体验差, 本篇文章也将围绕此问题,分析一起漫部是如何逐步优化,提升用户体验的。

开发环境

分析性能问题之前,简单介绍下所使用的开发环境,主要包括设备环境、Flutter 环境和 Nginx 环境三方面。

设备环境

Flutter 环境

如图所示,我们团队是在 Flutter 3.0.5 版本上进行 App to Web 的工作。

Nginx 环境

    server {listen       9090;server_name  localhost;location / {root   /build/web;index  index.html index.htm;try_files $uri $uri/ /index.html;}location /api {proxy_pass xxx-xxx-xxx; # your server domain}}

为了方便发布测试,我在本地搭建了一个 nginx 服务器,版本是 1.21.6,同时新建了个 server 配置,将本地 9090 端口指向 Flutter Web打包产物的根路径,当在浏览器输入http://localhost:9090/ 即可正常访问一起漫部 Web 应用,具体的的 server 配置见上图。

渲染模式

对开发环境有了大概了解后,我们再学习下如何构建 Flutter Web 应用。

官方提供了Flutter build web命令来构建 Web 应用,并且支持 canvaskit、html 两种渲染器模式,通过--web-renderer参数来选择使用。

canvaskit

当使用 canvaskit 渲染器模式时,flutter 将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染元素

  • 优点:渲染性能更好,跨端一致性高,
  • 缺点:应用体积变大,打开速度慢(需要加载 canvaskit.wasm 文件),兼容性相对差

html

当使用 html 渲染器模式时,flutter 采用 HTML 的 Custom Element、CSS、SVG、2D Canvas 和 WebGL 组合渲染元素

  • 优点:应用体积更小,打开速度较快,兼容性更好
  • 缺点:渲染性能相对差,跨端一致性受到影响

此外,执行Flutter build web命令构建时,--web-renderer参数的默认值是auto,即实际执行的是flutter build web --web-renderer auto命令。 有趣的是,auto模式会自动根据当前运行环境来选择渲染器,当运行在移动浏览器端时使用 html渲染器,当运行在桌面浏览器端时使用 canvaskit 渲染器。

一起漫部 H5 版本主要是运行在移动浏览器端,为了有更好的兼容性、更快的打开速度以及相对较小的应用体积,直接采用 html 渲染器模式。

首屏白屏

当执行flutter build web --web-renderer html命令完成 Web 应用构建后,我们使用 Chrome 浏览器直接访问http://192.168.1.4:9090/, 很明显的感觉到了首屏加载慢,用户白屏的体验,即首屏白屏问题。那么为什么会出现白屏问题?

首先,我们需要了解浏览器渲染过程:

  1. 解析 HTML,构建 DOM 树
  2. 解析 CSS,构建 CSSOM 树
  3. 合并 DOM 树和 CSSOM 树,构建 Render 渲染树
  4. 遍历 Render 渲染树计算节点位置大小进行布局
  5. 根据节点位置大小信息,进行绘制
  6. 遇到script暂停渲染,优先解析执行javascript,再继续渲染
  7. 最后绘制出所有节点,展现页面

通过 Performance 工具分析:

  • 浏览器等待 HTML 文档返回,此时处于白屏状态,理论白屏时间
  • 解析完HTML文档后开始渲染首屏,出现灰屏(测试背景)状态,实际白屏时间-理论白屏时间
  • 加载JS、解析JS等过程耗时长,导致界面长时间处于灰屏(测试背景)状态
  • JS解析完成后,界面渲染出大概的框架结构
  • 请求API获取到数据后开始显示渲染出首屏页面

通过 Network 工具分析:

  • 首屏页面总共发起 21 个 request,传输 7.3MB 数据,耗时 8.31s;
  • 根据请求资源大小排序,main.dart.js传输 5.6M 资源耗时 5.22s,MaterialIcons-Regular.otf传输 1.6M 资源耗时 1.58s, 其它资源传输数据小耗时短。

由分析得出结论,在首屏渲染过程当中,因为等待资源文件加载、DOM 树构建、JS 解析、布局和绘制等耗时工作, 导致用户长时间处于不可交互的白屏状态,给用户的一种网页很的感觉。

优化方案

如果网站太慢会影响用户体验,那么要如何优化呢?

启屏页优化

针对白屏问题,我们从 Flutter 为 Android 提供 SplashScreenDrawable 的设置得到启发,在 Web 上同样建立一个启屏页,在启屏页中 通过添加 Loading或骨架屏去给用户呈现了一个动态的页面,从而降低白屏体验差的影响。当然,这只是一个治标不治本的方案,因为从根本上没有解决加载慢的问题。具体实现的话,在index.html里面放置一起漫部的 logo并添加相应的动画样式,在 window 的 load 事件 触发时显示 logo,最后在应用程序第一帧渲染完成后移除即可。

启屏页实现代码,仅供参考:


<div id="loading"><style>body {inset: 0;overflow: hidden;margin: 0;padding: 0;position: fixed;left: 0;top: 0;right: 0;bottom: 0;}#loading {width: 100%;height: 100%;display: flex;align-items: center;justify-content: center;}#loading img {border-radius: 16px;width: 90px;height: 90px;animation: 1s ease-in-out 0s infinite alternate breathe;opacity: 0.66;transition: opacity 0.4s;}#loading.main_done img {opacity: 1;}#loading.init_done img {opacity: 0.05;}@keyframes breathe {from {transform: scale(1);}to {transform: scale(0.95);}}</style><img src="icons/Icon-192.png" alt="Loading..."/>
</div>
<script>window.addEventListener("load", function (ev) {var loading = document.querySelector("#loading");// Download main.dart.js_flutter.loader.loadEntrypoint({serviceWorker: {serviceWorkerVersion: serviceWorkerVersion,},}).then(function (engineInitializer) {loading.classList.add("main_done");return engineInitializer.initializeEngine();}).then(function (appRunner) {loading.classList.add("init_done");return appRunner.runApp();}).then(function (app) {// Wait a few milliseconds so users can see the "zoooom" animation// before getting rid of the "loading" div.window.setTimeout(function () {loading.remove();}, 200);});});
</script>

包体积优化

我们先了解下 Flutter Web 的打包文件结构:

├── assets                                          // 静态资源文件,主要包括图片、字体、清单文件等
│   ├── AssetManifest.json                    // 资源(图片、视频、文件等)清单文件
│   ├── FontManifest.json                     // 字体清单文件
│   ├── NOTICES
│   ├── fonts
│   │   └── MaterialIcons-Regular.otf   // 字体文件,Material风格的图标
│   ├── images                                // 图片文件夹
├── canvaskit                                       // canvaskit渲染模式构建产生的文件
├── favicon.png
├── flutter.js                                      // FlutterLoader的实现,主要是下载main.dart.js文件、读取service worker缓存等,被index.html调用
├── flutter_service_worker.js                       // service worker的使用,主要实现文件缓存
├── icons                                           // pwa应用图标
├── index.html                                      // 入口文件
├── main.dart.js                                    // JS主体文件,由flutter框架、第三方库、业务代码编译产生的
├── manifest.json                                   // pwa应用清单文件
└── version.json                                    // 版本文件

分析可知,Flutter Web 本质上也是个单应用程序,主要由index.html入口文件、main.dart.js主体文件和其它资源文件组成。浏览器请求 index.html 后,首先下载main.dart.js主文件,再解析和执行js文件,最后渲染出页面。通过首屏白屏问题分析,我们知道网页慢主要是加载资源文件耗时过长,尤其是main.dart.jsMaterialIcons-Regular.otf两个文件,针对这两个文件我们又进行了以下优化。

去除无用的icon

Flutter 默认会引用cupertino_icons,打包Web应用会产生一个大小283KB的CupertinoIcons.ttf文件,如果不需要的话可以在pubspec.yaml文件中去掉cupertino_icons: ^2.0.0的引用,减少这些资源的加载。

裁剪字体文件

Flutter 默认会打包MaterialIcons-Regular.otf 字体库,里面包含了一些预置的 Material 设计风格 icon,所以体积比较大。但是每次都加载一个1.6M的字体文件是不合理的,我们发现flutter提供--tree-shake-icons命令去裁剪掉没有使用的图标,在尝试flutter build web --web-renderer html --tree-shake-icons打包Web应用时却出现异常。

通过分析我们发现flutter build apk命令也会对MaterialIcons-Regular.otf字体文件进行了裁剪并且没有出现构建异常,因此我们在Flutter Web 下使用 Android 下MaterialIcons-Regular.otf字体文件,结果字体大小从 1.6M 下降到 6kb。

cp -r ./build/app/intermediates/flutter/release/flutter_assets/fonts ./web/assets

MaterialIcons-Regular.otf拷贝至/web/assets目录下,以后每次进行 Web 应用构建将会使用 Android 下MaterialIcons-Regular.otf字体。

deferred延迟加载

main.dart.js包括项目中所有的Dart代码,导致文件体积很大,对此官方提供了deferred关键字来实现Widget的延迟加载,具体使用查看官方文档

我们对deferred的使用进行了封装处理,仅供参考:

/// loadLibrary
typedef AppLibraryLoader = Future<dynamic> Function();/// deferredWidgetBuilder
typedef AppDeferredWidgetBuilder = Widget Function();/// 延迟加载组件
/// 不在 build 里使用 FutureBuilder 加载,因为 build 执行多少次就会导致 widget 创建多少次
/// 这里在 initState 加载,或者当 AppDeferredWidgetBuilder 改变时重新加载
class AppDeferredWidget extends StatefulWidget {const AppDeferredWidget({Key? key,required this.libraryLoader,required this.builder,Widget? placeholder,}): placeholder = placeholder ?? const AppDeferredLoading(),super(key: key);final AppLibraryLoader libraryLoader;final AppDeferredWidgetBuilder builder;final Widget placeholder;static final Map<AppLibraryLoader, Future<dynamic>> _moduleLoaders =<AppLibraryLoader, Future<dynamic>>{};static final Set<AppLibraryLoader> _loadedModules = <AppLibraryLoader>{};/// 预加载static Future<dynamic> preload(AppLibraryLoader loader) {if (!_moduleLoaders.containsKey(loader)) {_moduleLoaders[loader] = loader().then((_) {_loadedModules.add(loader);});}return _moduleLoaders[loader]!;}@overrideState<AppDeferredWidget> createState() => _AppDeferredWidgetState();
}class _AppDeferredWidgetState extends State<AppDeferredWidget> {Widget? _loadedChild;AppDeferredWidgetBuilder? _loadedBuilder;@overridevoid initState() {super.initState();if (AppDeferredWidget._moduleLoaders.containsKey(widget.libraryLoader)) {_onLibraryLoaded();} else {AppDeferredWidget.preload(widget.libraryLoader).then((_) => _onLibraryLoaded());}}void _onLibraryLoaded() {setState(() {_loadedBuilder = widget.builder;_loadedChild = _loadedBuilder?.call();});}@overrideWidget build(BuildContext context) {if (_loadedBuilder != widget.builder && _loadedChild != null) {_loadedBuilder = widget.builder;_loadedChild = _loadedBuilder?.call();}return _loadedChild ?? widget.placeholder;}
}/// 延迟加载Loading
class AppDeferredLoading extends StatelessWidget {const AppDeferredLoading({Key? key}) : super(key: key);@overrideWidget build(BuildContext context) {return Container(color: Colors.white,alignment: Alignment.center,child: const AppLogo(),);}
}
import '../groups/login/login_phone/view/login_phone_page.dart' deferred as login_phone_page;
{AppRoutes.routes_login_phone: (BuildContext context,{Map<String, dynamic>? arguments}) =>AppDeferredWidget(libraryLoader: login_phone_page.loadLibrary,builder: () => login_phone_page.LoginPhonePage(),)
}

使用deferred延迟加载后,业务代码被拆分到多个xxx.part.js的文件,同时主体main.dart.js文件体积从 5.6M 减少至 4.3M,对包体积优化有一定效果。

一开始,我们将项目中所有的路由都使用deferred进行延迟加载,但是 50 个页面却产生了近 200 个xxx.part.js文件,如何管理数量增多的xxx.part.js文件成了新的问题,况且main.dart.js体积减小并没有达到预期,后来我们决定放弃全量使用deferred延迟加载,仅在不同模块间使用。

启用gzip压缩

  #开启gzipgzip  on;#低于1kb的资源不压缩gzip_min_length 1k;# 设置压缩所需要的缓冲区大小gzip_buffers 16 64k;#压缩级别1-9,越大压缩率越高,同时消耗cpu资源也越多,建议设置在5左右。gzip_comp_level 5;#需要压缩哪些响应类型的资源,多个空格隔开。不建议压缩图片.gzip_types text/plain text/css text/javascript text/xml application/json application/x-javascript application/javascript application/xml application/xml+rss image/jpeg image/gif image/png image/jpg;#配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)gzip_disable "MSIE [1-6]\.";#是否添加“Vary: Accept-Encoding”响应头gzip_vary on;

通过配置 nginx 开启 gzip 压缩,main.dart.js传输大小从 5.6M 下降到 1.6M,耗时从 5.22s 减少到 1.42s,速度提升显著。

加载优化

加载优化的话主要从大文件分片下载、资源文件hash化和资源文件cdn化和三方面考虑,对此我们又做了以下优化。为了方便大家使用,开源出flutter_web_optimizer插件,详细编码实现请参考插件。

大文件分片下载

由于main.dart.js体积大,单独下载大文件速度慢,势必影响首屏的加载性能。对此,我们提出分片加载的方案,具体实现如下:

  1. 通过脚本将main.dart.js切割成 6 个单独的纯文本文件
  2. 通过XHR的方式并行下载 6 个纯文本文件
  3. 等待下载完成后,将 6 个纯文本文件按照顺序拼接,得到完整的main.dart.js文件
  4. 创建script标签,将完整的main.dart.js文件内容赋值给text属性
  5. 最后将script标签插入到html body

分片代码,仅供参考:

// 写入单个文件
Future<bool> writeSingleFile({required File file,required String filename,required int startIndex,required endIndex,
}) {final Completer<bool> completer = Completer();final File f = File(path.join(file.parent.path, filename));if (f.existsSync()) {f.deleteSync();}final RandomAccessFile raf = f.openSync(mode: FileMode.write);final Stream<List<int>> inputStream = file.openRead(startIndex, endIndex);inputStream.listen((List<int> data) {raf.writeFromSync(data);},onDone: () {raf.flushSync();raf.closeSync();completer.complete(true);},onError: (dynamic data) {raf.flushSync();raf.closeSync();completer.completeError(data);},);return completer.future;
}final int totalChunk = 6;
final Uint8List bytes = file.readAsBytesSync();
int chunkSize = (bytes.length / totalChunk).ceil();
final List<Future<bool>> futures = List<Future<bool>>.generate(totalChunk,(int index) {return writeSingleFile(file: file,filename: 'main.dart_$index.js',startIndex: index * chunkSize,endIndex: (index + 1) * chunkSize,);},
);
await Future.wait(futures);/// 分片完成后删除 main.dart.js
file.deleteSync();

并行下载代码,仅供参考:

    _downloadSplitJs(url){return new Promise((resolve, reject)=>{const xhr = new XMLHttpRequest();xhr.open("get", url, true);xhr.onreadystatechange = () => {if (xhr.readyState == 4) {if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){resolve(xhr.responseText);}}};xhr.onerror = reject;xhr.ontimeout = reject;xhr.send();})}_retryCount = 0;  const promises = Object.keys(jsManifest).filter(key => /main.dart_\d.js/g.test(key)).sort().map(key => `${assetBase}${jsManifest[key]}`).map(this._downloadSplitJs);Promise.all(promises).then((values)=>{const contents = values.join("");const script = document.createElement("script");script.text = contents;script.type = "text/javascript";this._didCreateEngineInitializerResolve = resolve;script.addEventListener("error", reject);document.body.appendChild(script);}).catch(()=>{// console.error("main.dart.js download fail,refresh and try again");// retry againif (++this._retryCount > 3) {const element = document.createElement("a");element.href = "javascript:location.reload()";element.style.textAlign = "center";element.style.margin = "50px auto";element.style.display = "block";element.style.color = "#f89800";element.innerText = "加载失败,点击重新请求页面";document.body.appendChild(a);} else {this._loadEntrypoint(entrypointUrl);}});

通过分片加载后,同时开启6个下载任务,最高耗时634ms,加载进一步提升。

资源文件hash化

浏览器会对同名文件缓存,为避免功能更新不及时,我们需要对资源文件进行hash化。

首先,我们需要确定哪些资源文件需要进行hash化?

通过对打包产物分析,会频繁变动的资源主要是图片、字体和js文件,因此需要对这些资源进行hash处理,实现步骤如下:

  1. 遍历图片、字体和js等文件
  2. 计算每个文件的hash值
  3. 为新文件命名为[name].[hash].[extension]

    资源hash完成后,如何加载新文件?针对图片、字体和js我们分别做了以下优化
  • js文件主要分为两类,main.dart.js分片后的文件是由XHR直接下载拼接得到的,deferred延迟加载拆分的文件是通过 window.dartDeferredLibraryLoader自定义方法 直接组成script标签插入html中,在对文件hash处理时记录下新旧文件名的映射关系,在获取js文件时就可以通过旧文件名获取到新文件名进行加载。

  • 图片和字体文件,通过对main.dart.js源码的分析,我们发现程序在启动时会先去读取AssetManifest.jsonFontManifest.json清单文件,根据清单文件里面的资源映射关系去加载对应的图片和字体,因此我们在打包后去修改资源映射关系,将里面的文件名换成hash后的新文件名就行了。

    资源hash代码,仅供参考:

/// md5
String md5(File file) {final Uint8List bytes = file.readAsBytesSync();// 截取8位即可final md5Hash = crypto.md5.convert(bytes).toString().substring(0, 8);// 文件名使用hash值final basename = path.basenameWithoutExtension(file.path);final extension = path.extension(file.path);return '$basename.$md5Hash$extension';
}/// 替换
String replace(Match match,File file,String key,Map<String, String> hashFiles,
) {// 文件名使用hash值final String filename = md5(file);final dirname = path.dirname(key);final String newKey = path.join(dirname, filename);// hash文件路径final String newPath = path.join(path.dirname(file.path), filename);hashFiles[file.path] = newPath;return '${match[1]}$newKey${match[3]}';
}// 读取资源清单文件
final File assetManifest = File('$_webOutput/assets/AssetManifest.json');
String assetManifestContent = assetManifest.readAsStringSync();
// 读取字体清单文件
final File fontManifest = File('$_webOutput/assets/FontManifest.json');
String fontManifestContent = fontManifest.readAsStringSync();// 需要hash的文件
final Map<String, String> hashFiles = <String, String>{};// 遍历构建产物assets目录,对资源文件md5后获取哈希值,并修改资源、字体清单文件
final Directory assetsDir = Directory(path.join(_webOutput, 'assets'));
assetsDir.listSync(recursive: true)
.whereType<File>() // 文件类型.where((File file) => !path.basename(file.path).startsWith('.'))
.forEach((File file) {final String key = path.relative(file.path, from: assetsDir.path);
// 替换资源清单文件
assetManifestContent = assetManifestContent.replaceAllMapped(
RegExp('(.*)($key)(.*)'),
(Match match) => replace(match, file, key, hashFiles),
);
// 替换字体清单文件
fontManifestContent = fontManifestContent.replaceAllMapped(
RegExp('(.*)($key)(.*)'),
(Match match) => replace(match, file, key, hashFiles),
);
});// 写入修改后的资源、字体清单文件
assetManifest.writeAsStringSync(assetManifestContent);
fontManifest.writeAsStringSync(fontManifestContent);// 将AssetManifest.json文件进行md5
String assetManifestFileName = 'AssetManifest.json';
final File assetManifestFile =
File(path.join(_webOutput, 'assets', assetManifestFileName));
assetManifestFileName = _md5File(assetManifestFile);
hashFiles[assetManifestFile.path] =
path.join(path.dirname(assetManifestFile.path), assetManifestFileName);// 将FontManifest.json进行md5
String fontManifestFileName = 'FontManifest.json';
final File fontManifestFile =
File(path.join(_webOutput, 'assets', fontManifestFileName));
fontManifestFileName = _md5File(fontManifestFile);
hashFiles[fontManifestFile.path] =
path.join(path.dirname(fontManifestFile.path), fontManifestFileName);// 遍历构建产物根目录
Directory(_webOutput)
.listSync().whereType<File>() // 文件类型.where(
(File file) => RegExp(r'main\.dart(.*)\.js$').hasMatch(file.path))
.forEach((File file) {// 修正源码引用AssetManifest.json和FontManifest.json文件
String contents = file.readAsStringSync().replaceAll(RegExp(r'AssetManifest.json'), assetManifestFileName).replaceAll(RegExp(r'FontManifest.json'), fontManifestFileName);
file.writeAsStringSync(contents);// 替换资js文件
final String filename = _md5File(file);
hashFiles[file.path] = path.join(path.dirname(file.path), filename);
_jsManifest[path.basename(file.path)] = filename;
});// 重命名文件
hashFiles.forEach((String key, String value) {File(key).renameSync(value);
});// 收集需要上传的文件
_toUploadFiles = hashFiles.values.toList();

测试结果:现在图片、字体和js都已经是加载hash后的资源,依然正常运行,证明此方案是可行的。

资源文件cdn化

cdn具有加速功能,为了提高网页加载速度,我们需要对资源文件进行cdn化。在实践中,发现Flutter仅支持相对路径的资源加载方式,而且对于图片和Javascript资源加载逻辑也不相同,为此我们需要分别进行优化。

图片处理

通过对main.dart.js源码的分析,我们发现在加载图片资源时,会先在index.html中查找<meta name="assetBase" content="任意值">的meta标签,获取meta标签的content值作为baseUrlasset(图片名称)进行拼接,最后根据拼接好的URL来加载资源。但是我们在index.html中并没有找到这种meta标签,于是就会根据相对路径进行图片加载。对此,我们在打包时向index.html注入 meta 标签并把 content 设置为CDN路径,就样就实现了图片资源cdn化。

JS处理

通过对main.dart.js源码的分析,我们发现在加载xxx.part.js文件时会先判断window.dartDeferredLibraryLoader是否存在,如果存在的话则使用自定义的dartDeferredLibraryLoader方法加载,否则使用默认的script标签加载。对此,我们在打包时向index.html注入dartDeferredLibraryLoader方法的实现,将传过来的uriAsString参数修改成CDN的地址,这样就实现了JS资源的cdn化。

实现代码,仅供参考:

import 'package:html/dom.dart';
import 'package:html/parser.dart' show parse;
import 'package:path/path.dart' as path;/// 读取index.html
final File file = File('$_webOutput/index.html');
String contents = file.readAsStringSync();/// flutter.js哈希化并修改index.html的 flutter.js script
final File flutterJsFile = File('$_webOutput/flutter.js');
final String filename = _md5File(flutterJsFile);
flutterJsFile.renameSync(path.join(path.dirname(flutterJsFile.path), filename));
contents = contents.replaceAll(
RegExp(r'<script src="flutter.js" defer></script>'),
'<script src="$filename" defer></script>',
);/// 解析 index.html 为 Document
final Document document = parse(contents);/// 注入meta标签
final List<Element> metas = document.getElementsByTagName('meta');
final Element? headElement = document.head;
if (headElement != null) {final Element meta = Element.tag('meta');meta.attributes['name'] = 'assetBase';meta.attributes['content'] = 'xxx';if (metas.isNotEmpty) {final Element lastMeta = metas.last;lastMeta.append(Text('\n'));lastMeta.append(Comment('content值必须以 / 结尾'));lastMeta.append(Text('\n'));lastMeta.append(meta);} else {headElement.append(Comment('content值必须以 / 结尾'));headElement.append(Text('\n'));headElement.append(meta);headElement.append(Text('\n'));}
}/// 注入script
String dartDeferredLibraryLoader = r'''
// auto-generate, dont edit!!!!!!
var assetBase = null;
var jsManifest = null;
function dartDeferredLibraryLoader(uri, successCallback, errorCallback, loadId) {console.info('===>', uri, successCallback, errorCallback, loadId);let src;try {const url = new URL(uri);src = `${assetBase}${jsManifest[url.pathname.substring(1)]}`;} catch (e) {src = `${assetBase}${jsManifest[uri.substring(1)]}`;}script = document.createElement("script");script.type = "text/javascript";script.src = src;script.addEventListener("load", successCallback, false);script.addEventListener("error", errorCallback, false);document.body.appendChild(script);
}
'''.replaceAll(RegExp('var assetBase = null;'), 'var assetBase = xxx;').replaceAll(RegExp('var jsManifest = null;'),'var jsManifest = ${jsonEncode(jsManifest)};',// 'var jsManifest = {"main.dart_0.js":"main.dart_0.7a183f1b.js", "main.dart.js_1.part.js":"main.dart.js_1.part.0445cc90.js"};',);
final List<Element> scripts = document.getElementsByTagName('script');
// 是否注入js
bool isInjected = false;
for (int i = 0; i < scripts.length; i++) {final Element element = scripts[i];if (element.text.contains(RegExp(r'var serviceWorkerVersion'))) {element.text = '${element.text}\n$dartDeferredLibraryLoader';isInjected = true;break;}
}
if (!isInjected) {final Element? headElement = document.head;if (headElement != null) {final Element script = Element.tag('script');script.text = '\n$dartDeferredLibraryLoader';if (scripts.length > 1) {final Element firstScript = scripts.first;headElement.insertBefore(script, firstScript);headElement.insertBefore(Text('\n'), firstScript);} else {headElement.append(script);headElement.append(Text('\n'));}}
}// 写入文件
file.writeAsStringSync(document.outerHtml);

关于cdn上传,目前选择的是上传至腾讯的对象存储(cos),可以使用 tencent_cos_plus 插件。
这里为了方便测试,在本地使用nginx搭建了个文件服务,再将build/web文件下的assets和js文件上传到build/cdn下,用于模拟cdn服务。

nginx配置,仅供参考:

server {listen       9091;server_name  localhost;root   /build/cdn;#   指定允许跨域的方法,*代表所有add_header Access-Control-Allow-Methods *;#   预检命令的缓存,如果不缓存每次会发送两次请求add_header Access-Control-Max-Age 3600;#   不带cookie请求,并设置为falseadd_header Access-Control-Allow-Credentials false;#   表示允许这个域跨域调用(客户端发送请求的域名和端口) #   $http_origin动态获取请求客户端请求的域   不用*的原因是带cookie的请求不支持*号add_header Access-Control-Allow-Origin $http_origin;#   表示请求头的字段 动态获取add_header Access-Control-Allow-Headers $http_access_control_request_headers;#缓存配置location ~ .*\.(jpg|png|ico)(.*){expires 30d;}#缓存配置location ~ .*\.(js|css)(.*){expires 7d;}location / {autoindex on;             #显示索引autoindex_exact_size off; #显示大小autoindex_localtime on;   #显示时间charset utf-8;            #避免中文乱码}#开启gzipgzip  on;  #低于1kb的资源不压缩 gzip_min_length 1k;# 设置压缩所需要的缓冲区大小gzip_buffers 16 64k;#压缩级别1-9,越大压缩率越高,同时消耗cpu资源也越多,建议设置在5左右。 gzip_comp_level 5; #需要压缩哪些响应类型的资源,多个空格隔开。不建议压缩图片.gzip_types text/plain text/css text/javascript text/xml application/json application/x-javascript application/javascript application/xml application/xml+rss image/jpeg image/gif image/png image/jpg;#配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)gzip_disable "MSIE [1-6]\.";  #是否添加“Vary: Accept-Encoding”响应头gzip_vary on;
}

测试结果:虽然网页请求url端口与图片请求url和js请求url的端口不一致,依然正常运行,证明此方案是可行的。

成果

  • 一起漫部 App
  • 一起漫部 Web

参考链接

  • FlutterWeb性能优化探索与实践

总结

综上所述,就是《一起漫部》对Flutter Web的性能优化探索与实践,目前我们所做的性能优化是有限的,未来我们会继续在Flutter Web上做更多的探索与实践。如果您对 Flutter Web 也感兴趣,欢迎在评论区留言或者给出建议,非常感谢。

Flutter Web 在《一起漫部》的性能优化探索与实践相关推荐

  1. FlutterWeb性能优化探索与实践

    点击"开发者技术前线",选择"星标" 让一部分开发者看到未来 来自:美团技术团队 美团外卖商家端基于 FlutterWeb 的技术探索已久,目前在多个业务中落地 ...

  2. 关于性能优化的一些实践

    关于性能优化的一些实践 2 背景 在海量并发业务的场景下,比如电商抢购.微信红包这样的场景下,我们经常会遇到各种各样的性能问题,在应对这些问题的时候,应该有怎样的方法论去指导我们解决问题,基于这几年的 ...

  3. Web前端教程分享:页面性能优化办法有哪些?

    引子 互联网有一项著名的8秒原则.用户在访问Web网页时,如果时间超过8秒就会感到不耐烦,如果加载需要太长时间,他们就会放弃访问.大部分用户希望网页能在2秒之内就完成加载.事实上,加载时间每多1秒,你 ...

  4. Python全栈(五)Web安全攻防之4.sqlmap性能优化和注入技术参数

    文章目录 一.Sqlmap性能优化 1.sqlmap设置持久HTTP连接 2.sqlmap设置不接收HTTP Body 3.sqlmap设置多线程 4.一键优化 二.sqlmap自定义检测参数 1.s ...

  5. Java 性能优化实战工具实践:如何获取代码性能数据?

    首先解答一下上一课时的问题.磁盘的速度这么慢,为什么 Kafka 操作磁盘,吞吐量还能那么高? 这是因为,磁盘之所以慢,主要就是慢在寻道的操作上面.Kafka 官方测试表明,这个寻道时间长达 10ms ...

  6. 京东微信购物性能优化,最佳实践总结!

    京东微信购物首页(以下简称微信首页)曾经作为微信购物一级入口(目前替换为京喜小程序)一直对性能有着极高的要求,本文将介绍微信首页的一些优化经验. 一般来说产品是按以下方式进行迭代的,我认为循环的起点应 ...

  7. flutter 图片压缩_Flutter包大小治理上的探索与实践

    Flutter作为一种全新的响应式.跨平台.高性能的移动开发框架,在性能.稳定性和多端体验一致上都有着较好的表现,自开源以来,已经受到越来越多开发者的喜爱. 但是,Flutter的引入往往带来包体积的 ...

  8. 让数据中台飞起来—— Quick BI性能优化解决方案及实践

    Quick BI"数据门户"在企业数据中台建设中的重要性 企业在数据中台初步建设完成以后,怎样让客户直观感受到数据中台的价值?企业决策者.各部门管理人员.业务运营人员如何通过统一的 ...

  9. 让数据中台“飞“起来— Quick BI性能优化解决方案及实践

    1 引言 Quick BI"数据门户"在企业数据中台建设中的重要性 企业在数据中台初步建设完成以后,怎样让客户直观感受到数据中台的价值?企业决策者.各部门管理人员.业务运营人员如何 ...

最新文章

  1. WINCE6.0+S3C2443的启动过程---eboot2
  2. go interface转int_32. 一篇文章理解 Go 里的函数
  3. INVEST优秀用户故事的特点
  4. python 中的 and / or
  5. Java GUI 开发专题
  6. 测试眉形的有哪个软件_这五款自动化软件测试工具,你最喜欢用哪个?
  7. 归纳推理测试没做完_朋友买了1斤紫菜,2年还没吃完,我教他这样做,2个月就吃完了...
  8. Jenkins自定义主题
  9. python dicom图像分割_python读取DICOM头文件的实例
  10. OSGI(面向Java的动态模型系统)和它的实现Equinox
  11. pyquery获取不到网页完整源代码_爬虫神器之PyQuery实用教程(二),50行代码爬取穷游网...
  12. Python里的++和--不是运算符
  13. 一步一步写算法(之克鲁斯卡尔算法 下)
  14. Laravel中自定义guard,自定义Auth的attempt方法
  15. Bailian4034 选择客栈【序列处理】
  16. 无监督学习------分类学习-----朴素贝叶斯(navie bayes)
  17. 百家号如何提高推荐量和阅读量,百家号提高推荐量和阅读量的方法
  18. 电音(5)Bass类电音
  19. 大学计算机基础实践教程实验八,《大学计算机基础》实验报告八.doc
  20. iOS开发之自定义的framework添加第三方framework,lipo和ar命令看.o文件

热门文章

  1. 慧拓 规划算法实习生 20220818 笔面试
  2. logistic回归和softmax回归
  3. python定义一个字符串变量_python怎么创建字符串变量
  4. [附源码]计算机毕业设计springbootQ宝商城
  5. 开源WebRTC库放大器模式在采集桌面图像时遇到的DPI缩放与内存泄漏问题排查
  6. 复变函数与积分变换 期末复习 思维导图
  7. 日常生活中的物理知识
  8. 车脉科技:业内首创“车企体验式营销“
  9. 一个十年操盘手的交易之路!
  10. 嵌入式~PCB专辑8