一起漫部 是基于区块链技术发明的新型数字生存。

目录

  • 前言
  • 开发环境
  • 渲染模式
  • 首屏白屏
  • 优化计划

    • 启屏页优化
    • 包体积优化

      • 去除无用的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 环境

如图所示,咱们团队是在 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。

cp -r ./build/app/intermediates/flutter/release/flutter_assets/fonts ./web/assets

MaterialIcons-Regular.otf拷贝至/web/assets目录下,当前每次进行 Web 利用构建将会应用 Android 下MaterialIcons-Regular.otf字体。

deferred提早加载

main.dart.js包含我的项目中所有的Dart代码,导致文件体积很大,对此官网提供了deferred关键字来实现Widget的提早加载,具体应用查看官网文档

咱们对deferred的应用进行了封装解决,仅供参考:

/// loadLibrarytypedef AppLibraryLoader = Future<dynamic> Function();/// deferredWidgetBuildertypedef AppDeferredWidgetBuilder = Widget Function();/// 提早加载组件/// 不在 build 里应用 FutureBuilder 加载,因为 build 执行多少次就会导致 widget 创立多少次/// 这里在 initState 加载,或者当 AppDeferredWidgetBuilder 扭转时从新加载class AppDeferredWidget extends StatefulWidget {  const AppDeferredWidget({    Key? key,    required this.libraryLoader,    required this.builder,    Widget? placeholder,  })      : placeholder = placeholder ?? const AppDeferredLoading(),        super(key: key);  final AppLibraryLoader libraryLoader;  final AppDeferredWidgetBuilder builder;  final Widget placeholder;  static final Map<AppLibraryLoader, Future<dynamic>> _moduleLoaders =  <AppLibraryLoader, Future<dynamic>>{};  static final Set<AppLibraryLoader> _loadedModules = <AppLibraryLoader>{};  /// 预加载  static Future<dynamic> preload(AppLibraryLoader loader) {    if (!_moduleLoaders.containsKey(loader)) {      _moduleLoaders[loader] = loader().then((_) {        _loadedModules.add(loader);      });    }    return _moduleLoaders[loader]!;  }  @override  State<AppDeferredWidget> createState() => _AppDeferredWidgetState();}class _AppDeferredWidgetState extends State<AppDeferredWidget> {  Widget? _loadedChild;  AppDeferredWidgetBuilder? _loadedBuilder;  @override  void initState() {    super.initState();    if (AppDeferredWidget._moduleLoaders.containsKey(widget.libraryLoader)) {      _onLibraryLoaded();    } else {      AppDeferredWidget.preload(widget.libraryLoader)          .then((_) => _onLibraryLoaded());    }  }  void _onLibraryLoaded() {    setState(() {      _loadedBuilder = widget.builder;      _loadedChild = _loadedBuilder?.call();    });  }  @override  Widget build(BuildContext context) {    if (_loadedBuilder != widget.builder && _loadedChild != null) {      _loadedBuilder = widget.builder;      _loadedChild = _loadedBuilder?.call();    }    return _loadedChild ?? widget.placeholder;  }}/// 提早加载Loadingclass AppDeferredLoading extends StatelessWidget {  const AppDeferredLoading({Key? key}) : super(key: key);  @override  Widget build(BuildContext context) {    return Container(      color: Colors.white,      alignment: Alignment.center,      child: const AppLogo(),    );  }}
import '../groups/login/login_phone/view/login_phone_page.dart' deferred as login_phone_page;{    AppRoutes.routes_login_phone: (BuildContext context,        {Map<String, dynamic>? arguments}) =>    AppDeferredWidget(      libraryLoader: login_phone_page.loadLibrary,      builder: () => login_phone_page.LoginPhonePage(),    )}

应用deferred提早加载后,业务代码被拆分到多个xxx.part.js的文件,同时主体main.dart.js文件体积从 5.6M 缩小至 4.3M,对包体积优化有肯定成果。

