Flutter Web 之「第一次首屏加载」体验优化

我正在参加跨端技术专题征文活动,详情查看:juejin.cn/post/710123…


本文将从 Flutter Web 页面加载的流程和最终部署后访问的资源入手,解决第一次首屏加载过久的问题,同时利用 Flutter 3.0 的新特性提升加载体验。

缘起

前几天偶然看到一篇 Flutter Web 新版本性能提升的文章,作者经过基准测试后得出的结论是,升级到 Flutter 3.0 之后,web 端的性能在三大桌面平台上都有着约10%~30% 不等的提升。

页面的流畅程度就像游戏中的延迟,能感受到时往往是已经出现了卡顿,所以对于帧率的下限来说,即使是少许的提高,整个使用体验也会好很多

看完后立刻翻出一个之前的 web 端项目,根据官网文档一顿升级。调试毫无问题。编译部署完点开连接,却看到足足五秒钟的白屏,页面才吭哧吭哧的渲染出来。What?这实际体验怎么不升反降了呢……同样是 html 渲染,比 2.10 版本感觉还慢了不少。

立马开始强制刷新,看看这个「首屏加载过慢」的问题到底出在哪里。

真 · 强制刷新: 打开一个无痕窗口访问目标页面,然后长按刷新按钮,选择 清空缓存并硬性重新加载(PS. 后面均使用这种方式强制取消所有缓存,来模拟设备首次加载的场景,Ctrl + F5 依然会有 disk cache)。

real-hard-refresh.jpg

神奇的事出现了,就在我打开开发者工具的 Network 选项卡准备一探究竟时,这个问题消失了。

不论我如何强制刷新,页面的加载速度都回到了正常范围。

失踪的 bug 之谜

难道说第一次访问的网络那么差吗?还是我的强制刷新并不彻底?

一番搜索和尝试无果,我只能使出终极杀器:换电脑。

在彻底的新环境下,这个超长的首屏加载终于又出现了。虽然也只复现了一次,但是已经被我记录了下来。如下图:

标注问题.png

抛去前面的网络延迟,经过对比,和后续正常状态的请求资源有区别且耗时较长的有两个:

  • 红色框:flutter_service_worker.js 文件,执行耗时
  • 蓝色框:NOTICES 文件,大小 850k,下载耗时

后续访问是不会请求这两个文件的,直接砍掉了两秒多的加载时间。

问题终于找到了,不过还得多说两句。

最下面的绿色框,MaterialIcons-Regular.otf 是图标的打包文件,默认的编译过程没有进行 tree-shake-icons,包含了所有的 MD 图标,因此比较大。这个耗时是后续请求中也包括的,解决方法可以参考 @恋猫de小郭 大佬的 这篇文章

这里其实还有个优化没提,就是 main.dart.js 文件的拆包。如果不使用 deferred as 对页面或较重的组件进行拆分懒加载,最后会只有一个大文件包含所有的dart转译代码,同样影响速度。上图中没有截进去是因为,这里标出的几个点都是页面加载以前,也就是还没有进行具体内容的渲染,而对这个文件的拆包优化是包括了下一个阶段的。

问题的 定位 & 解决

先总结一下,造成首屏加载过久的问题:

问题表格-更新.png

有了这张图就清楚多了,现有的文章基本都在说后续的问题,本文则重点关注圈起来的这两个第一次访问时,在渲染内容前出现的问题。

flutter_service_worker.js 执行耗时

这个文件的作用是提供 PWA 功能支持,管理 service worker 相关的缓存和下载更新。具体的逻辑比较复杂。不过在之前版本的宿主页面web/index.html中,内嵌的初始化脚本也在做同样的事,它的整体逻辑相对简单:如果浏览器支持且尚未安装,则安装 service worker 来提供 PWA 能力,然后继续加载;否则直接加载 main.dart.js,开始渲染整个页面。

相关代码片段如下:

