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

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

目录

  • 前言
  • 开发环境
  • 渲染模式
  • 首屏白屏
  • 优化方案
    • 启屏页优化
    • 包体积优化
      • 去除无用的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 Web 在《一起漫部》的性能优化探索与实践_第1张图片

Flutter 环境Flutter Web 在《一起漫部》的性能优化探索与实践_第2张图片

如图所示,我们团队是在 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 工具分析:
Flutter Web 在《一起漫部》的性能优化探索与实践_第3张图片

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

通过 Network 工具分析:Flutter Web 在《一起漫部》的性能优化探索与实践_第4张图片

  • 首屏页面总共发起 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 Web 在《一起漫部》的性能优化探索与实践_第5张图片

裁剪字体文件

Flutter 默认会打包MaterialIcons-Regular.otf 字体库,里面包含了一些预置的 Material 设计风格 icon,所以体积比较大。但是每次都加载一个1.6M的字体文件是不合理的,我们发现flutter提供--tree-shake-icons命令去裁剪掉没有使用的图标,在尝试flutter build web --web-renderer html --tree-shake-icons打包Web应用时却出现异常。
Flutter Web 在《一起漫部》的性能优化探索与实践_第6张图片
通过分析我们发现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 下

你可能感兴趣的:(flutter,前端,性能优化)