虽然不甚成熟,但是大部分场景也已够用。经过一通魔改优化后,终于能完成「真·单技术栈全平台」这个理想的最后一块拼图了。

彻底解决访问速度慢的问题,让 Flutter Web 迈向实用。

前言

真是前人种树,后人乘凉呀。感谢网上各位大佬的的付出,本文综合了官方仓库中的相关 Issues 和 各类文章,以及自己的踩坑经验,应该算是当下(2022-11)比较完整的实践总结了。

Flutter Web 的基本原理是把 Dart 代码转译为 js,然后插入宿主 HTML 中运行,在根节点渲染整个页面。形式上类似于 Vue 的编译产物,最终都是个 SPA。

其他体验都还好,最大的问题就是:首次加载太慢,有时甚至慢到无法接受

缺陷的由来

因为实现方式,编译产物中会有几个大文件:

文件名 作用
main.dart.js 核心文件,内含转译所需的各个 SDK,以及全部业务层代码。
Icon字体文件 提供所有内置图标。
canvaskit.wasm (如果选择了 Canvaskit 渲染模式) 移动端使用的渲染引擎 Skia 的 WebAssembly 文件,提供较高的一致性和渲染性能。
各语言的字体文件 (如果选择了 Canvaskit 渲染模式) Skia 需要自带字体文件。

一般渲染模式会选择 HTML ,兼容性比较好而且编译产物要小很多。缺点是性能比较一般,复杂的动画,图表展示和列表滚动上掉帧很厉害,还可能出现绘制模糊的情况,所以很可能必须选择 Canvaskit ,此处也列入其带来的额外产物以备解决。

除了这些大文件下载缓慢,首次加载的过程也会造成白屏,影响体验。

此外在测试中发现,国内部分机型对于 PWA 的支持很差,第一次加载相关资源和配置异常缓慢,而 Flutter Web 的编译是默认开启的,因此也需要关掉。

解决方案

美团的 这篇文章 FlutterWeb性能优化探索与实践 里提到过针对编译过程进行优化,裁剪 SDK 中用不着的部分等(写的很好,研究的也很深入👍),确实是很好的实践,但是对于个人来说,暂时没有能力去维护过于底层的修改,因此这里只考虑应用层面方便进行的优化。

可优化的点 解决方式
编译产物体积较大,加载慢 减小编译产物体积,优化加载速度
渲染完成之前的白屏等待 添加过渡效果
国内PWA影响首次加载速度 去除PWA支持

具体细节如下:

去除PWA支持

编译时添加参数:

 --pwa-strategy none

然后即可在编译产物中删去 flutter_service_worker.js, flutter.js 和 manifest.json 文件。这些都是跟 PWA 相关的配置文件,不会再被用到。

在宿主 HTML 中将加载部分改为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
   <script>
    var scriptLoaded = false;
    function loadMainDartJs() {
      if (scriptLoaded) {
        return;
      }
      scriptLoaded = true;
      var scriptTag = document.createElement('script');
      scriptTag.src = 'main.dart.js';
      scriptTag.type = 'application/javascript';
      document.body.append(scriptTag);
    }
    loadMainDartJs();
  </script>

即直接挂载并渲染。

添加过渡效果

在下载各类资源时,网页默认是白屏状态,添加一个 Loading 动画或欢迎页面都可以提升体验。

这里用 CSS 做一个简单的过渡效果:

在 HTML 中添加的代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
  <head>
    <style>
      body{
        background:rgb(255, 241, 219);
      }

      .loading {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        margin: 0;
        position: absolute;
        top: 50%;
        left: 50%;
        -ms-transform: translate(-50%, -50%);
        transform: translate(-50%, -50%);
      }

      .loader {
        display: block;
        position: relative;
        width: 6px;
        height: 10px;
        animation: rectangle infinite 1s ease-in-out -0.2s;
        background-color: #000;
      }

      .loader:before,
      .loader:after {
        position: absolute;
        width: 6px;
        height: 10px;
        content: "";
        background-color: #000;
      }

      .loader:before {
        left: -14px;
        animation: rectangle infinite 1s ease-in-out -0.4s;
      }

      .loader:after {
        right: -14px;
        animation: rectangle infinite 1s ease-in-out;
      }

      @keyframes rectangle {
        0%,
        80%,
        100% {
          height: 20px;
          box-shadow: 0 0 #000;
        }
        40% {
          height: 30px;
          box-shadow: 0 -20px #000;
        }
      }
    </style>
  </head>

  <body>
    <div class="loading">
      <div class="loader"></div>
    </div>
  </body>    

页面内容加载后会挂载到根节点,取代过度动画。

减小编译产物体积

  1. 指定 HTML 渲染

如果实际测试不影响体验,则优先选择 HTML 渲染模式。

编译时添加参数:

 --web-renderer html

首次编译时可能会生产 canvaskit 目录,移除即可。该参数默认为 auto,即两种模式都包含,在桌面浏览器使用 Canvaskit,手机上使用 HTML。

  1. 裁剪图标字体文件

编译为原生App时会做这个对图标的树摇,但是 Web 编译时没有,因此先编译一下原生再复制过来。

flutter build apk
cp -r ./build/app/intermediates/flutter/release/flutter_assets/fonts ./web/assets
  1. 大文本文件分片

主要是 main.dart.js,至少也有 1.6M 大小,完全下载之后才会开始渲染网页。

编译之后,通过脚本将该文件切分成几个大小合适的文本文件,然后在网页中通过 XHR 并行下载后拼接在一起。

这样就能使编译产物中不再有大文件,显著减少下载资源的时间。

优化加载速度

经过上面的一堆处理,网络好的情况下,首屏访问的体验已经可以了。

但如果必须使用 Canvaskit 渲染模式,而且网页中又有中文,那么又多了两个静态资源:canvaskit.wsam 和中文字体文件,都是好几兆的大文件。默认通过 unpkg 和 gstatic 下载,国内部分地区的速度可想而知了。

因此还需要最后一步,把静态资源本地化,或者 CDN 化。

编译命令添加指定 canvaskit 路径的参数。

 # 这个 URL 可以是本地相对路径,也可以是 CDN 
 --dart-define=FLUTTER_WEB_CANVASKIT_URL=/

中文字体默认使用 Roboto CJK,全局配置字体就会使用本地的资源。如果内容不多,字体还能使用3500字精简版进一步缩小体积。

同时可以把 Flutter 切换到最新的 Master Channel,以开启两者的并发下载,之前是依次下载的。

流程总结

零零散散说了这么多,总结一个相对有代表性的流程,中间需要脚本完成一些批量处理,可以参考 这篇文章 FlutterWeb性能优化探索与实践 中的示例代码,简单改改就能合并到自己的 CI 流程中。

一个面向国内的普通工具型小网站,首次编译部署流程:

  1. 处理网页文件:添加 Loading 动画,精简加载流程。
  2. 处理配置文件:删去用不到的依赖,如 iOS 相关图标资源。
  3. 编译:指定 HTML 渲染模式。
  4. 替换图标文件:编译一次安卓App,替换生成的裁剪后的图标文件。
  5. 清理静态文件:删除用不到的一些编译产物,如 NOTICES 等。
  6. 静态文件 Hash 化:计算各个静态文件的 md5 附在文件名中。
  7. 大文件分片:分割 main.dart.hash.js 文件,在网页文件中加入下载拼接功能。
  8. 部署:将处理后的最终产物部署到静态Web服务,配置 SPA,配置 CDN。

实际效果已经基本和同类网页相同了,首屏加载没有明显的体验差异。

参考