本文記錄了基於 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 Dev Tool 中的 CPU 火焰圖,可以看到時間佔用如下:
效能優化
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);
''');
}
複製程式碼
結果如下:
可以看到,載入部分的耗時減少了,而包含插入指令碼的 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
函式插入。