本文來源於公司內部的一次閃電分享,稍做潤色分享出來。主要討論 React 性能優化的主要方向和一些小技巧。若是你以爲能夠,請多點贊,鼓勵我寫出更精彩的文章🙏。html
文章首發於掘金react
React 渲染性能優化的三個方向,其實也適用於其餘軟件開發領域,這三個方向分別是:git
目錄github
首先從計算的量上下功夫,減小節點渲染的數量或者下降渲染的計算量能夠顯著的提升組件渲染性能。緩存
好比不要在渲染函數(render)中進行數組排序、數據轉換、訂閱事件、建立事件處理器等等. 渲染函數中不該該放置太多反作用安全
咱們團隊是重度的 styled-components
用戶,其實大部分狀況下咱們都不須要這個玩意,好比純靜態的樣式規則,以及須要重度性能優化的場景。除了性能問題,另一個困擾咱們的是它帶來的節點嵌套地獄(如上圖)。性能優化
因此咱們須要理性地選擇一些工具,好比使用原生的 CSS,減小 React 運行時的負擔.
通常沒必要要的節點嵌套都是濫用高階組件/RenderProps 致使的。因此仍是那句話‘只有在必要時才使用 xxx’。 有不少種方式來代替高階組件/RenderProps,例如優先使用 props、React Hooks
虛擬列表是常見的‘長列表'和'複雜組件樹'優化方式,它優化的本質就是減小渲染的節點。
虛擬列表只渲染當前視口可見元素:
虛擬列表渲染性能對比:
虛擬列表經常使用於如下組件場景:
相關組件方案:
擴展:
惰性渲染的初衷本質上和虛表同樣,也就是說咱們只在必要時纔去渲染對應的節點。
舉個典型的例子,咱們經常使用 Tab 組件,咱們沒有必要一開始就將全部 Tab 的 panel 都渲染出來,而是等到該 Tab 被激活時纔去惰性渲染。
還有不少場景會用到惰性渲染,例如樹形選擇器,模態彈窗,下拉列表,摺疊組件等等。
這裏就不舉具體的代碼例子了,留給讀者去思考.
如圖(圖片來源於THE PERFORMANCE OF STYLED REACT COMPONENTS), 這個圖片是17年的了,可是大抵的趨勢仍是這樣。
因此在樣式運行時性能方面大概能夠總結爲:CSS > 大部分CSS-in-js > inline style
減小沒必要要的從新渲染也是 React 組件性能優化的重要方向. 爲了不沒必要要的組件從新渲染須要在作到兩點:
shouldComponentUpdate
生命週期函數來比對 state 和 props, 肯定是否要從新渲染。對於函數組件可使用React.memo
包裝另外這些措施也能夠幫助你更容易地優化組件從新渲染:
① 若是一個組件的 props 太複雜通常意味着這個組件已經違背了‘單一職責’,首先應該嘗試對組件進行拆解.
② 另外複雜的 props 也會變得難以維護, 好比會影響shallowCompare
效率, 還會讓組件的變更變得難以預測和調試.
下面是一個典型的例子, 爲了判斷列表項是否處於激活狀態,這裏傳入了一個當前激活的 id:
這是一個很是糟糕的設計,一旦激活 id 變更,全部列表項都會從新刷新. 更好的解決辦法是使用相似actived
這樣的布爾值 prop. actived 如今只有兩種變更狀況,也就是說激活 id 的變更,最多隻有兩個組件須要從新渲染.
簡化的 props 更容易理解, 且能夠提升組件緩存的命中率
①避免使用箭頭函數形式的事件處理器, 例如:
<ComplexComponent onClick={evt => onClick(evt.id)} otherProps={values} />
假設 ComplexComponent 是一個複雜的 PureComponent, 這裏使用箭頭函數,其實每次渲染時都會建立一個新的事件處理器,這會致使 ComplexComponent 始終會被從新渲染.
更好的方式是使用實例方法:
class MyComponent extends Component { render() { <ComplexComponent onClick={this.handleClick} otherProps={values} />; } handleClick = () => { /*...*/ }; }
② 即便如今使用hooks
,我依然會使用useCallback
來包裝事件處理器,儘可能給下級組件暴露一個靜態的函數:
const handleClick = useCallback(() => { /*...*/ }, []); return <ComplexComponent onClick={handleClick} otherProps={values} />;
可是若是useCallback
依賴於不少狀態,你的useCallback
可能會變成這樣:
const handleClick = useCallback(() => { /*...*/ // 🤭 }, [foo, bar, baz, bazz, bazzzz]);
這種寫法實在讓人難以接受,這時候誰還管什麼函數式非函數式的。我是這樣處理的:
function useRefProps<T>(props: T) { const ref = useRef < T > props; // 每次渲染更新props useEffect(() => { ref.current = props; }); } function MyComp(props) { const propsRef = useRefProps(props); // 如今handleClick是始終不變的 const handleClick = useCallback(() => { const { foo, bar, baz, bazz, bazzzz } = propsRef.current; // do something }, []); }
③設計更方便處理的 Event Props. 有時候咱們會被逼的不得不使用箭頭函數來做爲事件處理器:
<List> {list.map(i => ( <Item key={i.id} onClick={() => handleDelete(i.id)} value={i.value} /> ))} </List>
上面的 onClick 是一個糟糕的實現,它沒有攜帶任何信息來標識事件來源,因此這裏只能使用閉包形式,更好的設計多是這樣的:
// onClick傳遞事件來源信息 const handleDelete = useCallback((id: string) => { /*刪除操做*/ }, []); return ( <List> {list.map(i => ( <Item key={i.id} id={i.id} onClick={handleDelete} value={i.value} /> ))} </List> );
若是是第三方組件或者 DOM 組件呢? 實在不行,看能不能傳遞data-*
屬性:
const handleDelete = useCallback(event => { const id = event.dataset.id; /*刪除操做*/ }, []); return ( <ul> {list.map(i => ( <li key={i.id} data-id={i.id} onClick={handleDelete} value={i.value} /> ))} </ul> );
不可變數據可讓狀態變得可預測,也讓 shouldComponentUpdate '淺比較'變得更可靠和高效. 筆者在React 組件設計實踐總結 04 - 組件的思惟介紹過不可變數據,有興趣讀者能夠看看.
相關的工具備Immutable.js、Immer、immutability-helper 以及 seamless-immutable。
不是全部狀態都應該放在組件的 state 中. 例如緩存數據。按照個人原則是:若是須要組件響應它的變更, 或者須要渲染到視圖中的數據才應該放到 state 中。這樣能夠避免沒必要要的數據變更致使組件從新渲染.
儘管 hooks 出來後,recompose 宣稱再也不更新了,但仍是不影響咱們使用 recompose 來控制shouldComponentUpdate
方法, 好比它提供瞭如下方法來精細控制應該比較哪些 props:
/* 至關於React.memo */ pure() /* 自定義比較 */ shouldUpdate(test: (props: Object, nextProps: Object) => boolean): HigherOrderComponent /* 只比較指定key */ onlyUpdateForKeys( propKeys: Array<string>): HigherOrderComponent
其實還能夠再擴展一下,好比omitUpdateForKeys
忽略比對某些 key.
所謂精細化渲染指的是只有一個數據來源致使組件從新渲染, 好比說 A 只依賴於 a 數據,那麼只有在 a 數據變更時才渲染 A, 其餘狀態變化不該該影響組件 A。
Vue 和 Mobx 宣稱本身性能好的一部分緣由是它們的'響應式系統', 它容許咱們定義一些‘響應式數據’,當這些響應數據變更時,依賴這些響應式數據視圖就會從新渲染. 來看看 Vue 官方是如何描述的:
大部分狀況下,響應式數據能夠實現視圖精細化的渲染, 但它仍是不能避免開發者寫出低效的程序. 本質上仍是由於組件違背‘單一職責’.
舉個例子,如今有一個 MyComponent 組件,依賴於 A、B、C 三個數據源,來構建一個 vdom 樹。如今的問題是什麼呢?如今只要 A、B、C 任意一個變更,那麼 MyComponent 整個就會從新渲染:
更好的作法是讓組件的職責更單一,精細化地依賴響應式數據,或者說對響應式數據進行‘隔離’. 以下圖, A、B、C 都抽取各自的組件中了,如今 A 變更只會渲染 A 組件自己,而不會影響父組件和 B、C 組件:
舉一個典型的例子,列表渲染:
import React from 'react'; import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; const initialList = []; for (let i = 0; i < 10; i++) { initialList.push({ id: i, name: `name-${i}`, value: 0 }); } const store = observable({ list: initialList, }); export const List = observer(() => { const list = store.list; console.log('List渲染'); return ( <div className="list-container"> <ul> {list.map((i, idx) => ( <div className="list-item" key={i.id}> {/* 假設這是一個複雜的組件 */} {console.log('render', i.id)} <span className="list-item-name">{i.name} </span> <span className="list-item-value">{i.value} </span> <button className="list-item-increment" onClick={() => { i.value++; console.log('遞增'); }} > 遞增 </button> <button className="list-item-increment" onClick={() => { if (idx < list.length - 1) { console.log('移位'); let t = list[idx]; list[idx] = list[idx + 1]; list[idx + 1] = t; } }} > 下移 </button> </div> ))} </ul> </div> ); });
上述的例子是存在性能問題的,單個 list-item 的遞增和移位都會致使整個列表的從新渲染:
緣由大概能猜出來吧? 對於 Vue 或者 Mobx 來講,一個組件的渲染函數就是一個依賴收集的上下文。上面 List 組件渲染函數內'訪問'了全部的列表項數據,那麼 Vue 或 Mobx 就會認爲你這個組件依賴於全部的列表項,這樣就致使,只要任意一個列表項的屬性值變更就會從新渲染整個 List 組件。
解決辦法也很簡單,就是將數據隔離抽取到單一職責的組件中。對於 Vue 或 Mobx 來講,越細粒度的組件,能夠收穫更高的性能優化效果:
export const ListItem = observer(props => { const { item, onShiftDown } = props; return ( <div className="list-item"> {console.log('render', item.id)} {/* 假設這是一個複雜的組件 */} <span className="list-item-name">{item.name} </span> <span className="list-item-value">{item.value} </span> <button className="list-item-increment" onClick={() => { item.value++; console.log('遞增'); }} > 遞增 </button> <button className="list-item-increment" onClick={() => onShiftDown(item)}> 下移 </button> </div> ); }); export const List = observer(() => { const list = store.list; const handleShiftDown = useCallback(item => { const idx = list.findIndex(i => i.id === item.id); if (idx !== -1 && idx < list.length - 1) { console.log('移位'); let t = list[idx]; list[idx] = list[idx + 1]; list[idx + 1] = t; } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); console.log('List 渲染'); return ( <div className="list-container"> <ul> {list.map((i, idx) => ( <ListItem key={i.id} item={i} onShiftDown={handleShiftDown} /> ))} </ul> </div> ); });
效果很明顯, list-item 遞增只會從新渲染自己; 而移位只會從新渲染 List, 由於列表項沒有變更, 因此下級 list-item 也不須要從新渲染:
其實 Context 的用法和響應式數據正好相反。筆者也看過很多濫用 Context API 的例子, 說到底仍是沒有處理好‘狀態的做用域問題’.
首先要理解 Context API 的更新特色,它是能夠穿透React.memo
或者shouldComponentUpdate
的比對的,也就是說,一旦 Context 的 Value 變更,全部依賴該 Context 的組件會所有 forceUpdate.
這個和 Mobx 和 Vue 的響應式系統不一樣,Context API 並不能細粒度地檢測哪些組件依賴哪些狀態,因此說上節提到的‘精細化渲染’組件模式,在 Context 這裏就成爲了‘反模式’.
總結一下使用 Context API 要遵循一下原則:
舉一個簡單的例子:
擴展:Context其實有個實驗性或者說非公開的選項observedBits
, 能夠用於控制ContextConsumer是否須要更新. 詳細能夠看這篇文章<ObservedBits: React Context的祕密功能>. 不過不推薦在實際項目中使用,並且這個API也比較難用,不如直接上mobx。
以下圖. 細粒度的 Context 訂閱會致使沒必要要的從新渲染, 因此這裏推薦粗粒度的訂閱. 好比在父級訂閱 Context,而後再經過 props 傳遞給下級。
另外程墨 Morgan 在避免 React Context 致使的重複渲染一文中也提到 ContextAPI 的一個陷阱:
<Context.Provider value={{ theme: this.state.theme, switchTheme: this.switchTheme }} > <div className="App"> <Header /> <Content /> </div> </Context.Provider>
上面的組件會在 state 變化時從新渲染整個組件樹,至於爲何留給讀者去思考。
因此咱們通常都不會裸露地使用 Context.Provider, 而是封裝爲獨立的 Provider 組件:
export function ThemeProvider(props) { const [theme, switchTheme] = useState(redTheme); return ( <Context.Provider value={{ theme, switchTheme }}> {props.children} </Context.Provider> ); } // 順便暴露useTheme, 讓外部必須直接使用Context export function useTheme() { return useContext(Context); }
如今 theme 變更就不會從新渲染整個組件樹,由於 props.children 由外部傳遞進來,並無發生變更。
其實上面的代碼還有另一個比較難發現的陷阱(官方文檔也有提到):
export function ThemeProvider(props) { const [theme, switchTheme] = useState(redTheme); return ( {/* 👇 💣這裏每一次渲染ThemeProvider, 都會建立一個新的value(即便theme和switchTheme沒有變更), 從而致使強制渲染全部依賴該Context的組件 */} <Context.Provider value={{ theme, switchTheme }}> {props.children} </Context.Provider> ); }
因此傳遞給 Context 的 value 最好作一下緩存:
export function ThemeProvider(props) { const [theme, switchTheme] = useState(redTheme); const value = useMemo(() => ({ theme, switchTheme }), [theme]); return <Context.Provider value={value}>{props.children}</Context.Provider>; }