一起漫部 是基于区块链技术创造的新型数字生活。
目录
前言
不久前,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,再继续渲染 - 最后绘制出所有节点,展现页面
- 浏览器等待 HTML 文档返回,此时处于白屏状态,理论白屏时间
- 解析完HTML文档后开始渲染首屏,出现灰屏(测试背景)状态,实际白屏时间-理论白屏时间
- 加载JS、解析JS等过程耗时长,导致界面长时间处于灰屏(测试背景)状态
- JS解析完成后,界面渲染出大概的框架结构
- 请求API获取到数据后开始显示渲染出首屏页面
- 首屏页面总共发起 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,最后在应用程序第一帧渲染完成后移除即可。
启屏页实现代码,仅供参考:
包体积优化
我们先了解下 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
的使用进行了封装处理,仅供参考:
/// loadLibrary
typedef AppLibraryLoader = Future 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> _moduleLoaders =
>{};
static final Set _loadedModules = {};
/// 预加载
static Future preload(AppLibraryLoader loader) {
if (!_moduleLoaders.containsKey(loader)) {
_moduleLoaders[loader] = loader().then((_) {
_loadedModules.add(loader);
});
}
return _moduleLoaders[loader]!;
}
@override
State createState() => _AppDeferredWidgetState();
}
class _AppDeferredWidgetState extends State {
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;
}
}
/// 延迟加载Loading
class 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? 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 writeSingleFile({
required File file,
required String filename,
required int startIndex,
required endIndex,
}) {
final Completer 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> inputStream = file.openRead(startIndex, endIndex);
inputStream.listen(
(List 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> futures = List>.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 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文件主要分为两类,
main.dart.js
分片后的文件是由XHR直接下载拼接得到的,deferred
延迟加载拆分的文件是通过window.dartDeferredLibraryLoader
自定义方法 直接组成script标签插入html中,在对文件hash处理时记录下新旧文件名的映射关系,在获取js文件时就可以通过旧文件名获取到新文件名进行加载。 图片和字体文件,通过对
main.dart.js
源码的分析,我们发现程序在启动时会先去读取AssetManifest.json
和FontManifest.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
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 hashFiles = {}; assetsDir .listSync(recursive: true) .whereType () // 文件类型 .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标签,获取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 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 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);
了方便测试,在本地使用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的端口不一致,依然正常运行,证明此方案是可行的。
成果
参考链接
总结
综上所述,就是《一起漫部》对Flutter Web的性能优化探索与实践,目前我们所做的性能优化是有限的,未来我们会继续在Flutter Web上做更多的探索与实践。如果您对 Flutter Web 也感兴趣,欢迎在评论区留言或者给出建议,非常感谢。