react 性能優化
shouldComponentUpdate
對於對視圖沒有關聯,但可能更新的數據,可以用shouldComponentUpdate
過濾掉,減少無所謂的更新
返回一個Boolean
值,true
則更新,false
則不更新,默認爲true
shouldComponentUpdate(nextProps,nextState){
if(this.props.xxx !== nextProps.xxx){
return true;
}else {
return false;
}
}
PureComponent
對組件的state
和props
僅淺對比更新,即傳入的是對象,單純的更改對象屬性是無法觸發更新,需要更改對象的引用(對於非基本類型的對比,對比的是引用的地址塊)
可以配合immutable.js
使用,以達到對象更改屬性時更新的效果。
Redux配合immutable.js使用
Immutable Data 就是一旦創建,就不能再被更改的數據。對 Immutable 對象的任何修改或添加刪除操作都會返回一個新的 Immutable 對象。Immutable 實現的原理是 Persistent Data Structure(持久化數據結構)。
memo
React.memo
爲高階組件。它與React.PureComponent
非常相似,但它適用於函數組件,但不適用於class
組件。
如果你的函數組件在給定相同props
的情況下渲染相同的結果,那麼你可以通過將其包裝在React.memo
中調用,以此通過記憶組件渲染結果的方式來提高組件的性能表現。這意味着在這種情況下,React 將跳過渲染組件的操作並直接複用最近一次渲染的結果。
原理同PureComponent
,但僅對props
僅做淺對比。
如果要手動添加對比過程,可以在第二個參數傳入自定義的對比函數。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/* 如果函數返回 true,就會跳過更新。*/
if(nextProps.xxx !== prevProps.xxx){
return false;
}else {
return true;
}
}
export default React.memo(MyComponent, areEqual);
useMemo
useMemo
允許你通過「記住」上一次計算結果的方式在多次渲染的之間緩存計算結果。
與memo
的區別是useMemo
作用於計算以及組件。
- 第一個參數是主體內容
- 第二個參數是數組,傳入要記住的參數
作用與function
:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
作用於組件:
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}
注意這種方式在循環中是無效的,因爲Hook
調用 不能 被放在循環中。但你可以爲列表項抽取一個單獨的組件,並在其中調用useMemo
。
useCallback
useCallback
和useMemo
有些相似。它接收一個內聯函數和一個數組,它返回的是一個記憶化版本的函數。
useCallback(fn, deps)
相當於 useMemo(() => fn, deps)
。
lazy & Suspense
React 集成了lazy
和Suspense
。lazy
會進行代碼分割,它能讓你像渲染常規組件一樣處理動態引入(的組件)。。fallback
屬性接受任何在組件加載過程中你想展示的React
元素。你可以將Suspense
組件置於懶加載組件之上的任何位置。你甚至可以用一個Suspense
組件包裹多個懶加載組件。
👇對路由組件進行代碼分割、懶加載。
import React, { lazy, Suspense } from "react";
import { renderRoutes } from "react-router-config";
import { HashRouter, Redirect } from "react-router-dom";
const Page1Comp = lazy(() => import("../Page1"));
const Page1 = props => {
return (
<Suspense fallback={<p>loading</p>}>
<Page1Comp {...props} />
</Suspense>
);
};
const Page2Comp = lazy(() => import("../Page2"));
const Page2 = props => {
return (
<Suspense fallback={<p>loading</p>}>
<Page2Comp {...props} />
</Suspense>
);
};
const routes = [
{ path: "/", exact: true, render: () => <Redirect to={"/page1"} /> },
{ path: "/page1", component: Page1 },
{ path: "/page2", component: Page2 }
];
function App() {
return (
<HashRouter>
<div className="App">
<h1>Hello</h1>
{renderRoutes(routes)}
</div>
</HashRouter>
);
}
export default App;
Fragment
在jsx裏面,會有很多無意義但必須有的的嵌套。可以使用Fragment代替減少無謂的嵌套。
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
// or
render() {
return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
);
}
不適用內聯函數
<input type="button" onClick={(e) => { this.setState({inputValue: e.target.value}) }} />
👆上面的函數創建了內聯函數。每次調用 render 函數時都會創建一個函數的新實例,render 函數會將該函數的新實例綁定到該按鈕。
改成如下👇:
export default class InlineFunctionComponent extends Component {
setNewStateData = (event) => {
this.setState({
inputValue: e.target.value
})
}
render() {
return <input type="button" onClick={this.setNewStateData} />
}
}
函數綁定在constructor完成
<input type="button" value="Click" onClick={this.handleButtonClick.bind(this)} />
每次調用 render 函數時都會創建並使用綁定到當前上下文的新函數,但在每次渲染時使用已存在的函數效率更高。優化方案如下:
constructor() {
this.handleButtonClick = this.handleButtonClick.bind(this)
}
箭頭函數與constructor函數綁定
當我們添加箭頭函數時,該函數被添加爲對象實例,而不是類的原型屬性。這意味着如果我們多次複用組件,那麼在組件外創建的每個對象中都會有這些函數的多個實例。
每個組件都會有這些函數的一份實例,影響了可複用性。此外因爲它是對象屬性而不是原型屬性,所以這些函數在繼承鏈中不可用。
因此箭頭函數確實有其缺點。實現這些函數的最佳方法是在構造函數中綁定函數。
節流和防抖
節流函數
節流是將多次執行變成每隔一段時間執行。
/**
* underscore 節流函數,返回函數連續調用時,func 執行頻率限定爲 次 / wait
*
* @param {function} func 回調函數
* @param {number} wait 表示時間窗口的間隔
* @param {object} options 如果想忽略開始函數的的調用,傳入{leading: false}。
* 如果想忽略結尾函數的調用,傳入{trailing: false}
* 兩者不能共存,否則函數不能執行
* @return {function} 返回客戶調用函數
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的時間戳
var previous = 0;
// 如果 options 沒傳則設爲空對象
if (!options) options = {};
// 定時器回調函數
var later = function() {
// 如果設置了 leading,就將 previous 設爲 0
// 用於下面函數的第一個 if 判斷
previous = options.leading === false ? 0 : _.now();
// 置空一是爲了防止內存泄漏,二是爲了下面的定時器判斷
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 獲得當前時間戳
var now = _.now();
// 首次進入前者肯定爲 true
// 如果需要第一次不執行函數
// 就將上次時間戳設爲當前的
// 這樣在接下來計算 remaining 的值時會大於0
if (!previous && options.leading === false) previous = now;
// 計算剩餘時間
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果當前調用已經大於上次調用時間 + wait
// 或者用戶手動調了時間
// 如果設置了 trailing,只會進入這個條件
// 如果沒有設置 leading,那麼第一次會進入這個條件
// 還有一點,你可能會覺得開啓了定時器那麼應該不會進入這個 if 條件了
// 其實還是會進入的,因爲定時器的延時
// 並不是準確的時間,很可能你設置了2秒
// 但是他需要2.2秒才觸發,這時候就會進入這個條件
if (remaining <= 0 || remaining > wait) {
// 如果存在定時器就清理掉否則會調用二次回調
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判斷是否設置了定時器和 trailing
// 沒有的話就開啓一個定時器
// 並且不能不能同時設置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
防抖
防抖動是將多次執行變爲最後一次執行。
/**
* 防抖函數,返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行
*
* @param {function} func 回調函數
* @param {number} wait 表示時間窗口的間隔
* @param {boolean} immediate 設置爲ture時,是否立即調用函數
* @return {function} 返回客戶調用函數
*/
export function debounce(func, wait = 200, immediate = false) {
let timer, context, args
// 延遲執行函數
const later = () => setTimeout(() => {
// 延遲函數執行完畢,清空緩存的定時器序號
timer = null
// 延遲執行的情況下,函數會在延遲函數中執行
// 使用到之前緩存的參數和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 這裏返回的函數是每次實際調用的函數
return function (...params) {
// 如果沒有創建延遲執行函數(later),就創建一個
if (!timer) {
timer = later()
// 如果是立即執行,調用函數
// 否則緩存參數和調用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延遲執行函數(later),調用的時候清除原來的並重新設定一個
// 這樣做延遲函數會重新計時
} else {
clearTimeout(timer)
timer = later()
context = this
args = params
}
}
}
避免內聯樣式
用 CSS 動畫代替 JavaScript 動畫
服務器gzip壓縮
Web Workers 處理 CPU 密集任務
整理學習自你需要掌握的21個React性能優化技巧