Flutter Web 开发实践及优化

你将了解到

  1. Flutter Web 架构概览及渲染器
  2. Flutter Web 适用场景及注意点
  3. Flutter Web 首次加载慢的优化

架构概览

虽然 Flutter 支持的所有平台的都适用于同一个架构概念,但是在 Web 平台的支持上有一些独特的特征值得说明。

Dart 语言存在之初就已经支持直接编译成 JavaScript,并且针对开发和生产目的对其工具链进行了优化。许多重要的应用已经使用 Dart 编译成的 JavaScript 在生产环境上运行,包括 Google Ads 的广告商工具 。由于
Flutter 框架是 Dart 编写的,将其编译成 JavaScript 相对而言更为简单。

然而,使用 C++ 编写的 Flutter 引擎是为了与底层操作系统进行交互的,而不是 Web 浏览器。因此我们需要另辟蹊径。Flutter 在 Web 平台上以浏览器的标准 API 重新实现了引擎。 目前我们有两种在 Web 上呈现内容的选项:HTML 和 WebGL。在 HTML 模式下,Flutter 使用 HTML、CSS、Canvas 和 SVG 进行渲染。而在 WebGL 模式下,Flutter 使用了一个编译为 WebAssembly 的 Skia 版本,名为 CanvasKit 。HTML 模式提供了最佳的代码大小,CanvasKit 则提供了浏览器图形堆栈渲染的最快途径,并为原生平台的内容 5 提供了更高的图形保真度。

Web 版本的分层架构图如下所示:


与其他运行 Flutter 的平台相比,最明显的区别也许是 Flutter 不再需要提供 Dart 的运行时。取而代之的是 Flutter 框架本身(和你写的代码)一并编译成 JavaScript。另外值得注意的是,Dart 在不同模式下(JIT 和 AOT、平台原生和 Web 编译)的语义几乎没有差异,大部分开发者绝对可以无差异地编写这两种模式下的代码。

两种编译器

Flutter 官方给我们提供了 dart2js 和 dartdevc 两个编译器,本质上都是编译成JavaScript

  • 进行开发时,Web 版本的 Flutter 使用支持增量编译的编译器 dartdevc 进行编译,以支持应用热重启(尽管目前尚未支持热重载)。
  • 相反,当你准备好创建一个生产环境的 Web 应用时,Dart 深度优化的编译器 dart2js 将会用于编译,将 Flutter 核心框架和你的应用打包至缩小的源文件中,可部署在任何服务器上。代码可以在单个文件中提供,也可拆分至多个文件以 延迟加载库 提供。

1、dartdevc 命令

Dart 开发编译器(dartdevc,也称为DDC) 允许您在 Chrome 浏览器中运行和调试您的 Dart Web 应用程序。

备忘: dartdevc 编译器仅用于开发。继续使用dart2js 编译部署。

备忘: 开发编译器 (dartdevc)仅支持 Chrome。

与 dart2js 不同,dartdevc 支持增量编译并发出模块化 JavaScript。像webdev serve这样使用 dartdevc 的工具,你可以编辑你的 Dart 文件,在 Chrome 中刷新,并且几乎可以立即看到你的编辑。这个速度是可能的,因为 dartdevc 只编译更新的模块,而不是你的应用程序依赖的所有包。

第一次使用 dartdevc 编译耗时最长,因为必须编译整个应用程序。之后,只要serve命令继续运行,dartdevc 的刷新时间就会比 dart2js 快得多。

2、dart2js 命令

Dart 生成 JS 编译器

使用dart2js工具将 Dart 代码编译为可部署的 JavaScript。
另一个 Dart-to-JavaScript 编译器dartdevc仅供开发使用。
webdev 构建命令默认使用dart2js。
webdev serve 命令默认使用dartdevc,但您可以使用–release标志切换到 dart2js。

基本用法,下面是一个将 Dart 文件编译为 JavaScript 的示例:

$ dart2js -O2 -o test.js test.dart

3、dartdevc、dart2js总结

  • dartdevc: 它提供渐进式编译和热启动。你可以编辑Dart文件,在Chrome中刷新,并立即查看文件修改后的结果。dartdevc只编译更新的模块,而不是编译应用所依赖的所有软件包。

  • dart2js: 为了部署环境而生成优化的精简的代码。使用dart2js工具将Dart代码编译为可部署的JavaScript

两种 Web 渲染器

可以选择两种不同的渲染器来运行和构建 Web 应用。
下文介绍两种渲染器以及它们的适用场景。

1、使用 HTML 渲染

使用 HTML,CSS,Canvas 和 SVG 元素来渲染,应用的大小相对较小。

2、使用 CanvasKit 渲染