一开始,咱们将我的项目中所有的路由都应用deferred进行提早加载,然而 50 个页面却产生了近 200 个xxx.part.js文件,如何治理数量增多的xxx.part.js文件成了新的问题,况且main.dart.js体积减小并没有达到预期,起初咱们决定放弃全量应用deferred提早加载,仅在不同模块间应用。

启用gzip压缩

  #开启gzip  gzip  on;  #低于1kb的资源不压缩  gzip_min_length 1k;  # 设置压缩所须要的缓冲区大小  gzip_buffers 16 64k;  #压缩级别1-9,越大压缩率越高,同时耗费cpu资源也越多,倡议设置在5左右。  gzip_comp_level 5;  #须要压缩哪些响应类型的资源,多个空格隔开。不倡议压缩图片.  gzip_types text/plain text/css text/javascript text/xml application/json application/x-javascript application/javascript application/xml application/xml+rss image/jpeg image/gif image/png image/jpg;  #配置禁用gzip条件,反对正则。此处示意ie6及以下不启用gzip(因为ie低版本不反对)  gzip_disable "MSIE [1-6]\.";  #是否增加“Vary: Accept-Encoding”响应头  gzip_vary on;

通过配置 nginx 开启 gzip 压缩,main.dart.js传输大小从 5.6M 降落到 1.6M,耗时从 5.22s 缩小到 1.42s,速度晋升显著。

加载优化

加载优化的话次要从大文件分片下载、资源文件hash化和资源文件cdn化和三方面思考,对此咱们又做了以下优化。具体的编码实现请参考 Web Optimize

大文件分片下载

因为main.dart.js体积大,独自下载大文件速度慢,势必影响首屏的加载性能。对此,咱们提出分片加载的计划,具体实现如下:

  1. 通过脚本将main.dart.js切割成 6 个独自的纯文本文件
  2. 通过XHR的形式并行下载 6 个纯文本文件
  3. 期待下载实现后,将 6 个纯文本文件依照程序拼接,失去残缺的main.dart.js文件
  4. 创立script标签,将残缺的main.dart.js文件内容赋值给text属性
  5. 最初将script标签插入到html body

分片代码,仅供参考:

// 写入单个文件Future<bool> writeSingleFile({  required File file,  required String filename,  required int startIndex,  required endIndex,}) {  final Completer<bool> completer = Completer();  final File f = File(path.join(file.parent.path, filename));  if (f.existsSync()) {    f.deleteSync();  }  final RandomAccessFile raf = f.openSync(mode: FileMode.write);  final Stream<List<int>> inputStream = file.openRead(startIndex, endIndex);  inputStream.listen(    (List<int> data) {      raf.writeFromSync(data);    },    onDone: () {      raf.flushSync();      raf.closeSync();      completer.complete(true);    },    onError: (dynamic data) {      raf.flushSync();      raf.closeSync();      completer.completeError(data);    },  );  return completer.future;}final int totalChunk = 6;final Uint8List bytes = file.readAsBytesSync();int chunkSize = (bytes.length / totalChunk).ceil();final List<Future<bool>> futures = List<Future<bool>>.generate(  totalChunk,  (int index) {    return writeSingleFile(      file: file,      filename: 'main.dart_$index.js',      startIndex: index * chunkSize,      endIndex: (index + 1) * chunkSize,    );  },);await Future.wait(futures);/// 分片实现后删除 main.dart.jsfile.deleteSync();

