一起漫部 是基于区块链技术创造的新型数字生活。
不久前,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 3.0.5 版本上进行 App to Web 的工作。
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 渲染器模式时,flutter 将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染元素
当使用 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/
, 很明显的感觉到了首屏加载慢,用户白屏的体验,即首屏白屏问题。那么为什么会出现白屏问题?
首先,我们需要了解浏览器渲染过程:
script
暂停渲染,优先解析执行javascript,再继续渲染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
两个文件,针对这两个文件我们又进行了以下优化。
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 下