将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染(见下面的WebGL说明)。应用在移动和桌面端保持一致,有更好的性能,以及降低不同浏览器渲染效果不一致的风险。但是应用的大小会增加大约 2MB

3、命令控制

通过 runbuild

flutter run -d chrome --web-renderer html
flutter build web --web-renderer canvaskit

4、选择合适的渲染器

  • 如果您在移动端浏览器平台上更关心应用大小,而桌面端浏览器更关心性能,请选择 auto 选项(默认)。
  • 如果您在移动端和桌面端都更关心应用大小,请选择 html 选项。
  • canvaskit:移动端和桌面端都更关心性能,和跨浏览器的像素级一致性。

浏览器知识

1、WebGL

WebGL(Web图形库)是一个JavaScript API,可在任何兼容的Web浏览器中渲染高性能的交互式3D和2D图形,而无需使用插件。WebGL通过引入一个与OpenGL ES 2.0非常一致的API来做到这一点,该API可以在HTML5 <canvas> 元素中使用。 这种一致性使API可以利用用户设备提供的硬件图形加速。
目前支持 WebGL 的浏览器有:Firefox 4+, Google Chrome 9+, Opera 12+, Safari 5.1+, Internet Explorer 11+和Microsoft Edge build 10240+;然而, WebGL 一些特性也需要用户的硬件设备支持。

WebGL 2 是WebGL的一个主要更新,它基于OpenGL ES 3.0,引入了对大部分的OpenGL ES 3.0功能集的支持,它是通过WebGL2RenderingContext界面提供的。

<canvas>元素也被 Canvas API 用 于在网页上进行2D图形处理。

2、<canvas> 标签

<canvas>元素可被用来通过 JavaScript(Canvas API 或 WebGL API)绘制图形及图形动画。 示例:

<canvas id="canvas" width="300" height="300">
抱歉,您的浏览器不支持 canvas 元素
(这些内容将会在不支持<canvas>元素的浏览器或是禁用了 JavaScript 的浏览器内渲染并展现)
</canvas>

<canvas> 元素本身只是一个位图,不提供任何绘制对象的信息。

3、Canvas API

Canvas API 提供了一个通过JavaScript 和 HTML的<canvas>元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。
Canvas API主要聚焦于2D图形。而同样使用<canvas>元素的 WebGL API 则用于绘制硬件加速的2D和3D图形。

4、CSS Painting API

CSS Painting API — CSS Houdini API 的一部分— 允许开发人员编写 JavaScript 函数,这些函数可以直接绘制到元素的背景、边框或内容中。

使用 CSS 绘画 API
CSS Paint API 旨在使开发人员能够以编程方式定义图像,然后可以在任何可以调用 CSS 图像的地方使用这些图像,例如 CSS background-image、border-image、mask-image等。

5、CSS Houdini

Houdini是一组底层API,它们公开了CSS引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展CSS。 Houdini是一组API,它们使开发人员可以直接访问CSS 对象模型 (CSSOM),使开发人员可以编写浏览器可以解析为CSS的代码,从而创建新的CSS功能,而无需等待它们在浏览器中本地实现。

Flutter Web 适用场景的注意点

  • 适合的场景:渐进式web应用、单页应用、现有Flutter移动应用拓展到Web(以应用为中心)

  • 不适合的场景:富文本和瀑布流的页面(以文档未中心的模式)

  • Web应用中不能使用 dart:io 库(文件系统在浏览器中无法访问)

  • Web应用的网络功能,使用 http package库

  • 目前在 Web 中尚未支持 Dart 通过 isolates 机制实现并发。Flutter Web 没有内置并发的支持,但你可以尝试通过 web workers 来解决这个问题。

  • Platform.is 这个API 在Web内还不能用

  • 不支持的API加上逻辑判断,在Web平台不调用,即可正常编译Web产物

    import 'package:flutter/foundation.dart' show kIsWeb;
    if (kIsWeb) {
    //do something
    }

Flutter Web 首次加载慢优化

Flutter Web项目部署后,首次加载耗时12秒!

Canvaskit渲染加载慢的原因

flutter build web --release 默认是Canvaskit渲染构建Web产物

1、默认使用外网地址加载JS库、字体库、CSS等资源文件

https://unpkg.com/canvaskit-wasm@0.24.0/bin/
https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf
https://fonts.googleapis.com/css2?family=Noto+Sans+JP

2、 资源文件过大

canvaskit.wasm  # canvaskit 库 2.8MB
main.dart.js # 业务及UI逻辑代码 600KB

Canvaskit渲染的优化

1、有 canvaskit 加载地址

  • 使用cdn地址

使用CDN地址的构建命令

