在上一篇文章的最後,我們發現問題點了,這篇文章中就來聊聊如何簡單又有效的解決 PageView 動畫加上複雜畫面造成的卡頓問題。
為了更準確解決 PlayerInfoGameLogView 被頻繁建立的問題,也為了讓讀者們可以一起同樂,我們先在 Dartpad 準備有問題的範例程式 [連結]。在範例程式中,當我們滑動 PageView 時,PageView 頻繁的呼叫 itemBuilder 來更新畫面,讓 PageView 中的每一個 Item 可以隨著滑動改變大小,但是這麼做也使得 GameLogView 頻繁的被 Rebuild,即便每次傳進去的 gameLogs 是一模一樣的。
class MyApp extends StatelessWidget {
const MyApp({super.key, required this.players});
final List<Player> players;
@override
Widget build(BuildContext context) {
return Center(
child: PlayerPageView(
itemCount: players.length,
itemBuilder: (BuildContext context, int index) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.center,
child: GameLogView(gameLogs: players[index].gameLogs),
);
},
),
);
}
}
讓我們看一下這段範例程式碼的效能分析,與正式程式碼的效能差不多,在 UI phase 階段所花的時間都偏高。
最終,我希望 Flutter 不要總是 Rebuild GameLogView,而達到這個目標,我們可以把 gameLogs 放在 Provider 中,然後需要使用 gameLogs 的地方呼叫 context.watch 去存取並監聽 gameLogs,這樣一來就能讓 Widget 不需要一層一層傳遞 gameLogs,最外層的 GameLogView 也就可以加上 const 修飾詞,讓 Flutter 知道這是一個固定的 Widget,避免 Flutter 總是 Rebuild 它。
class MyApp extends StatelessWidget {
const MyApp({super.key, required this.players});
final List<Player> players;
@override
Widget build(BuildContext context) {
return Center(
child: PlayerPageView(
itemCount: players.length,
itemBuilder: (BuildContext context, int index) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.center,
child: Provider<List<GameLog>>.value(
value: players[index].gameLogs,
child: const GameLogView(),
),
);
},
),
);
}
}
class _GameLogTable extends StatelessWidget {
const _GameLogTable();
@override
Widget build(BuildContext context) {
var gameLogs = context.watch<List<GameLog>>();
return Table(
...
);
}
}
當我們調整好程式碼之後,PageView 執行 setState 之後,GameLog 就不會 Rebuild,而是會重複使用已經建好的 Widget,有興趣的觀眾也可以在 GameLogView 的 build 方法印 log 觀察看看。最後讓我們看一下問題解決之後的範例程式碼的效能分析,在少數幾個 Frame 中,UI phase 花的時間是超時的,剩下大部分時間都是在標準以內。
如果實際上運行解決後的範例程式之後 [連結],可以發現滑動的過程中比較順了,但還是有一些時刻會感受到卡頓。
當我們使用 Provider 提升效能之後,我們發現第一次 Build GameLogView 的時候還是會超時,使得下一個 GameLog 顯示時,畫面會出現明顯卡頓。此時我們暫時沒有比較好的辦法可以解決問題,因為 Table 目前沒有提供 builder 的方法,當渲染比較大的 Table 時,所有欄位都會在第一時間被建立,無論他有沒有出現在畫面上,使得 UI phase 的時間還是會比較長,也就是我們上面效能分析所顯示的狀況。
當我們把這個做法放回產品程式碼中,並再次檢測 App 效能,可以發現超時的 UI phase 大幅減少,更多的是超時的 Raster phase,這也表示我們方法有效這個畫面的效能。
過早優化是萬惡之源,當我們發現效能問題時,透過釐清與分析問題,找到並解決瓶頸,在能花最小的力氣獲得最大的增益。如果我們再開發的時候為了使用 const 而寫了很多不必要的程式碼,除了浪費時間之外,也降低程式碼的可讀性,獲得的增益可能微乎其微。