一起漫部 是基于区块链技术发明的新型数字生存。
目录
- 前言
- 开发环境
- 渲染模式
- 首屏白屏
优化计划
- 启屏页优化
包体积优化
- 去除无用的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/
, 很显著的感觉到了首屏加载慢,用户白屏的体验,即首屏白屏问题。那么为什么会呈现白屏问题?
首先,咱们须要理解浏览器渲染过程:
- 解析 HTML,构建 DOM 树
- 解析 CSS,构建 CSSOM 树
- 合并 DOM 树和 CSSOM 树,构建 Render 渲染树
- 遍历 Render 渲染树计算节点地位大小进行布局
- 依据节点地位大小信息,进行绘制
- 遇到
script
暂停渲染,优先解析执行javascript,再持续渲染 - 最初绘制出所有节点,展示页面
通过 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.js
和MaterialIcons-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
的应用进行了封装解决,仅供参考:
/// loadLibrarytypedef AppLibraryLoader = Future<dynamic> Function();/// deferredWidgetBuildertypedef 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]!; } @override State<AppDeferredWidget> createState() => _AppDeferredWidgetState();}class _AppDeferredWidgetState extends State<AppDeferredWidget> { Widget? _loadedChild; AppDeferredWidgetBuilder? _loadedBuilder; @override void 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(); }); } @override Widget build(BuildContext context) { if (_loadedBuilder != widget.builder && _loadedChild != null) { _loadedBuilder = widget.builder; _loadedChild = _loadedBuilder?.call(); } return _loadedChild ?? widget.placeholder; }}/// 提早加载Loadingclass AppDeferredLoading extends StatelessWidget { const AppDeferredLoading({Key? key}) : super(key: key); @override Widget 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压缩
#开启gzip gzip 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化和三方面思考,对此咱们又做了以下优化。具体的编码实现请参考 Web Optimize
大文件分片下载
因为main.dart.js
体积大,独自下载大文件速度慢,势必影响首屏的加载性能。对此,咱们提出分片加载的计划,具体实现如下:
- 通过脚本将
main.dart.js
切割成 6 个独自的纯文本文件 - 通过XHR的形式并行下载 6 个纯文本文件
- 期待下载实现后,将 6 个纯文本文件依照程序拼接,失去残缺的
main.dart.js
文件 - 创立
script
标签,将残缺的main.dart.js
文件内容赋值给text
属性 - 最初将
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.jsfile.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 again if (++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解决,实现步骤如下:
- 遍历图片、字体和js等文件
- 计算每个文件的hash值
- 为新文件命名为[name].[hash].[extension]
资源hash实现后,如何加载新文件?针对图片、字体和js咱们别离做了以下优化
- js文件次要分为两类,
main.dart.js
分片后的文件是由XHR间接下载拼接失去的,deferred
提早加载拆分的文件是通过window.dartDeferredLibraryLoader
自定义办法 间接组成script标签插入html中,在对文件hash解决时记录下新旧文件名的映射关系,在获取js文件时就能够通过旧文件名获取到新文件名进行加载。 图片和字体文件,通过对
main.dart.js
源码的剖析,咱们发现程序在启动时会先去读取AssetManifest.json
和FontManifest.json
清单文件,依据清单文件外面的资源映射关系去加载对应的图片和字体,因而咱们在打包后去批改资源映射关系,将外面的文件名换成hash后的新文件名就行了。
资源hash代码,仅供参考:/// md5String 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('$webArtifactsOutputDir/assets/AssetManifest.json');String assetManifestContent = assetManifest.readAsStringSync();// 读取字体清单文件final File fontManifest = File('$webArtifactsOutputDir/assets/FontManifest.json');String fontManifestContent = fontManifest.readAsStringSync();// 遍历assets目录final Directory assetsDir = Directory(webArtifactsOutputDir);Map<String, String> hashFiles = <String, String>{};assetsDir .listSync(recursive: true) .whereType<File>() // 文件类型 .where((File file) => !path.basename(file.path).startsWith('.')) .forEach((File file) {if (RegExp(r'main.dart(.*)\.js$').hasMatch(file.path)) { // 替换资js文件 final String filename = md5(file); hashFiles[file.path] = path.join(path.dirname(file.path), filename); jsManifest[path.basename(file.path)] = filename;}if (file.path.contains('$webArtifactsOutputDir/assets')) { final String key = path.relative(file.path, from: '$webArtifactsOutputDir/assets'); // 替换资源清单文件 assetManifestContent = assetManifestContent.replaceAllMapped( RegExp('(.*)($key)(.*)'), (Match match) => replace(match, file, key, hashFiles), ); // 替换字体清单文件 fontManifestContent = fontManifestContent.replaceAllMapped( RegExp('(.*)($key)(.*)'), (Match match) => replace(match, file, key, hashFiles), );}});// 重命名文件hashFiles.forEach((String key, String value) {File(key).renameSync(value);});// 写入资源、字体清单文件assetManifest.writeAsStringSync(assetManifestContent);fontManifest.writeAsStringSync(fontManifestContent);
测试后果:当初图片、字体和js都曾经是加载hash后的资源,仍然失常运行,证实此计划是可行的。
资源文件cdn化
cdn具备减速性能,为了进步网页加载速度,咱们须要对资源文件进行cdn化。在实践中,发现Flutter仅反对相对路径的资源加载形式,而且对于图片和Javascript资源加载逻辑也不雷同,为此咱们须要别离进行优化。
图片解决
通过对main.dart.js
源码的剖析,咱们发现在加载图片资源时,会先在index.html
中查找<meta name="assetBase" content="任意值">
的meta标签,获取meta标签的content
值作为baseUrl
和asset
(图片名称)进行拼接,最初依据拼接好的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;final File file = File('$webArtifactsOutputDir/index.html');final String contents = file.readAsStringSync();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')); }}/// 注入scriptString 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');// 是否注入jsbool 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);
为了不便测试,在本地应用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申请,并设置为false add_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; #防止中文乱码 } #开启gzip gzip 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 也感兴趣,欢送在评论区留言或者给出倡议,非常感谢。