<!-- ... -->
<!-- This script installs service_worker.js to provide PWA functionality to application -->
  <script>
    var serviceWorkerVersion = null;
    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);
    }

    if ('serviceWorker' in navigator) {
      // Service workers are supported. Use them.
      <!-- ... -->
      loadMainDartJs();
      <!-- ... -->
    } else {
      // Service workers not supported. Just drop the <script> tag.
      loadMainDartJs();
    }
  </script>
  <!-- ... -->
复制代码

这就解释了为啥只有第一次访问时会出现。如果不需要 PWA,直接去掉对它的支持就能解决这个问题。

NOTICES 下载耗时

这个文件是个单纯的开源协议集合,不知道为啥要放在编译产物里,可能是协议方面的要求。它的出现也在 flutter_service_worker.js 中,是 service worker 初始化所必须的资源文件。

相关代码片段如下:

// ...
// The application shell files that are downloaded before a service worker can
// start.
const CORE = [
  "main.dart.js",
"index.html",
"assets/NOTICES",
"assets/AssetManifest.json",
"assets/FontManifest.json"];
// During install, the TEMP cache is populated with the application shell files.
self.addEventListener("install", (event) => {
  self.skipWaiting();
  return event.waitUntil(
    caches.open(TEMP).then((cache) => {
      return cache.addAll(
        CORE.map((value) => new Request(value, {'cache': 'reload'})));
    })
  );
});
// ...
复制代码

同理,去掉对于 PWA 的支持即可解决这个加载耗时。

总结与体验优化

原来第一次访问时多出来的这两秒加载时间,都是为了 PWA。看得出真是谷歌的心头好呀。

虽然在国内很少用这个特性,但是直接放弃支持,也并不是很理想的解决方式。

当然对于大型团队和大牛来说,直接修改 Flutter 源码,定制一个更合适的加载和更新策略,或许是最本质的解决方案,可以在整个大版本上一劳永逸。但是这样做的成本有点过于高了,一般情况下还是难以接受。

那么有没有其他的办法呢?

把这个耗时的加载过程,放在一个引导页当中,尽可能偷偷的提前加载,然后顺滑的过渡,以减少用户的等待感,这样应该是比较优雅的降级处理方法。

如果是在之前的版本,就需要比较麻烦的修改宿主 HTML,但好在 Flutter 3.0 把整个初始化过程抽象并拆分成了三部分:

  1. 加载入口(下载 main.dart.js 文件,配置 PWA);
  2. 初始化 Flutter 引擎;
  3. 渲染页面(执行 runApp() 方法)。

每个部分都是链式调用,中间可以插入个性化的行为,在需要的时候继续执行。

相关代码片段如下:

<!-- ... -->
<body>
  <script>
    window.addEventListener('load', function (ev) {
      // Download main.dart.js
      _flutter.loader.loadEntrypoint({
        serviceWorker: {
          serviceWorkerVersion: serviceWorkerVersion,
        }
      }).then(function (engineInitializer) {
        return engineInitializer.initializeEngine();
      }).then(function (appRunner) {
        return appRunner.runApp();
      });
    });
  </script>
</body>
<!-- ... -->
复制代码

这样向页面里添加一些引导部分,控制整个加载流程,就变得十分容易了。

篇(bu)幅(xiang)所(xie)限(le),这里就不贴具体的代码了。放个简单的场景思路抛砖引玉一下:首先加载几个简介和说明的div,多写点字,在用户阅读时加载入口,当用户浏览完毕,点击底部的进入按钮时,再执行初始化和渲染页面。这样整个使用过程的体验就很好了。


题外话:在掘金读了不少大牛的文章,今天终于狠心自己写了篇。虽然已经做好了费时费力的心理准备,但还是没想到改了这么久。感谢阅读,希望我的分享能让你有所收获。祝你拥有美好的一天!

初次发文,多有纰漏,欢迎大家的讨论和debug~

猜你喜欢

转载自juejin.im/post/7106601288854405128