跳到主要內容

效能總覽

使用 React Native 而非基於 WebView 的工具的一個主要原因是,它可以達到至少每秒 60 幀的速度,並為您的應用程式提供原生外觀和體驗。在可行的情況下,我們的目標是讓 React Native 自動處理最佳化,讓您可以專注於您的應用程式,而無需擔心效能。然而,在某些領域,我們尚未完全達到該水準,而在另一些領域,React Native(類似於直接編寫原生程式碼)無法為您確定最佳的最佳化方法。在這種情況下,手動介入就變得必要。我們努力預設提供如奶油般順暢的 UI 效能,但可能會有無法實現的情況。

本指南旨在教您一些基礎知識,以幫助您排除效能問題,並討論常見的問題來源及其建議的解決方案

您需要了解的幀

您的祖父母那一代將電影稱為「活動影像」是有原因的:影片中逼真的動作是一種幻覺,它是由以一致的速度快速切換靜態圖像所產生的。我們將這些圖像中的每一個都稱為幀。每秒顯示的幀數直接影響影片(或使用者介面)看起來有多流暢和逼真。iOS 裝置至少以每秒 60 幀的速度顯示,這讓您和 UI 系統最多有 16.67 毫秒的時間來完成生成靜態圖像(幀)所需的所有工作,使用者將在該時間間隔內在螢幕上看到該圖像。如果您無法在分配的時間段內完成生成該幀所需的工作,那麼您將「丟失一幀」,並且 UI 將顯得沒有反應。

現在為了讓事情更複雜一點,請打開您應用程式中的開發人員選單,並切換 Show Perf Monitor。您會注意到有兩種不同的幀率。

JS 幀率 (JavaScript 執行緒)

對於大多數 React Native 應用程式來說,您的商業邏輯將在 JavaScript 執行緒上執行。這是您的 React 應用程式所在的位置,API 呼叫、觸控事件處理等等都在這裡進行... 對原生支援視圖的更新會被批次處理,並在事件迴圈的每次迭代結束時,在幀截止時間之前(如果一切順利)發送到原生端。如果 JavaScript 執行緒在一幀內沒有反應,它將被視為丟失一幀。例如,如果您在複雜應用程式的根組件上呼叫 this.setState,並且導致重新渲染計算密集型組件子樹,則可以想像這可能需要 200 毫秒並導致丟失 12 幀。任何由 JavaScript 控制的動畫在那段時間內都會顯得凍結。如果任何事情花費超過 100 毫秒,使用者就會感受到。

這種情況經常發生在 Navigator 過渡期間:當您推送新路由時,JavaScript 執行緒需要渲染場景所需的所有組件,以便將正確的命令發送到原生端以建立支援視圖。這裡完成的工作通常需要幾幀並導致卡頓,因為過渡由 JavaScript 執行緒控制。有時組件會在 componentDidMount 上執行額外的工作,這可能會導致過渡中出現第二次停頓。

另一個例子是回應觸控:如果您在 JavaScript 執行緒上跨多幀執行工作,您可能會注意到回應 TouchableOpacity 時出現延遲。這是因為 JavaScript 執行緒很忙,無法處理從主執行緒發送過來的原始觸控事件。因此,TouchableOpacity 無法對觸控事件做出反應,也無法命令原生視圖調整其不透明度。

UI 幀率 (主執行緒)

許多人注意到 NavigatorIOS 的效能比 Navigator 更好。原因在於過渡的動畫完全在主執行緒上完成,因此它們不會被 JavaScript 執行緒上的幀丟失所中斷。

同樣地,當 JavaScript 執行緒被鎖定時,您可以愉快地上下滾動 ScrollView,因為 ScrollView 位於主執行緒上。滾動事件會被分派到 JS 執行緒,但它們的接收對於滾動的發生並非必要。

常見的效能問題來源

在開發模式下執行 (dev=true)

在開發模式下執行時,JavaScript 執行緒效能會大幅降低。這是不可避免的:在執行時需要完成更多工作,以便為您提供良好的警告和錯誤訊息。始終確保在發布版本中測試效能。

使用 console.log 語句

當運行捆綁的應用程式時,這些語句可能會在 JavaScript 執行緒中造成很大的瓶頸。這包括來自除錯函式庫(例如 redux-logger)的呼叫,因此請確保在捆綁之前將它們移除。您也可以使用這個 babel 外掛程式來移除所有 console.* 呼叫。您需要先使用 npm i babel-plugin-transform-remove-console --save-dev 安裝它,然後像這樣編輯專案目錄下的 .babelrc 檔案

json
{
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}

這將自動移除您專案發布(生產)版本中的所有 console.* 呼叫。

即使您的專案中沒有進行任何 console.* 呼叫,也建議使用此外掛程式。第三方函式庫也可能呼叫它們。

ListView 初始渲染速度太慢或大型列表的滾動效能不佳

