React 應用主要的性能問題在於多餘的處理和組件的 DOM 比對。爲了避免這些性能陷阱,你應該儘可能的在shouldComponentUpdate 中返回 false 。
簡而言之,歸結於如下兩點:
文章中的示例是用 React + Redux 寫的。如果你用的是其它的數據流庫,原理是相通的但是實現會不同。
在文章中我沒有使用 immutability (不可變)庫,只是一些普通的 es6 和一點 es7。有些東西用不可變數據庫要簡單一點,但是我不準備在這裏討論這一部分內容。
我們來看一下 React 是如何渲染組件的。
在初始化渲染時,我們需要渲染整個應用
(綠色 = 已渲染節點)
每一個節點都被渲染 —— 這很贊!現在我們的應用呈現了我們的初始狀態。
我們想更新一部分數據。這些改變只和一個葉子節點相關
我們只想渲染通向葉子節點的關鍵路徑上的這幾個節點
如果你不告訴 React 別這樣做,它便會如此
(橘黃色 = 浪費的渲染)
哦,不!我們所有的節點都被重新渲染了。
React 的每一個組件都有一個 shouldComponentUpdate(nextProps, nextState) 函數。它的職責是當組件需要更新時返回true , 而組件不必更新時則返回 false 。返回 false 會導致組件的 render 函數不被調用。React 總是默認在shouldComponentUpdate 中返回 true,即便你沒有顯示地定義一個 shouldComponentUpdate 函數。
// 默認行爲
shouldComponentUpdate(nextProps, nextState) {
return true;
}
這就意味着在默認情況下,你每次更新你的頂層級的 props,整個應用的每一個組件都會渲染。這是一個主要的性能問題。
儘可能的在 shouldComponentUpdate 中返回 false 。
簡而言之:
理想情況下我們不希望在 shouldComponentUpdate 中做深等檢查,因爲這非常昂貴,尤其是在大規模和擁有大的數據結構的時候。
class Item extends React.component {
shouldComponentUpdate(nextProps) {
// 這很昂貴
return isDeepEqual(this.props, nextProps);
}
// ...
}
一個替代方法是_只要對象的值發生了變化,就改變對象的引用_。
const newValue = {
...oldValue
// 在這裏做你想要的修改
};
// 快速檢查 —— 只要檢查引用
newValue === oldValue; // false
// 如果你願意也可以用 Object.assign 語法
const newValue2 = Object.assign({}, oldValue);
newValue2 === oldValue; // false
在 Redux reducer 中使用這個技巧:
// 在這個 Redux reducer 中,我們將改變一個 item 的 description
export default (state, action) {
if(action.type === 'ITEM_DESCRIPTION_UPDATE') {
const { itemId, description } = action;
const items = state.items.map(item => {
// action 和這個 item 無關 —— 我們可以不作修改直接返回這個 item
if(item.id !== itemId) {
return item;
}
// 我們想改變這個 item
// 這會保留原本 item 的值,但
// 會返回一個更新過 description 的新對象
return {
...item,
description
};
});
return {
...state,
items
};
}
return state;
}
如果你採用這個方法,那你只需在 shouldComponentUpdate 函數中作引用檢查
// 超級快 —— 你所做的只是檢查引用!
shouldComponentUpdate(nextProps) {
return isObjectEqual(this.props, nextProps);
}
isObjectEqual 的一個實現示例
const isObjectEqual = (obj1, obj2) => {
if(!isObject(obj1) || !isObject(obj2)) {
return false;
}
// 引用是否相同
if(obj1 === obj2) {
return true;
}
// 它們包含的鍵名是否一致?
const item1Keys = Object.keys(obj1).sort();
const item2Keys = Object.keys(obj2).sort();
if(!isArrayEqual(item1Keys, item2Keys)) {
return false;
}
// 屬性所對應的每一個對象是否具有相同的引用?
return item2Keys.every(key => {
const value = obj1[key];
const nextValue = obj2[key];
if(value === nextValue) {
return true;
}
// 數組例外,再檢查一個層級的深度
return Array.isArray(value) &&
Array.isArray(nextValue) &&
isArrayEqual(value, nextValue);
});
};
const isArrayEqual = (array1 = [], array2 = []) => {
if(array1 === array2) {
return true;
}
// 檢查一個層級深度
return array1.length === array2.length &&
array1.every((item, index) => item === array2[index]);
};
先看一個_複雜_的 shouldComponentUpdate 示例
// 關注分離的數據結構(標準化數據)
const state = {
items: [
{
id: 5,
description: 'some really cool item'
}
]
// 表示用戶與系統交互的對象
interaction: {
selectedId: 5
}
};
如果這樣組織你的數據,會使得在 shouldComponentUpdate 中進行檢查變得_困難_
import React, { Component, PropTypes } from 'react'
class List extends Component {
propTypes = {
items: PropTypes.array.isRequired,
iteraction: PropTypes.object.isRequired
}
shouldComponentUpdate (nextProps) {
// items 中的元素是否發生了改變?
if(!isArrayEqual(this.props.items, nextProps.items)) {
return true;
}
// 從這裏開始事情會變的很恐怖
// 如果 interaction 沒有變化,那可以返回 false (真棒!)
if(isObjectEqual(this.props.interaction, nextProps.interaction)) {
return false;
}
// 如果代碼運行到這裏,我們知道:
// 1. items 沒有變化
// 2. interaction 變了
// 我們需要 interaction 的變化是否與我們相干
const wasItemSelected = this.props.items.any(item => {
return item.id === this.props.interaction.selectedId
})
const isItemSelected = nextProps.items.any(item => {
return item.id === nextProps.interaction.selectedId
})
// 如果發生了改變就返回 true
// 如果沒有發生變化就返回 false
return wasItemSelected !== isItemSelected;
}
render() {
<div>
{this.props.items.map(item => {
const isSelected = this.props.interaction.selectedId === item.id;
return (<Item item={item} isSelected={isSelected} />);
})}
</div>
}
}
你可以看出一個非常簡單的數據對應的 shouldComponentUpdate 即龐大又複雜。這是因爲它需要知道數據的結構以及它們之間的關聯。shouldComponentUpdate 函數的複雜度和體積只隨着你的數據結構增長。這_很容易_導致兩點錯誤:
爲什麼要讓事情變得這麼複雜?你只想讓這些檢查變得簡單一點,以至於你根本就不必考慮它們。
通常而言,應用都要推廣鬆耦合(組件對其它的組件知道的越少越好)。父組件應該儘量避免知曉其子組件的工作原理。這就允許你改變子組件的行爲而無須讓父級知曉這些變化(假設 PropsTypes 保持不變)。它還允許子組件獨立運轉,而不必讓父級緊密的控制其行爲。
通過壓平(合併)你的數據結構,你可以重新使用非常簡單的引用檢查來看是否有什麼發生了變化。
const state = {
items: [
{
id: 5,
description: 'some really cool item',
// interaction 現在存在於 item 的內部
interaction: {
isSelected: true
}
}
}
};
這樣組織你的數據使得在 shouldComponentUpdate 中做檢查變得_簡單_
import React, {Component, PropTypes} from 'react'
class List extends Component {
propTypes = {
items: PropTypes.array.isRequired
}
shouldComponentUpdate(nextProps) {
// so easy,麻麻再也不用擔心我的更新檢查了
return isObjectEqual(this.props, nextProps);
}
render() {
<div>
{this.props.items.map(item => {
return (
<Item item={item}
isSelected={item.interaction.isSelected} />)
})}
</div>
}
}
如果你想要更新 interaction 你就改變整個對象的引用
// redux reducer
export default (state, action) => {
if(action.type === 'ITEM_SELECT') {
const { itemId } = action;
const items = state.items.map(item => {
if(item.id !== itemId) {
return item;
}
// 改變整個對象的引用
return {
...item,
interaction: {
isSelected: true
}
}
})
return {
...state,
items
};
}
return state;
};
一個創建動態 props 的例子
class Foo extends React.Component {
render() {
const {items} = this.props;
// 這個對象每次都有一個新的引用
const newData = { hello: 'world' };
return <Item name={name} data={newData} />
}
}
class Item extends React.Component {
// 即便前後兩個對象的值相同,檢查也總會返回true,因爲 `data` 每次都會得到一個新的引用
shouldComponentUpdate(nextProps) {
return isObjectEqual(this.props, nextProps);
}
}
通常我們不會在組件中創建一個新的 props 把它傳下來 。但是,這在循環中更爲常見
class List exntends React.Component {
render() {
const {items} = this.props;
<div>
{items.map((item, index) => {
// 這個對象每次都會獲得一個新引用
const newData = {
hello: 'world',
isFirst: index === 0
};
return <Item name={name} data={newData} />
})}
</div>
}
}
這在創建函數時很常見
import myActionCreator from './my-action-creator';
class List extends React.Component {
render() {
const {items, dispatch} = this.props;
<div>
{items.map(item => {
// 這個函數的引用每次都會變
const callback = () => {
dispatch(myActionCreator(item));
}
return <Item name={name} onUpdate={callback} />
})}
</div>
}
}
改善你的數據模型,這樣你就可以直接把 props 傳下來
eg:
const bool1 = true;
const bool2 = true;
bool1 === bool2; // true
const string1 = 'hello';
const string2 = 'hello';
string1 === string2; // true
如果你實在需要傳遞動態對象,那就把它當作字符串傳下來,再在子級進行解構
render() {
const {items} = this.props;
<div>
{items.map(item => {
// 每次獲得新引用
const bad = {
id: item.id,
type: item.type
};
// 相同的值可以滿足嚴格的全等 '==='
const good = `${item.id}::${item.type}`;
return <Item identifier={good} />
})}
</div>
}
方案4 的示例
// 引入另外一層 'ListItem'
<List>
<ListItem> // 你可以在這裏創建正確的 this 綁定
<Item />
</ListItem>
</List>
class ListItem extends React.Component {
// 這樣總能得到正確的 this 綁定,因爲它綁定在了實例上
// 感謝 es7!
const callback = () => {
dispatch(doSomething());
}
render() {
return <Item callback={this.callback} item={this.props.item} />
}
}
以上列出來的所有規則和技巧都是通過使用性能測量工具發現的。使用工具可以幫助你發現你的應用的具體性能問題所在。
這一個相當簡單:
一個比較好的做法是使用 Redux 中間件:
export default store => next => action => {
console.time(action.type)
// `next` 是一個函數,它接收 'action' 並把它發送到 ‘reducers' 進行處理
// 這會導致你應有的一次重渲
const result = next(action);
// 渲染用了多久?
console.timeEnd(action.type);
return result;
};
用這個方法可以記錄你應用的每一個 action 和它引起的渲染所花費的時間。你可以快速知道哪些 action 渲染時間最長,這樣當你解決性能問題時就可以從那裏着手。拿到時間值還能幫助你判斷你所做的性能優化是否奏效了。
這個工具的思路和 console.time 是一致的,只不過用的是 React 的性能工具:
Redux 中間件示例:
import Perf from 'react-addons-perf';
export default store => next => action => {
const key = `performance:${action.type}`;
Perf.start();
// 拿到新的 state 重渲應用
const result = next(action);
Perf.stop();
console.group(key);
console.info('wasted');
Perf.printWasted();
// 你可以在這裏打印任何你感興趣的 Perf 測量值
console.groupEnd(key);
return result;
};
與 console.time 方法類似,它能讓你看到你每一個 action 的性能指標。更多關於 React 性能 addon 的信息請點擊這裏
CPU 分析器火焰圖表在尋找你的應用程序的性能問題時也能發揮作用。
在做性能分析時,火焰圖表會展示出每一毫秒你的代碼的 Javascript 堆棧的狀態。在記錄的時候,你就可以確切地知道任意時間點執行的是哪一個函數,它執行了多久,又是誰調用了它。—— Mozilla