效能最佳實踐
如何確保您的 Flutter 應用具有高效能。
通常情況下,Flutter 應用預設就具備高效能,因此您只需避免常見的陷阱即可獲得卓越的效能。這些最佳實踐建議將幫助您編寫出效能表現儘可能優異的 Flutter 應用。
如何設計一個 Flutter 應用以最高效地渲染場景?特別是,如何確保框架生成的繪製程式碼儘可能高效?一些渲染和佈局操作已知較慢,但並不總能避免。應遵循以下指南,謹慎使用它們。
最小化高開銷操作
#有些操作比其他操作開銷更大,這意味著它們消耗更多的資源。顯然,您只想在必要時使用這些操作。您設計和實現應用 UI 的方式對執行效率有很大影響。
控制 build() 開銷
#設計 UI 時請牢記以下幾點:
- 避免在
build()方法中進行重複且昂貴的操作,因為當祖先元件重建時,build()可能會被頻繁呼叫。 - 避免使用擁有龐大
build()函式的單一超大元件。應基於封裝原則以及它們的變化頻率將它們拆分為不同的元件。- 當在一個
State物件上呼叫setState()時,所有子代元件都會重建。因此,請將setState()呼叫定位到實際需要更改 UI 的子樹部分。如果更改僅限於子樹的一小部分,請避免在樹的高層呼叫setState()。 - 當重新遇到與前一幀相同的子元件例項時,重建所有子代的遍歷將停止。此技術被框架廣泛用於最佳化動畫,即動畫不會影響子樹的情況。請參閱
TransitionBuilder模式和SlideTransition的原始碼,它利用此原理來避免在動畫過程中重建其子代。(“相同例項”透過operator ==進行評估,但請參閱本頁末尾的“常見陷阱”部分,獲取關於何時避免重寫operator ==的建議。) - 儘可能在元件上使用
const建構函式,因為它們允許 Flutter 跳過大部分重建工作。若要自動提醒您在可能的情況下使用const,請啟用flutter_lints包中的推薦 lints。有關更多資訊,請檢視flutter_lints遷移指南。 - 若要建立可複用的 UI 片段,請優先使用
StatelessWidget而不是函式。
- 當在一個
有關更多資訊,請檢視:
-
效能考量,屬於
StatefulWidgetAPI 文件的一部分。 -
Widgets vs helper methods,這是 Flutter 官方 YouTube 頻道釋出的一個影片,解釋了為什麼元件(尤其是帶有
const建構函式的元件)比函式效能更高。
使用 StringBuffer 高效構建字串
#當需要從多個部分構建字串時,特別是在迴圈內部,使用 + 運算子可能效率低下,因為它在每次拼接時都會建立一個新的 String 物件。更好的方法是使用 StringBuffer,它會收集所有字串,僅在呼叫 toString() 時進行一次性拼接。
謹慎使用 saveLayer()
#一些 Flutter 程式碼使用 saveLayer()(一種昂貴的操作)來實現 UI 中的各種視覺效果。即使您的程式碼沒有顯式呼叫 saveLayer(),您使用的其他元件或包也可能在後臺呼叫它。也許您的應用呼叫 saveLayer() 的頻率超出了必要程度;過多的 saveLayer() 呼叫會導致卡頓(jank)。
為什麼 saveLayer 昂貴?
#呼叫 saveLayer() 會分配一個離屏緩衝區,將內容繪製到該緩衝區可能會觸發渲染目標切換。GPU 的工作方式類似於消防水帶,而渲染目標切換會強制 GPU 臨時重定向流,然後再導回。在移動端 GPU 上,這對渲染吞吐量的干擾尤為嚴重。
何時需要 saveLayer?
#在執行時,如果您需要動態顯示來自伺服器的各種形狀(例如),每個形狀都帶有一定的透明度,且它們可能(或可能不)重疊,那麼您幾乎必須使用 saveLayer()。
除錯 saveLayer 呼叫
#如何判斷您的應用呼叫 saveLayer() 的頻率(無論是直接還是間接)?saveLayer() 方法會在 DevTools 時間軸上觸發一個事件;透過在 DevTools 效能檢視中勾選 PerformanceOverlayLayer.checkerboardOffscreenLayers 開關,可以瞭解您的場景何時使用了 saveLayer。
最小化 saveLayer 呼叫
#您可以避免呼叫 saveLayer 嗎?這可能需要重新思考如何建立視覺效果。
-
如果這些呼叫來自您自己的程式碼,能否減少或消除它們?例如,也許您的 UI 重疊了兩個都有非零透明度的形狀。
- 如果它們總是以相同的方式、相同的透明度重疊,您可以預先計算這個重疊的半透明物件的樣子並將其快取,然後使用快取結果來代替呼叫
saveLayer()。這適用於任何您可以預先計算的靜態形狀。 - 您能否重構繪圖邏輯以完全避免重疊?
- 如果它們總是以相同的方式、相同的透明度重疊,您可以預先計算這個重疊的半透明物件的樣子並將其快取,然後使用快取結果來代替呼叫
-
如果這些呼叫來自您不擁有的包,請聯絡包所有者並詢問這些呼叫為何是必要的。它們可以被減少或消除嗎?如果不能,您可能需要尋找另一個包,或者編寫自己的實現。
其他可能觸發 saveLayer() 且開銷較大的元件
ShaderMask-
ColorFilter -
Chip— 如果disabledColorAlpha != 0xff,可能會觸發saveLayer()呼叫。 -
Text— 如果存在overflowShader,可能會觸發saveLayer()呼叫。
最小化不透明度和裁剪的使用
#不透明度(Opacity)和裁剪(Clipping)也是昂貴的操作。以下是一些有用的建議:
- 僅在必要時使用
Opacity元件。請參閱OpacityAPI 頁面中的 透明影像 部分,瞭解如何將不透明度直接應用於影像,這比使用Opacity元件更快。 - 與其將簡單的形狀或文字包裹在
Opacity元件中,不如直接用半透明顏色繪製它們,這樣通常更快。(但這僅在被繪製的形狀中沒有重疊部分時有效。) - 若要實現影像的淡入效果,請考慮使用
FadeInImage元件,它使用 GPU 的片元著色器應用漸變不透明度。有關更多資訊,請檢視Opacity文件。 -
裁剪(Clipping)不會呼叫
saveLayer()(除非顯式指定為Clip.antiAliasWithSaveLayer),因此這些操作不像Opacity那樣昂貴,但裁剪仍然是有代價的,請謹慎使用。預設情況下,裁剪是停用的(Clip.none),因此您必須在需要時顯式啟用它。 - 若要建立圓角矩形,請考慮使用許多元件類提供的
borderRadius屬性,而不是應用裁剪矩形。
謹慎實現網格和列表
#您的網格和列表的實現方式可能會導致應用的效能問題。本節描述了建立網格和列表時的一個重要最佳實踐,以及如何確定您的應用是否使用了過多的佈局傳遞。
保持懶載入!
#構建大型網格或列表時,請使用帶有回撥的懶載入構建器方法。這能確保在啟動時只構建螢幕可見的部分。
有關更多資訊和示例,請檢視:
- 使用長列表
- 建立一次載入一頁的 ListView,由 AbdulRahman AlHamali 撰寫的社群文章。
-
ListView.builderAPI
避免內在測量(Intrinsics)
#有關內在測量傳遞如何導致網格和列表問題的資訊,請參閱下一節。
最小化由內在(intrinsic)操作引起的佈局傳遞
#如果您進行過大量 Flutter 開發,您可能熟悉在建立 UI 時佈局和約束是如何工作的。您甚至可能記住了 Flutter 的基本佈局規則:約束向下傳遞。尺寸向上傳遞。父元件決定位置。
對於某些元件(尤其是網格和列表),佈局過程可能很昂貴。Flutter 努力對元件執行一次佈局傳遞,但有時需要第二次傳遞(稱為內在測量傳遞/intrinsic pass),這會降低效能。
什麼是內在測量傳遞?
#內在測量傳遞發生在例如您希望所有單元格具有最大或最小單元格的大小(或者需要輪詢所有單元格的其他類似計算)時。
例如,考慮一個大型的 Card 網格。網格應該具有統一大小的單元格,因此佈局程式碼執行一次傳遞,從網格根部(在元件樹中)開始,要求網格中的每個卡片(不僅僅是可見卡片)返回其內在尺寸——即假設沒有約束時元件偏好的大小。利用這些資訊,框架確定統一的單元格大小,並第二次訪問所有網格單元,告知每張卡片要使用的大小。
除錯內在測量傳遞
#要確定是否存在過多的內在測量傳遞,請在 DevTools 中啟用 跟蹤佈局選項 (Track layouts option)(預設停用),並檢視應用的 堆疊追蹤,以瞭解執行了多少次佈局傳遞。啟用跟蹤後,內在測量時間軸事件將標記為 '$runtimeType intrinsics'。
避免內在測量傳遞
#您有幾種選擇來避免內在測量傳遞:
- 預先將單元格設定為固定大小。
- 選擇特定的單元格作為“錨點”單元格——所有單元格都將相對於該單元格進行調整大小。編寫一個自定義
RenderObject,它首先定位子錨點,然後圍繞它佈局其他子元件。
要深入瞭解佈局的工作原理,請檢視 Flutter 架構概覽 中的 佈局和渲染 部分。
在 16ms 內構建並顯示幀
#由於構建和渲染有兩個獨立的執行緒,因此在 60Hz 的顯示器上,您有 16ms 用於構建,16ms 用於渲染。如果延遲是一個問題,請在 16ms 或更短的時間內構建並顯示一幀。請注意,這意味著在 8ms 或更短的時間內構建,並在 8ms 或更短的時間內渲染,總計 16ms 或更短。
如果在 Profile 模式下您的幀渲染時間總計遠低於 16ms,即使存在一些效能陷阱,您可能也不必擔心效能,但仍應儘量以最快的速度構建和渲染幀。為什麼?
- 將幀渲染時間降至 16ms 以下可能不會帶來明顯的視覺差異,但它改善了電池續航和熱問題。
- 它可能在您的裝置上執行良好,但請考慮您所針對的最底層裝置的效能。
- 隨著 120fps 裝置變得越來越普及,您需要將幀渲染時間(總計)控制在 8ms 以下,以提供最流暢的體驗。
如果您想知道為什麼 60fps 會帶來流暢的視覺體驗,請觀看影片 Why 60fps?
常見陷阱
#如果您需要調整應用的效能,或者 UI 的流暢度不如預期,DevTools 效能檢視 可以為您提供幫助!
此外,您的 IDE 的 Flutter 外掛也很有用。在 Flutter Performance 視窗中,啟用 Show widget rebuild information 複選框。此功能可幫助您檢測幀渲染和顯示時間是否超過 16ms。在可能的情況下,外掛會提供指向相關提示的連結。
以下行為可能會對應用的效能產生負面影響:
-
避免使用
Opacity元件,尤其是在動畫中。請改用AnimatedOpacity或FadeInImage。有關更多資訊,請檢視 不透明度動畫的效能考量。 -
使用
AnimatedBuilder時,避免在 builder 函式中放置與動畫無關的元件子樹。該子樹會在動畫的每一幀重新構建。相反,應構建該部分子樹一次,並將其作為子元件傳遞給AnimatedBuilder。有關更多資訊,請檢視 效能最佳化。 -
避免在動畫中使用裁剪。如果可能,在動畫播放前預先對影像進行裁剪。
-
避免在大多數子元件在螢幕上不可見時使用帶有具體
List子元件的建構函式(如Column()或ListView()),以避免構建開銷。 -
避免在
Widget物件上重寫operator ==。雖然看起來可以透過避免不必要的重建來提供幫助,但實際上由於會導致 O(N²) 行為,它會損害效能。此規則的唯一例外是葉子元件(沒有子元件的元件),在比較元件屬性可能比重建元件更高效且元件配置很少更改的情況下。即使在這種情況下,通常也更傾向於依賴元件快取,因為即使是一個operator ==的重寫也可能導致全域性的效能下降,因為編譯器無法再假設該呼叫總是靜態的。
資源
#有關更多效能資訊,請檢視以下資源:
- 效能最佳化 (AnimatedBuilder API 頁面)
- 不透明度動畫的效能考量 (Opacity API 頁面)
- 子元件的生命週期 以及如何高效載入它們 (ListView API 頁面)
-
StatefulWidget的 效能考量 - 最佳化 Flutter Web 載入速度的最佳實踐