一起漫部 是基于区块链技术创造的新型数字生活。
目录
前言
不久前,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。