一次 Flutter WebView 性能優化

本文記錄了基於 WebView 的 Flutter 可視化庫:echarts_flutter 的一次優化加載性能的過程。css

對於任何基於 WebView 的組件,html 的加載都是關乎性能的一個重要環節。 echarts_flutter 的基本原理是用 WebView 渲染本地的 echarts 圖表,所以也不例外。html

echarts_flutter 的 WebView 加載主要涉及如下幾個部分:git

  • 模板 HTML
  • echarts 腳本
  • echarts 擴展腳本
  • 圖表邏輯代碼

其中模板 html 和圖表邏輯代碼的體量很小,重點是 echarts 本體及擴展腳本加載。github

Echarts 最強大的功能之一,就是具備不少功能強大的擴展,好比 WebGL 3D圖表、Map 地圖組件,在數據可視化要求愈來愈高的今天,這些擴展幾乎成了和本體同樣重要的部分,所以容許用戶方便的引入擴展是一個必不可少的功能。此外爲了不麻煩的 asset 管理,咱們但願不管是 HTML 仍是 JavaScript 腳本,都能以字符串的方式處理,即 WebView 加載統一資源定位符(URI)。web

所以這其中就有如下幾個問題:安全

  • 腳本的加載時機是直接放在 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 的圖表,第三個引入帶動畫的水球圖:bash

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

性能優化

Echarts 本體和不少擴展的腳本體積都很是大,在運行時拼接字符串和編碼轉換無疑都是很耗時的,可是經過 URI加載的話爲保證合法又是必要的,如何解決這個矛盾呢?async

不如捨棄「一次所有加載」的想法,把不定的、動態的部分經過 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); ''');
  }
複製代碼

結果以下:

能夠看到,加載部分的耗時減小了,而包含插入腳本的 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,
      ...
    );
  }
複製代碼

這樣測試結果以下:

能夠看到,耗時又有進一步的減小,主要體如今加載部分。

這樣相對於最初的時候,性能提高仍是比較大的。

Echarts 本體的腳本也是肯定的、靜態的,若是把它事先放在HTML裏,並事先轉好碼呢:

const echartsHtmlBase64 = '...';

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

結果以下:

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

可見「腳本放在 HTML 中「並不必定比」 evaluateJavascript 函數插入」好,甚至因爲編碼等緣由,反而可能更耗時。

結論

綜上,最終的優化方案就採用:模板 HTML 以UTF-8 URI 字符串的形式加載,全部腳本和邏輯代碼以 evaluateJavascript 函數插入。

相關文章
相關標籤/搜索