Flutter 應用效能最佳化最佳實踐
通常來說,Flutter 建立的應用程式在預設情況下都是高效能的。所以你只需要避開常見的陷阱,不需要使用複雜的分析工具對細節做優化,就可以獲得優異的效能,這些最佳建議將幫助你編寫效能最佳的 Flutter 應用程式。
如何設計一個能最有效地渲染頁面的 Flutter 應用程式?特別是如何確保底層框架生成的繪圖程式碼儘可能高效?你需要經過謹慎地思考之後再使用那些已知效能很低的渲染和佈局操作。請遵循下面的指導。
儘量減少消耗資源的操作
有些操作特別消耗資源。很顯然,你只想在必要地時候使用這些操作。在這種情況下,如何設計和實現應用的 UI,對執行效率有很大影響。
控制 build() 方法的耗時
以下是在設計你的 UI 時的一些建議:
-
避免在
build()
方法中進行重複且耗時的工作,因為當父 widget 重建時,子 Wdiget 的build()
方法會被頻繁地呼叫。 -
避免在一個超長的
build()
方法中回傳一個過於龐大的 widget。把它們分拆成不同的 widget,並進行封裝,另外它們要這樣改變:-
當在
State
物件上呼叫setState()
時,所有後代 widget 都將重建。因此,將setState()
的呼叫轉移到其 UI 實際需要更改的 widget 子樹部分。如果改變的部分僅包含在 widget 樹的一小部分中,請避免在 widget 樹的更高層級中呼叫setState()
。 -
當重新遇到與前一幀相同的子 widget 實例時,將停止遍歷。這種技術在框架內部大量使用,用於最佳化動畫不影響子樹的動畫。請參閱
TransitionBuilder
模式和遵循此原則的 SlideTransition 程式碼,以避免在動畫過程中重建其後代 widget。(『相同的實例』是使用operator ==
進行評估的,但是請參閱本頁面末尾的陷阱部分,瞭解有關何時避免覆蓋operator ==
的建議。) -
請儘可能地在 widget 上使用
const
建構式函式,這將讓 Flutter 的 widget 重建時間大幅縮短。要自動提醒使用const
,請啟用flutter_lints
package 中推薦的 lints。瞭解更多訊息,請檢視flutter_lints
遷移指南. -
在建立可複用的 UI 程式碼時,最好使用
StatelessWidget
而不是函式。
-
瞭解更多訊息,請檢視:
-
StatefulWidget
API 文件中的 Performance considerations 部分。 -
Widget 與 helper 方法, Flutter 關於為什麼使用 widget 是更好的做法的解析影片。(特別是使用
const
構造的 widget)
謹慎使用 saveLayer()
一些 Flutter 程式碼呼叫了效能代價很大的 saveLayer()
方法來實現 UI 中的各種視覺效果。你使用的其他 widget 或者 package 可能會在幕後呼叫它。也許你的應用程式會大量呼叫 saveLayer()
;過多呼叫 saveLayer()
會導致卡頓。
為什麼 saveLayer 代價很大?
呼叫 saveLayer()
會開闢一片離屏緩衝區並將內容繪製到離屏緩衝區可能會觸發渲染目標切換。
GPU 希望直達目標地執行,但渲染目標迫使 GPU 暫時重導向到該資料流,然後又直接把它切回來。這樣會對渲染吞吐量造成特別大的破壞性。
什麼時候需要 saveLayer?
在執行時,如果你需要動態地顯示各種形狀效果(例如),每個形狀都有一定地透明度,可能(或可能不)重疊,那麼你幾乎必須使用 saveLayer()
。
除錯 saveLayer 的呼叫
你如何才能知道應用程式呼叫 saveLayer()
的頻率,直接還是間接呼叫?
saveLayer()
方法會觸發 DevTools timeline 上的一個事件;透過檢查 DevTools 效能檢視 (DevTools Performance view)
中的 PerformanceOverlayLayer.checkerboardOffscreenLayers
來了解何時在使用 saveLayer
。
儘量減少 saveLayer 的呼叫
你能避免對 saveLayer
的呼叫嗎?這可能需要你重新思考如何建立視覺效果:
-
如果這些呼叫是來自於 你 的程式碼,你能減少或消除它們嗎?例如,也許你的使用者介面重疊了兩個形狀,每個形狀都有非零的透明度:
-
如果它們總是以相同的數量、相同的方式、相同的透明度進行重疊,你就可以預先計算出這個重疊的、半透明物體的樣子,將其快取起來,然後用它代替呼叫
saveLayer()
。這適用於任何你可以預先計算的靜態形狀。 -
你可以換一種繪畫形式以完全避免重疊嗎?
-
-
如果這些呼叫不是來自你的 package,請聯絡 package 的所有者並詢問為什麼需要使用這些呼叫。能否減少或消除它們?如果不能,你可能需要尋找其他作者的 package,或自己編寫。
其他會觸發 saveLayer()
的 widget,也有潛在的效能代價:
ShaderMask
ColorFilter
-
Chip
— 當disabledColorAlpha != 0xff
的時候,會呼叫saveLayer()
-
Text
— 當有overflowShader
時,會呼叫saveLayer()
儘量減少使用不透明度和裁剪
不透明度和裁剪都是特別消耗資源的操作。這裡有一些建議也許對你有幫助:
-
能不用
Opacity
widget,就儘量不要用。有關將透明度直接應用於影象的範例,請檢視 Transparent image,這比使用Opacity
widget 更快。 -
與其將簡單的形狀或文字包裹在一個
Opacity
widget 中,不如用半透明的顏色來繪製它們會更快。(這僅在要畫的形狀中沒有重疊的部分時有效)。 -
要在影象中實現淡入淡出,請考慮使用
FadeInImage
widget,該 widget 使用 GPU 的片段著色器應用漸變不透明度。瞭解更多詳情,請檢視Opacity
文件。 -
Clipping 不會呼叫
saveLayer()
(除非明確使用Clip.antiAliasWithSaveLayer
),因此這些操作沒有Opacity
那麼耗時,但仍然很耗時,所以請謹慎使用。 -
要建立帶圓角的矩形,而不是裁剪矩形來達到圓角的效果,請考慮使用很多 widget 都提供的
borderRadius
屬性。
謹慎使用網格清單和清單
你的網格清單和清單的實現方式可能會給你的應用程式帶來效能問題。本節介紹了建立網格清單和清單時的一個重要的最佳實現方式,以及如何確定你的應用程式是否使用了過多的佈局傳遞。
延遲載入!
當建立一個大型網格清單或清單時,使用帶有回呼的懶惰建構式方法。這可以確保在啟動時只建立螢幕的可見部分。
瞭解更多的訊息和範例,請檢視:
-
實用教程 (Cookbook) 裡的 長清單的處理 文件
-
來自社群的 AbdulRahman AlHamali 撰寫的文章 Creating a
ListView
that loads one page at a time -
Listview.builder
API
避免內部傳遞
關於內部傳遞可能導致你的網格清單和清單出現問題的訊息,見下一節。
儘量減少由內部操作引起的佈局傳遞
如果你經常使用 Flutter 程式設計,你可能已經熟悉了在建立 UI 時 佈局和約束是如何工作的。你甚至可能已經記住了 Flutter 的基本佈局規則:
首先,上層 widget 向下層 widget 傳遞約束條件;
然後,下層 widget 向上層 widget 傳遞大小訊息;
最後,上層 widget 決定下層 widget 的位置。
對於一些 widget,特別是網格清單和清單,佈局過程中效能代價很大。 Flutter 儘可能在 widget 上只執行一次佈局傳遞,但有時需要再次傳遞(稱為 內部傳遞),這可能會降低效能。
什麼是內部傳遞?
例如,你想要所有單元格都具有或大或小的效果(或類似需要輪詢所有單元格的計算)時,就會發生內部傳遞。
例如,考慮一個大型的 卡片
網格清單時。一個網格清單應該有統一大小的單元格,所以佈局程式碼執行了一次傳遞,從網格清單的根部開始(在 widget 樹中),要求網格清單中的 每個 卡片(不僅僅是可見的卡片)來回傳 內部 尺寸—假設沒有任何限制,widget 更喜歡這樣的尺寸。有了這些訊息,底層框架就確定了一個統一的單元格尺寸,並再次重新訪問所有的網格單元,告訴每個卡片應該使用什麼尺寸。
除錯內部傳遞
要確認你是否有過多的內部傳遞,請在 DevTools 中啟用 佈局跟蹤 (Track layouts option) (預設情況下是停用的),並檢視應用程式的 堆疊跟蹤 (stack trace) 以瞭解執行了多少次佈局傳遞。一旦你啟用跟蹤,內部的時間線事件將被標記為 “$runtimeType intrinsics”。
避免內部傳遞
你有幾個選擇來避免內部傳遞:
-
將單元格設定為固定大小。
-
選擇一個特定的單元格作為『錨』 —所有單元格的尺寸都將相對於這個單元格。編寫一個自定義渲染物件,首先定位子錨點,然後在其周圍佈置其他子單元。
要更深入地瞭解佈局的工作原理,請檢視 Flutter 架構概覽 中的 佈局和渲染。
在 16ms 內渲染完成每一幀
由於建立和渲染有兩個獨立的執行緒,因此建立時間為 16ms,60Hz 顯示器上渲染時間為 16ms。如果需要考慮延遲,就要在 16ms 或更短 的時間內建立和顯示幀。請注意,這意味著建立需要少於 8ms,渲染也需要少於 8ms,總計 16ms 或更短。
如果在 profile 建立 狀態下,每一幀渲染時間低於 16ms,你可能不必擔心效能問題以及一些效能陷阱,但仍然應該致力於儘可能快地渲染每一幀。為什麼?
-
將幀渲染時間降低到 16ms 以下可能在視覺上看不出來什麼變化,但可以 延長電池壽命 以及避免發熱問題。
-
可能在你當前測試裝置上執行良好,但請考慮在應用所支援的最低端裝置上的情況。
-
當 120fps 的裝置普及之後,便需要在 8ms 之內完成每一幀的渲染來保證流暢平滑的體驗。
如果你想弄明白為什麼 60fps 會帶來平滑的視覺體驗,請看影片 60fps 是啥意思?
陷阱
如果你需要改善應用程式的效能,或者 UI 流暢度沒達到你的預期,那麼 DevTools 效能檢視 (DevTools Performance view) 可以幫到你!
另外,IDE 中 Flutter plugin 也許也能幫到你。在 Flutter Performance 視窗中,勾選 Show widget rebuild information 複選框。此功能可幫助你檢測幀的渲染和顯示時間是否超過 16ms。外掛也會盡可能提供指向相關提示的連結。
以下行為可能會對你應用的效能產生負面影響。
-
避免使用
Opacity
widget,尤其是在動畫中避免使用。可以使用AnimatedOpacity
或FadeInImage
代替該操作。更多訊息,請參閱 Performance considerations for opacity animation。 -
使用
AnimatedBuilder
時,請避免在不依賴於動畫的 widget 的構造方法中建立 widget 樹,不然,動畫的每次變動都會重建這個 widget 樹,應當將這部分子樹作為 child 傳遞給AnimatedBuilder
,從而只建立一次。更多內容,請參閱 Performance optimizations。 -
避免在動畫中裁剪,儘可能的在動畫開始之前預先裁剪影象。
-
Avoid using constructors with a concrete
List
of children (such asColumn()
orListView()
) if most of the children are not visible on screen to avoid the build cost. -
如果大多數 children widget 在螢幕上不可見,請避免使用回傳具體清單的建構式函式(例如
Column()
或ListView()
),以避免建立成本。 -
避免在
Widget
物件上重寫operator ==
。雖然這看起來有助於避免不必要的重建,但在實踐中,它實際上損害了效能,因為這是 O(N²) 的行為。只有 leaf widget(沒有子的 widget)是個例外,在這種特殊的情況下,比較 widget 的屬性可能比重建 widget 更加有效,也能更少改變 widget 的設定。即使在這種情況下,最好還要快取 widget,因為哪怕有一次對operator ==
進行覆蓋也會導致全面性能的下降,編譯器也會因此不再認為呼叫總是靜態的。
參考資料
瞭解更多效能訊息,請參閱以下資源:
-
AnimatedBuilder API 頁面的 Performance optimizations 部分;
-
Opacity API 頁面的 Performance considerations for opacity animation 部分;
-
ListView API 頁面中 Child elements’ lifecycle,以及如何高效載入元素;
-
StatefulWidget
的 Performance considerations API 文件。