請改用新的 FlatListSectionList 組件。除了簡化 API 之外,新的列表組件還具有顯著的效能提升,其中主要的一項是對於任何數量的行,記憶體使用量幾乎恆定。

如果您的 FlatList 渲染速度很慢,請確保您已實作 getItemLayout,透過跳過渲染項目的測量來最佳化渲染速度。

當重新渲染幾乎沒有變化的視圖時,JS FPS 會驟降

如果您正在使用 ListView,則必須提供 rowHasChanged 函數,它可以透過快速確定是否需要重新渲染行來減少大量工作。如果您正在使用不可變資料結構,則這只需要是一個參考相等性檢查。

同樣地,您可以實作 shouldComponentUpdate 並指出您希望組件重新渲染的確切條件。如果您編寫純組件(其中 render 函數的傳回值完全取決於 props 和 state),您可以利用 PureComponent 為您執行此操作。再次強調,不可變資料結構對於保持快速很有用——如果您必須對大型物件列表進行深度比較,那麼重新渲染整個組件可能會更快,而且肯定需要更少的程式碼。

由於同時在 JavaScript 執行緒上執行大量工作而導致 JS 執行緒 FPS 下降

「Navigator 過渡緩慢」是這種情況最常見的表現形式,但還有其他時候也會發生這種情況。使用 InteractionManager 可能是一個好方法,但如果延遲動畫期間的工作的使用者體驗成本太高,那麼您可能需要考慮 LayoutAnimation。

Animated API 目前在 JavaScript 執行緒上按需計算每個關鍵幀,除非您設定 useNativeDriver: true,而 LayoutAnimation 利用 Core Animation,並且不受 JS 執行緒和主執行緒幀丟失的影響。

我使用過這種情況的一個例子是在初始化時,以及可能接收多個網路請求的回應、渲染 modal 的內容以及更新從中打開 modal 的視圖時,在 modal 中製作動畫(從頂部向下滑動並淡入半透明覆蓋層)。有關如何使用 LayoutAnimation 的更多資訊,請參閱動畫指南。

注意事項

  • LayoutAnimation 僅適用於一次性動畫(「靜態」動畫)——如果必須是可中斷的,您將需要使用 Animated

在螢幕上移動視圖(滾動、平移、旋轉)會降低 UI 執行緒 FPS

當您將具有透明背景的文字放置在圖像之上,或任何其他需要在每幀上重新繪製視圖的情況下,尤其如此,在這種情況下,需要 alpha 混合。您會發現啟用 shouldRasterizeIOSrenderToHardwareTextureAndroid 可以顯著幫助解決這個問題。

小心不要過度使用此功能,否則您的記憶體使用量可能會暴增。在使用這些 props 時,請分析您的效能和記憶體使用量。如果您不打算再移動視圖,請關閉此屬性。

動畫化圖像的大小會降低 UI 執行緒 FPS

在 iOS 上,每次您調整 Image 組件的寬度或高度時,都會從原始圖像重新裁剪和縮放。這可能會非常耗費資源,尤其是對於大型圖像而言。相反地,請使用 transform: [{scale}] 樣式屬性來動畫化大小。您可能會這樣做的一個例子是當您點擊圖像並將其放大到全螢幕時。

我的 TouchableX 視圖反應不是很靈敏

有時,如果我們在調整回應觸控的組件的不透明度或亮度的同一幀中執行操作,我們將在 onPress 函數傳回後才會看到該效果。如果 onPress 執行 setState 導致大量工作和丟失幾幀,則可能會發生這種情況。對此的解決方案是將 onPress 處理程序中的任何操作包裝在 requestAnimationFrame

tsx
handleOnPress() {
requestAnimationFrame(() => {
this.doExpensiveAction();
});
}

Navigator 過渡緩慢

如上所述,Navigator 動畫由 JavaScript 執行緒控制。想像一下「從右側推入」的場景過渡:每幀,新場景從右向左移動,從螢幕外開始(假設 x 偏移量為 320),最終當場景位於 x 偏移量為 0 時穩定下來。在此過渡期間的每一幀,JavaScript 執行緒都需要向主執行緒發送新的 x 偏移量。如果 JavaScript 執行緒被鎖定,它就無法執行此操作,因此該幀上不會發生更新,並且動畫會停頓。

對此的一個解決方案是允許將基於 JavaScript 的動畫卸載到主執行緒。如果我們使用這種方法執行與上述範例相同的操作,我們可能會在開始過渡時計算新場景的所有 x 偏移量列表,並將它們發送到主執行緒以最佳化方式執行。現在 JavaScript 執行緒已從此責任中解放出來,即使它在渲染場景時丟失了幾幀也沒什麼大不了的——您可能甚至不會注意到,因為您會被漂亮的過渡所吸引。

解決這個問題是新的 React Navigation 函式庫背後的主要目標之一。React Navigation 中的視圖使用原生組件和 Animated 函式庫來提供至少 60 FPS 的動畫,這些動畫在原生執行緒上運行。