Flutter For Web性能優化和新場景探索
背景
近些年隨著Flutter開發的App不斷涌現,其靈活高效的編程體驗、建設良好的開發生態和后期易維護等優點,逐漸得到開發者和企業的認可。
Flutter代碼稍作調整,即可同時編譯、打包出來App和Web/H5站點。后者即為Flutter For Web(簡寫FFW)。例如:若App內嵌了Flutter頁面,那么這些頁面就可以被重復利用,生成M站。
但是FFW直接產出的Web/H5站點,首屏加載速度普遍較慢。另外,深入使用FFW也會發現,其2種渲染模式在復雜頁面的交互上,有不同程度的卡頓問題。
針對上述性能問題的解決,作者做了較為詳盡的調研:本文首先分享了性能優化的經驗;然后引入element-embedding的概念;最后分享一種探索出的、適用于某些場景的試驗方案。
一.渲染模式及性能優化
FFW有2種渲染模式,是由同一套源碼,使用2種不同的命令,打包出來的2套編譯產物。
1.1渲染模式介紹
- html渲染模式:Flutter采用html的custom element,CSS,canvas和SVG來渲染UI元素。值得注意的是,此模式最終產物的html標簽數量十分有限,仍以canvas繪制為核心。這也是導致其不夠“瀏覽器友好”的原因。
- canvaskit渲染模式:Flutter將 Skia 編譯成 WebAssembly 格式,并使用 WebGL 渲染。此模式必需加載wasm內核文件和noto字體文件。此模式性能表現、瀏覽器兼容性表現更優秀。若是對首屏速度要求較少的場景,如內部Web系統,建議使用此模式。
1.2優化經驗(html渲染)
?1.2.1首屏速度優化
- Icon font裁剪。若項目中使用到了MaterialDesign圖標字體庫,請使用最新的Flutter SDK,在編譯期間自動對字體資源進行了裁剪,并重新生成otf/ttf文件。
- gzip開啟。若Server端開啟gzip,主Javascript文件(main.dart.js)的體積優化將超過1倍。
- 分片和hash化。主Javascript文件體積較大,可以利用腳本在每次打包之前,將其拆分成n個子文件;在入口處增加邏輯,用戶在進入html后,并行下載n個子文件,最后動態組裝。
- 可借助flutter_web_optimizer工具庫。打包命令:
flutter build web --web-renderer html --release --pwa-strategy none
flutter pub run flutter_web_optimizer optimize --asset-base ./
- 主html優化。利用傳統前端優化方法:defer、preconnect和dns-prefetch等屬性配置。
? 1.2.2 刷新幀率優化
- build刷新相關:
- 局部刷新。目的是減少rebuild范圍。在大型、復雜頁面的性能優化上,可以利用StreamBuilder或Provider機制,實現局部刷新。
- Clean的build方法。不在build方法內進行邏輯計算。通過DevTools性能監測工具,可以發現用戶交互操作(如滾動長列表)后,build方法可能被頻繁調用。所以build方法越復雜,越可能導致卡頓等性能問題。
- 多使用無狀態的、靜態化的Widget。如:若Widget不涉及狀態,就封裝為StatelessWidget。又如:用得上KeeppAlive模式的Widget,在性能優化的時候,也可以加以使用,以獲得Widget的狀態保持、減少build刷新。
- Scroll組件相關
- SingleChildScrollView內嵌Column。若情況為“列表item結構復雜、不統一,且item數量有限”時建議使用。
- ListView.builder。若情況為“列表的item結構類似,或長度很長、甚至不限”時使用。其優點是可動態復用item的資源,節省內存開銷。
- 可以采用自定義的“彈簧屬性”的physics。自定義的physics可調整滾動的速度、延伸、回彈效果等。
class AhCustomScrollPhysics extends ScrollPhysics {
const AhCustomScrollPhysics({ScrollPhysics? parent}) : super(parent: parent);
@override
AhCustomScrollPhysics applyTo(ScrollPhysics? ancestor) {
return AhCustomScrollPhysics(parent: buildParent(ancestor));
}
@override
SpringDescription get spring => SpringDescription.withDampingRatio(
mass: 0.1, //質量,控制滾動的慣性
stiffness: double.maxFinite, //剛性,滾動收尾速度
ratio: 0.1, //damping: 0.1, //阻尼,俗稱摩擦力
);
1.3優化經驗(canvaskit渲染)
?1.3.1首屏速度優化
- wasm內核處理
在主html里配置canvaskit.wasm加載路徑的前綴(最新FlutterSDK支持的功能)。
或者存放在國內CDN并使用url前綴。
否則此文件會從Google的一個外網CDN匹配和下載,國內訪問速度較慢。
goCanvaskit = () => {
console.log(target);
_flutter.loader.loadEntrypoint({
entrypointUrl: "./flutter_canvaskit/main.dart.js",
onEntrypointLoaded: async (engineInitializer) => {
let appRunnerCanvaskit = await engineInitializer.initializeEngine({
hostElement: target,
canvasKitBaseUrl: "./flutter_canvaskit/canvaskit/", //前綴處理
});
await appRunnerCanvaskit.runApp();
console.log("canvaskit loaded.");
}
});
};
- noto字體處理。
在入口處(main.dart)里主動下載、加載noto字體。
否則,此文件將從外網CDN匹配和下載;并且加載過程中,界面的文字會展示亂碼。
var fontLoader2 = FontLoader("Noto Sans SC");
fontLoader2.addFont(fetchFont2());
await fontLoader2.load()
Future<ByteData> fetchFont2() async {
var url = Uri.parse(
'http://{your-cdn-host}/ah-assets/k3kXo84MPvpLmixcA63oeALhL4iJ-Q7m8w%20%281%29.otf'
);
final response = await http.get(url);
if (response.statusCode == 200) {
return ByteData.view(response.bodyBytes.buffer);
} else {
throw Exception('Failed to load font');
}
}
?1.3.2刷新幀率優化
同html的刷新幀率優化。
1.4首屏優化數據
html模式數據分析對比
抽樣測速的數據
同內容的、Vue.js線上版本,抽樣測速數據
首次加載, js大文件列表
1.5分析結果
FFW的html渲染模式,首開速度已經接近傳統Vue.js站點;canvaskit模式的刷新幀率效率,也已經接近App端的flutter代碼。但是,后者的首屏速度,由于必要的noto字體和wasm內核文件,首開耗時依然過久。
另外,html模式在刷新幀率上有略卡頓的問題。這是由于渲染產物使用了較少的html標簽,主要仍依靠canvas繪制;而主流瀏覽器對于canvas繪制的優化,遠沒有html標簽、DOM樹成熟。
Google團隊已經將canvaskit渲染模式作為未來優化的方向。為了提升加載速度,在 112 或更高版本的 Chromium中優化了wasm的底層支持,以縮小wasm的體積和提升性能表現。但是短期內現狀難以得到有效解決。
所以問題歸結為:首開速度和交互性能,不能兼得。
最新的element-embedding技術,為解決此“二選一”難題提供了新的思路。
二. element-embedding新功能
element-embedding是Flutter SDK 3.7的新功能;在2023年的Flutter Forward大會上被推出。在Github的Flutter Sample項目,有兩個demo:html+js集成和Angular.js集成。
圖片
? 特性:
- Flutter的渲染產物,可以作為一個div里的canvas繪制層,而被宿主使用;
- 這個div可以在任何合適的時機被開啟渲染(否則不會加載);
- Flutter的代碼與它的宿主代碼(Angular.js或Vue.js等),可以通過js函數通信。
? 優點:
- 低侵入性:與傳統Vue.js等項目的“混合”,不影響宿主的首屏加載速度等。
- 可交互性:通過js函數即可完成與宿主的通信。
- 增加了FFW的使用場景。
三. 替換AB方案
3.1利用FFW打包2種渲染產物
利用FFW可以方便地打包2種渲染產物的特性:
- 首屏速度較快,使用html渲染產物。
- 后續交互性能為了更順滑,使用canvaskit渲染產物。
3.2目標是切換過程中用戶無感
- 前者內嵌1個后者的隱藏element-embedding。它會在前者加載完畢后“延遲加載”。
- 在合適的時機,把前者的“狀態參數”傳遞給后者。
- 后者更新狀態后,“靜默替換”展示。
3.3方案描述
圖片
- html渲染產物作為“宿主”,首先被加載。
- canvaskit渲染產物是一個內部隱藏的element-embedding。
- 當用戶每一次交互“操作”后,都判斷一下canvaskit是否渲染完畢。
- 如果canvaskit已經渲染完畢,則傳參、切換、展示。
- 用戶的操作狀態得以保持。由于用戶的交互狀態(滾動位置、切換位置等)通過傳參得到保持,切換的過程“近乎無感”。
3.4應用場景
頁面交互(滾動、點擊、切換等)操作不太復雜的場景。
最后
在純Web開發領域,傳統框架(Vue.js,React.js等)仍是優先的選擇。但是,經過技術探索,仍能找到FFW的一些應用場景。尤其App端Flutter代碼轉為Web/H5的需求很強時,可以考慮使用本文最后講述的、經過優化和重新架構的FFW方案。
作者簡介
魏子博
■ 經銷商技術部-移動APP團隊
■ 之家新人,移動端全棧開發經驗。