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

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

目录

前言

不久前,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/, 很明显的感觉到了首屏加载慢,用户白屏的体验,即首屏白屏问题。那么为什么会出现白屏问题?

首先,我们需要了解浏览器渲染过程:

  1. 解析 HTML,构建 DOM 树
  2. 解析 CSS,构建 CSSOM 树
  3. 合并 DOM 树和 CSSOM 树,构建 Render 渲染树
  4. 遍历 Render 渲染树计算节点位置大小进行布局
  5. 根据节点位置大小信息,进行绘制
  6. 遇到script暂停渲染,优先解析执行javascript,再继续渲染
  7. 最后绘制出所有节点,展现页面

通过 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.jsMaterialIcons-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。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值