flutter build web --release --base-href '/web/' --dart-define=FLUTTER_WEB_CANVASKIT_URL=https://cdn.cnht.com.cn/cdn_resources/common/canvaskit-wasm/0.31.0/bin/
  • 使用服务的本地文件加载 canvaskit 库
#/web是Tomcat里项目的名称
flutter build web --release --base-href '/web/' --dart-define=FLUTTER_WEB_CANVASKIT_URL=/web/canvaskit/

2、本地化加载字体

1)KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf字体下载放到工程内
2)生产Web产物后,需要修改build目录下的main.dart.js

路径修改为:

Bn("assets/fonts/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf","Roboto")

3、本地化加载css2?family=Noto_Sans+SC

1)css文件以及css文件内的所有woff2文件,下载放到工程内

2)生产Web产物后,需要修改build目录下的main.dart.js


修改为:

return A.a1(p.vy("assets/fonts/googlefonts.css"),$async$mc)

4、Canvaskit渲染的结论

对比上图,减少4秒,但总体耗时还在4秒左右,不够理想。
主要原因是canvaskit.wasm文件太大,下载耗时3秒多。

使用Html渲染构建及优化

此次更换了一个更复杂的页面,包含几个网络请求、图表绘制、列表数据等

1、使用HTML渲染构建

#  --base-href 可以根据服务器域名的path来设置
flutter build web --release --web-renderer html --base-href '/h/f/demo_web/'

2、加入tree-shake优化

flutter 构建 web 不支持 --tree-shake-icons 命令,不能直接使用,报错如下

借助构建其他平台做资源文件的摇树优化,然后拷贝到Web产物内

  1. flutter build apk –tree-shake-icons 构建Android产物
  2. 找到优化后的资源文件使用rar工具解压缩得到优化后的MaterialIcons-Regular.otf,
    build/host/intermediates/compressed_assets/release/out/assets/flutter_assets/fonts/MaterialIcons-Regular.otf.jar
  3. 把优化后的MaterialIcons-Regular.otf替换Web产物的文件
    web/assets/fonts/MaterialIcons-Regular.otf
  4. 优化后 MaterialIcons-Regular.otf 大小从1.6MB优化到1KB

优化后,只把使用的返回箭头保留了下来

3、tree-shake优化后效果

1、部署到本地Tomcat,耗时1.62s

2、部署到服务器,耗时2.59s
/h/f/demo_web/assets/NOTICES 许可证文件,比较大,耗时较长,可以优化

4、禁用PWA 渐进式web应用

修改index.html内的js即可,去掉flutter.js,去掉flutter_service_worker.js。
flutter_service_worker.js 这个文件的作用是提供 PWA 功能支持,国内很少用这个特性。
加载flutter_service_worker.js,内部会下载如下4个文件,去掉后减少5个请求,减少1.2秒

// 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'})));
})
);
});

5、去掉无用的字体

如下:build/web/assets/FontManifest.json 生成的原文件内容,为什么会有这么多字体?
后来经过查看,是因为工程pubspec.yaml里引用了 ,font_awesome_flutter: ^8.8.1cupertino_icons: ^1.0.1 两个字体图标库造成的

[{
"family": "MaterialIcons",
"fonts": [{
"asset": "fonts/MaterialIcons-Regular.otf"
}]
}, {
"family": "packages/cupertino_icons/CupertinoIcons",
"fonts": [{
"asset": "packages/cupertino_icons/assets/CupertinoIcons.ttf"
}]
}, {
"family": "packages/font_awesome_flutter/FontAwesomeBrands",
"fonts": [{
"weight": 400,
"asset": "packages/font_awesome_flutter/lib/fonts/fa-brands-400.ttf"
}]
}, {
"family": "packages/font_awesome_flutter/FontAwesomeRegular",
"fonts": [{
"weight": 400,
"asset": "packages/font_awesome_flutter/lib/fonts/fa-regular-400.ttf"
}]
}, {
"family": "packages/font_awesome_flutter/FontAwesomeSolid",
"fonts": [{
"weight": 900,
"asset": "packages/font_awesome_flutter/lib/fonts/fa-solid-900.ttf"
}]
}]

删除pubspec内两个库后

[{
"family": "MaterialIcons",
"fonts": [{
"asset": "fonts/MaterialIcons-Regular.otf"
}]
}]

6、HTML渲染构建结论

使用html渲染构建 + 3步优化,清空缓存加载耗时1.23秒,基本和正常Web开发的页面加载速度一致。

3步优化里,图标摇树优化目前还是手动处理,其他2步都在工程那修改。

根据文档说明和实践,目前我们用 HTML 方式构建渲染是最佳选择;

Flutter 官网看着也是使用 HTML 构建方式.