一次 Flutter WebView 效能優化

entronad發表於2020-01-16

本文記錄了基於 WebView 的 Flutter 視覺化庫:echarts_flutter 的一次優化載入效能的過程。

對於任何基於 WebView 的元件,html 的載入都是關乎效能的一個重要環節。 echarts_flutter 的基本原理是用 WebView 渲染本地的 echarts 圖表,因此也不例外。

echarts_flutter 的 WebView 載入主要涉及以下幾個部分:

  • 模板 HTML
  • echarts 指令碼
  • echarts 擴充套件指令碼
  • 圖表邏輯程式碼

其中模板 html 和圖表邏輯程式碼的體量很小,重點是 echarts 本體及擴充套件指令碼載入。

Echarts 最強大的功能之一,就是具有很多功能強大的擴充套件,比如 WebGL 3D圖表、Map 地圖元件,在資料視覺化要求越來越高的今天,這些擴充套件幾乎成了和本體一樣重要的部分,因此允許使用者方便的引入擴充套件是一個必不可少的功能。此外為了避免麻煩的 asset 管理,我們希望無論是 HTML 還是 JavaScript 指令碼,都能以字串的方式處理,即 WebView 載入統一資源定位符(URI)。

因此這其中就有以下幾個問題:

  • 指令碼的載入時機是直接放在 HTML 中還是後期插入
  • URI 對一些特殊字元有限制,需要安全的編碼方式

最初的時候,方案是這樣考慮的:按照一般理解內容儘量都放在 HTML 中一次載入是最好的。考慮到 JavaScript 指令碼中有大量的 URI 限制字元,組裝完 HTML 後轉換成 Base64 編碼。由於事先不知道使用者引入的指令碼,編碼轉換通過函式動態完成:

String _getHtml(
  String echartsScript,
  List<String> extensions,
  String extraScript,
) {
  ... // 拼接並返回所有 HTML 和指令碼
}


  @override
  void initState() {
    super.initState();
    // 初始化的時候進行 Base64 轉換
    _htmlBase64 = 'data:text/html;base64,' + base64Encode(
      const Utf8Encoder().convert(_getHtml(
        echartsScript,
        widget.extensions ?? [],
        widget.extraScript ?? '',
      ))
    );
    _currentOption = widget.option;
  }


  @override
  Widget build(BuildContext context) {
    return WebView(
      // 載入所有內容
      initialUrl: _htmlBase64,
      ...
    );
  }
複製程式碼

效能測試

為進行效能分析,進行一個簡單初步的效能測試。用例是載入三個圖表,其中第二個引入了 WebGL 渲染 3D 的圖表,第三個引入帶動畫的水球圖:

一次 Flutter WebView 效能優化

利用 Flutter Dev Tool 中的 CPU 火焰圖,可以看到時間佔用如下:

一次 Flutter WebView 效能優化

效能優化

Echarts 本體和很多擴充套件的指令碼體積都非常大,在執行時拼接字串和編碼轉換無疑都是很耗時的,但是通過 URI載入的話為保證合法又是必要的,如何解決這個矛盾呢?

不如捨棄“一次全部載入”的想法,把不定的、動態的部分通過 evaluateJavascript 函式插入,無需編碼轉換;把確定的、靜態的事先轉碼好直接載入。

為此,先做個實驗,其他條件都不動,僅把所有的指令碼(Echarts 本體、擴充套件)移出 HTML ,用 evaluateJavascript 函式插入,看效能變化如何:

  @override
  void initState() {
    super.initState();
    _htmlBase64 = 'data:text/html;base64,' + base64Encode(
      const Utf8Encoder().convert(_getHtml(
        // 將編碼轉換中傳入的所有指令碼去掉
        // echartsScript,
        // widget.extensions ?? [],
        // widget.extraScript ?? '',
      ))
    );
    _currentOption = widget.option;
  }
  
  
  void init() async {
    final extensionsStr = this.widget.extensions.length > 0
    ? this.widget.extensions.reduce(
        (value, element) => (value ?? '') + '\n' + (element ?? '')
      )
    : '';
    await _controller?.evaluateJavascript('''
      // 改在頁面載入完成後注入
      $echartsScript
      $extensionsStr
      const chart = echarts.init(document.getElementById('chart'), null);
      ${this.widget.extraScript}
      chart.setOption($_currentOption, true);
    ''');
  }
複製程式碼

結果如下:

一次 Flutter WebView 效能優化

可以看到,載入部分的耗時減少了,而包含插入指令碼的 onPageFinished 函式耗時增加了,總耗時減少了不少。

看來對大段字串的編碼轉換確實價效比低,改用 evaluateJavascript 函式插入是個可行的方向。

這樣我們再把所有的動態編碼邏輯去掉,模板 HTML 直接以常理字串載入。而且由於現在的 HTML 靜態、簡短,我們可以手動轉換非法字元,直接傳入 UTF-8 編碼,這樣我們的元件就無需引入 dart:convert 庫了,而且原始碼更直觀。

const htmlUtf8 = 'data:text/html;UTF-8,<!DOCTYPE html><html><head><meta charset="utf-8"><style type="text/css">body,html,%23chart{height: 100%;width: 100%;margin: 0px;}div {-webkit-tap-highlight-color:rgba(255,255,255,0);}</style></head><body><div id="chart" /></body></html>';


  @override
  void initState() {
    super.initState();
    _currentOption = widget.option;
  }
  
  @override
  Widget build(BuildContext context) {
    return WebView(
      initialUrl: htmlUtf8,
      ...
    );
  }
複製程式碼

這樣測試結果如下:

一次 Flutter WebView 效能優化

可以看到,耗時又有進一步的減少,主要體現在載入部分。

這樣相對於最初的時候,效能提升還是比較大的。

Echarts 本體的指令碼也是確定的、靜態的,如果把它事先放在HTML裡,並事先轉好碼呢:

const echartsHtmlBase64 = '...';

  
  @override
  Widget build(BuildContext context) {
    return WebView(
      initialUrl: echartsHtmlBase64,
      ...
    );
  }
複製程式碼

結果如下:

一次 Flutter WebView 效能優化

相比之前優化的結果耗時反而更多了。

可見“指令碼放在 HTML 中“並不一定比” evaluateJavascript 函式插入”好,甚至由於編碼等原因,反而可能更耗時。

結論

綜上,最終的優化方案就採用:模板 HTML 以UTF-8 URI 字串的形式載入,所有指令碼和邏輯程式碼以 evaluateJavascript 函式插入。

相關文章