并行下载代码,仅供参考:

    _downloadSplitJs(url){      return new Promise((resolve, reject)=>{        const xhr = new XMLHttpRequest();        xhr.open("get", url, true);        xhr.onreadystatechange = () => {            if (xhr.readyState == 4) {                if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){                  resolve(xhr.responseText);                }            }        };        xhr.onerror = reject;        xhr.ontimeout = reject;        xhr.send();      })    }      _retryCount = 0;    const promises = Object.keys(jsManifest).filter(key => /main.dart_\d.js/g.test(key)).sort().map(key => `${assetBase}${jsManifest[key]}`).map(this._downloadSplitJs);  Promise.all(promises).then((values)=>{    const contents = values.join("");    const script = document.createElement("script");    script.text = contents;    script.type = "text/javascript";    this._didCreateEngineInitializerResolve = resolve;    script.addEventListener("error", reject);    document.body.appendChild(script);  }).catch(()=>{    // console.error("main.dart.js download fail,refresh and try again");    // retry again    if (++this._retryCount > 3) {      const element = document.createElement("a");      element.href = "javascript:location.reload()";      element.style.textAlign = "center";      element.style.margin = "50px auto";      element.style.display = "block";      element.style.color = "#f89800";      element.innerText = "加载失败,点击从新申请页面";      document.body.appendChild(a);    } else {      this._loadEntrypoint(entrypointUrl);    }  });

通过分片加载后,同时开启6个下载工作,最高耗时634ms,加载进一步晋升。

资源文件hash化

浏览器会对同名文件缓存,为防止性能更新不及时,咱们须要对资源文件进行hash化。

首先,咱们须要确定哪些资源文件须要进行hash化?

通过对打包产物剖析,会频繁变动的资源次要是图片、字体和js文件,因而须要对这些资源进行hash解决,实现步骤如下:

  1. 遍历图片、字体和js等文件
  2. 计算每个文件的hash值
  3. 为新文件命名为[name].[hash].[extension]

    资源hash实现后,如何加载新文件?针对图片、字体和js咱们别离做了以下优化
  • js文件次要分为两类,main.dart.js分片后的文件是由XHR间接下载拼接失去的,deferred提早加载拆分的文件是通过 window.dartDeferredLibraryLoader自定义办法 间接组成script标签插入html中,在对文件hash解决时记录下新旧文件名的映射关系,在获取js文件时就能够通过旧文件名获取到新文件名进行加载。
  • 图片和字体文件,通过对main.dart.js源码的剖析,咱们发现程序在启动时会先去读取AssetManifest.jsonFontManifest.json清单文件,依据清单文件外面的资源映射关系去加载对应的图片和字体,因而咱们在打包后去批改资源映射关系,将外面的文件名换成hash后的新文件名就行了。

    资源hash代码,仅供参考:

    /// md5String md5(File file) {final Uint8List bytes = file.readAsBytesSync();// 截取8位即可final md5Hash = crypto.md5.convert(bytes).toString().substring(0, 8);// 文件名应用hash值final basename = path.basenameWithoutExtension(file.path);final extension = path.extension(file.path);return '$basename.$md5Hash$extension';}/// 替换String replace(Match match,File file,String key,Map<String, String> hashFiles,) {// 文件名应用hash值final String filename = md5(file);final dirname = path.dirname(key);final String newKey = path.join(dirname, filename);// hash文件门路final String newPath = path.join(path.dirname(file.path), filename);hashFiles[file.path] = newPath;return '${match[1]}$newKey${match[3]}';}// 读取资源清单文件final File assetManifest =  File('$webArtifactsOutputDir/assets/AssetManifest.json');String assetManifestContent = assetManifest.readAsStringSync();// 读取字体清单文件final File fontManifest =  File('$webArtifactsOutputDir/assets/FontManifest.json');String fontManifestContent = fontManifest.readAsStringSync();// 遍历assets目录final Directory assetsDir = Directory(webArtifactsOutputDir);Map<String, String> hashFiles = <String, String>{};assetsDir  .listSync(recursive: true)  .whereType<File>() // 文件类型  .where((File file) => !path.basename(file.path).startsWith('.'))  .forEach((File file) {if (RegExp(r'main.dart(.*)\.js$').hasMatch(file.path)) {  // 替换资js文件  final String filename = md5(file);  hashFiles[file.path] = path.join(path.dirname(file.path), filename);  jsManifest[path.basename(file.path)] = filename;}if (file.path.contains('$webArtifactsOutputDir/assets')) {  final String key =      path.relative(file.path, from: '$webArtifactsOutputDir/assets');  // 替换资源清单文件  assetManifestContent = assetManifestContent.replaceAllMapped(    RegExp('(.*)($key)(.*)'),    (Match match) => replace(match, file, key, hashFiles),  );  // 替换字体清单文件  fontManifestContent = fontManifestContent.replaceAllMapped(    RegExp('(.*)($key)(.*)'),    (Match match) => replace(match, file, key, hashFiles),  );}});// 重命名文件hashFiles.forEach((String key, String value) {File(key).renameSync(value);});// 写入资源、字体清单文件assetManifest.writeAsStringSync(assetManifestContent);fontManifest.writeAsStringSync(fontManifestContent);

    测试后果:当初图片、字体和js都曾经是加载hash后的资源,仍然失常运行,证实此计划是可行的。

    资源文件cdn化

    cdn具备减速性能,为了进步网页加载速度,咱们须要对资源文件进行cdn化。在实践中,发现Flutter仅反对相对路径的资源加载形式,而且对于图片和Javascript资源加载逻辑也不雷同,为此咱们须要别离进行优化。

图片解决

通过对main.dart.js源码的剖析,咱们发现在加载图片资源时,会先在index.html中查找<meta name="assetBase" content="任意值">的meta标签,获取meta标签的content值作为baseUrlasset(图片名称)进行拼接,最初依据拼接好的URL来加载资源。然而咱们在index.html中并没有找到这种meta标签,于是就会依据相对路径进行图片加载。对此,咱们在打包时向index.html注入 meta 标签并把 content 设置为CDN门路,就样就实现了图片资源cdn化。

JS解决

通过对main.dart.js源码的剖析,咱们发现在加载xxx.part.js文件时会先判断window.dartDeferredLibraryLoader是否存在,如果存在的话则应用自定义的dartDeferredLibraryLoader办法加载,否则应用默认的script标签加载。对此,咱们在打包时向index.html注入dartDeferredLibraryLoader办法的实现,将传过来的uriAsString参数批改成CDN的地址,这样就实现了JS资源的cdn化。

实现代码,仅供参考:

import 'package:html/dom.dart';import 'package:html/parser.dart' show parse;import 'package:path/path.dart' as path;final File file = File('$webArtifactsOutputDir/index.html');final String contents = file.readAsStringSync();final Document document = parse(contents);/// 注入meta标签final List<Element> metas = document.getElementsByTagName('meta');final Element? headElement = document.head;if (headElement != null) {  final Element meta = Element.tag('meta');  meta.attributes['name'] = 'assetBase';  meta.attributes['content'] = 'xxx';  if (metas.isNotEmpty) {    final Element lastMeta = metas.last;    lastMeta.append(Text('\n'));    lastMeta.append(Comment('content值必须以 / 结尾'));    lastMeta.append(Text('\n'));    lastMeta.append(meta);  } else {    headElement.append(Comment('content值必须以 / 结尾'));    headElement.append(Text('\n'));    headElement.append(meta);    headElement.append(Text('\n'));  }}/// 注入scriptString dartDeferredLibraryLoader = r'''// auto-generate, dont edit!!!!!!var assetBase = null;var jsManifest = null;function dartDeferredLibraryLoader(uri, successCallback, errorCallback, loadId) {  console.info('===>', uri, successCallback, errorCallback, loadId);    let src;  try {    const url = new URL(uri);    src = `${assetBase}${jsManifest[url.pathname.substring(1)]}`;  } catch (e) {    src = `${assetBase}${jsManifest[uri.substring(1)]}`;  }    script = document.createElement("script");  script.type = "text/javascript";  script.src = src;  script.addEventListener("load", successCallback, false);  script.addEventListener("error", errorCallback, false);  document.body.appendChild(script);}'''    .replaceAll(RegExp('var assetBase = null;'), 'var assetBase = xxx;')    .replaceAll(      RegExp('var jsManifest = null;'),      'var jsManifest = ${jsonEncode(jsManifest)};',      // 'var jsManifest = {"main.dart_0.js":"main.dart_0.7a183f1b.js", "main.dart.js_1.part.js":"main.dart.js_1.part.0445cc90.js"};',    );final List<Element> scripts = document.getElementsByTagName('script');// 是否注入jsbool isInjected = false;for (int i = 0; i < scripts.length; i++) {  final Element element = scripts[i];  if (element.text.contains(RegExp(r'var serviceWorkerVersion'))) {    element.text = '${element.text}\n$dartDeferredLibraryLoader';    isInjected = true;    break;  }}if (!isInjected) {  final Element? headElement = document.head;  if (headElement != null) {    final Element script = Element.tag('script');    script.text = '\n$dartDeferredLibraryLoader';    if (scripts.length > 1) {      final Element firstScript = scripts.first;      headElement.insertBefore(script, firstScript);      headElement.insertBefore(Text('\n'), firstScript);    } else {      headElement.append(script);      headElement.append(Text('\n'));    }  }}// 写入文件file.writeAsStringSync(document.outerHtml);

为了不便测试,在本地应用nginx搭建了个文件服务,再将build/web文件下的assets和js文件上传到build/cdn下,用于模仿cdn服务。

nginx配置,仅供参考:

server {    listen       9091;    server_name  localhost;    root   /build/cdn;    #   指定容许跨域的办法,*代表所有    add_header Access-Control-Allow-Methods *;    #   预检命令的缓存,如果不缓存每次会发送两次申请    add_header Access-Control-Max-Age 3600;    #   不带cookie申请,并设置为false    add_header Access-Control-Allow-Credentials false;    #   示意容许这个域跨域调用(客户端发送申请的域名和端口)     #   $http_origin动静获取申请客户端申请的域   不必*的起因是带cookie的申请不反对*号    add_header Access-Control-Allow-Origin $http_origin;    #   示意申请头的字段 动静获取    add_header Access-Control-Allow-Headers     $http_access_control_request_headers;     #缓存配置    location ~ .*\.(jpg|png|ico)(.*){        expires 30d;    }    #缓存配置    location ~ .*\.(js|css)(.*){        expires 7d;    }    location / {        autoindex on;             #显示索引        autoindex_exact_size off; #显示大小        autoindex_localtime on;   #显示工夫        charset utf-8;            #防止中文乱码    }    #开启gzip    gzip  on;      #低于1kb的资源不压缩     gzip_min_length 1k;    # 设置压缩所须要的缓冲区大小    gzip_buffers 16 64k;    #压缩级别1-9,越大压缩率越高,同时耗费cpu资源也越多,倡议设置在5左右。     gzip_comp_level 5;     #须要压缩哪些响应类型的资源,多个空格隔开。不倡议压缩图片.    gzip_types text/plain text/css text/javascript text/xml application/json application/x-javascript application/javascript application/xml application/xml+rss image/jpeg image/gif image/png image/jpg;    #配置禁用gzip条件,反对正则。此处示意ie6及以下不启用gzip(因为ie低版本不反对)    gzip_disable "MSIE [1-6]\.";      #是否增加“Vary: Accept-Encoding”响应头    gzip_vary on;}

测试后果:尽管网页申请url端口与图片申请url和js申请url的端口不统一,仍然失常运行,证实此计划是可行的。

成绩

  • 一起漫部 App
  • 一起漫部 Web

参考链接

  • FlutterWeb性能优化摸索与实际

总结

综上所述,就是《一起漫部》对Flutter Web的性能优化摸索与实际,目前咱们所做的性能优化是无限的,将来咱们会持续在Flutter Web上做更多的摸索与实际。如果您对 Flutter Web 也感兴趣,欢送在评论区留言或者给出倡议,非常感谢。