我們是袋鼠云數(shù)棧 UED 團(tuán)隊(duì),致力于打造優(yōu)秀的一站式數(shù)據(jù)中臺(tái)產(chǎn)品。我們始終保持工匠精神,探索前端道路,為社區(qū)積累并傳播經(jīng)驗(yàn)價(jià)值。
本文作者:的盧
引入
在日常開(kāi)發(fā)過(guò)程中,我們會(huì)使用很多性能優(yōu)化的 API
,比如像使用 memo
、useMemo
優(yōu)化組件或者值,再比如使用 shouldComponentUpdate
減少組件更新頻次,懶加載等等,都是一些比較好的性能優(yōu)化方式,今天我將從組件設(shè)計(jì)、結(jié)構(gòu)上來(lái)談一下 React 性能優(yōu)化以及數(shù)棧產(chǎn)品內(nèi)的實(shí)踐。
如何設(shè)計(jì)組件會(huì)有好的性能?
先看下面一張圖:
這是一顆 React 組件樹(shù),App
下面有三個(gè)子組件,分別是 Header
、Content
、Footer
,在 Content
組件下面又分別有 FolderTree
、WorkBench
、SiderBar
三個(gè)子組件,現(xiàn)在如果在 WorkBench 中觸發(fā)一次更新,那么 React 會(huì)遍歷哪些組件呢?Demo1
function FolderTree() {
console.log('render FolderTree');
return <p>folderTree</p>;
}
function SiderBar() {
console.log('render siderBar');
return <p>i'm SiderBar</p>;
}
export const WorkBenchGrandChild = () => {
console.log('render WorkBenchGrandChild');
return <p>i'm WorkBenchGrandChild</p>
};
export const WorkBenchChild = () => {
console.log('render WorkBenchChild');
return (
<>
<p>i'm WorkBenchChild</p>
<WorkBenchGrandChild />
</>
);
};
function WorkBench() {
const [num, setNum] = useState<number>(1);
console.log('render WorkBench');
return (
<>
<input
value={num}
onChange={(e) => {
setNum(+e.target.value || 0);
}}
/>
<p>num is {num}</p>
<WorkBenchChild />
</>
);
}
function Content() {
console.log('render content');
return (
<>
<FolderTree />
<WorkBench />
<SiderBar />
</>
);
};
function Footer() {
console.log('render footer');
return <p>i'm Footer</p>
};
function Header() {
console.log('render header');
return <p>i'm Header</p>;
}
// Demo1
function App() {
// const [, setStr] = useState<string>();
return (
<>
<Header />
<Content />
<Footer />
{/* <input onChange={(e) => { setStr(e.target.value) }} /> */}
</>
);
};
根據(jù)上面斷點(diǎn)和日志就可以得到下面的結(jié)論:
- 子孫組件每觸發(fā)一次更新,
React
都會(huì)重新遍歷整顆組件樹(shù)
當(dāng) input
輸入數(shù)字,引起 updateNum
變更狀態(tài)后,react-dom
中 beginWork
的 current
由頂層組件依次遍歷
-
React
更新時(shí)會(huì)過(guò)濾掉未變化的組件,達(dá)到減少更新的組件數(shù)的目的
在更新過(guò)程中,雖然 React
重新遍歷了組件樹(shù),但 沒(méi)有打印沒(méi)有變化的 Header
、Footer
、FolderTree
、SiderBar
組件內(nèi)的日志
- 父組件狀態(tài)變化,會(huì)引起子組件更新
WorkBenchChild
屬于 WorkBench
的子組件,雖然 WorkBenchChild
沒(méi)有變化,但仍被重新渲染,打印了輸入日志,如果更近一步去斷點(diǎn)會(huì)發(fā)現(xiàn) WorkBenchChild
的 oldProps
和 newProps
是不相等的,會(huì)觸發(fā) updateFunctionComponent
更新。
綜上我們可以得出一個(gè)結(jié)論,就是 React
自身會(huì)有一些性能優(yōu)化的操作,會(huì)盡可能只更新變化的組件,比如 Demo1 中 WorkBench
、WorkBenchChild
、WorkBenchGrandChild
組件,而會(huì)繞開(kāi) 不變的 Header
、Footer
等組件,那么盡可能的讓 React
更新的粒度就是性能優(yōu)化的方向,既然盡可能只更新變化的組件,那么如何定義組件是否變化?
如何定義組件是否變化?
React
是以數(shù)據(jù)驅(qū)動(dòng)視圖的單向數(shù)據(jù)流,核心也就是數(shù)據(jù),那么什么會(huì)影響數(shù)據(jù),以及數(shù)據(jù)的承載方式,有以下幾點(diǎn):
- props
- state
- context
- 父組件不變!
父組件與當(dāng)前組件其實(shí)沒(méi)有關(guān)聯(lián)性,放到這里是因?yàn)?,上面的例子?WorkBenchChild
組件中沒(méi)有 state、props、context,理論上來(lái)說(shuō)就不變,實(shí)際上卻重新 render
了,因?yàn)?其父組件 WorkBench
有狀態(tài)的變動(dòng),所以這里也提了一下,在不使用性能優(yōu)化 API 的前提下,只要保證 props、state、context & 其父組件不變,那么組件就不變
還是回到剛剛的例子 Demo WorkBench
export const WorkBenchGrandChild = () => {
console.log('render WorkBenchGrandChild');
return <p>i'm WorkBenchGrandChild</p>
};
export const WorkBenchChild = () => {
console.log('render WorkBenchChild');
return (
<>
<p>i'm WorkBenchChild</p>
<WorkBenchGrandChild />
</>
);
};
function WorkBench() {
const [num, setNum] = useState<number>(1);
console.log('render WorkBench');
return (
<>
<input
value={num}
onChange={(e) => {
setNum(+e.target.value || 0);
}}
/>
<p>num is {num}</p>
<WorkBenchChild />
</>
);
}
export default WorkBench;
看一下這個(gè) demo
,WorkBench
組件有一個(gè) num
狀態(tài),還有一個(gè) WorkBenchChild
的子組件,沒(méi)有狀態(tài),純渲染組件,同時(shí) WorkBenchChild
組件也有一個(gè) 純渲染組件 WorkBenchGrandChild
子組件,當(dāng)輸入 input
改變 num
的值時(shí),WorkBenchChild
組件 和 WorkBenchGrandChild
組件都重新渲染。我們來(lái)分析一下在 WorkBench
組件中,它的子組件 WorkBenchChild
自始至終其實(shí)都沒(méi)有變化,有變化的其實(shí)是 WorkBench
中的 狀態(tài)
,但是就是因?yàn)?WorkBench
中的 狀態(tài)
發(fā)生了變化,導(dǎo)致了其子組件也一并更新,這就帶來(lái)了一定的性能損耗,找到了問(wèn)題,那么就需要解決問(wèn)題。
如何優(yōu)化?
使用性能優(yōu)化 API
export const WorkBenchGrandChild = () => {
console.log('render WorkBenchGrandChild');
return <p>i'm WorkBenchGrandChild</p>
};
export const WorkBenchChild = React.memo(() => {
console.log('render WorkBenchChild');
return (
<>
<p>i'm WorkBenchChild</p>
<WorkBenchGrandChild />
</>
);
});
// Demo WorkBench
function WorkBench() {
const [num, setNum] = useState<number>(1);
console.log('render WorkBench');
return (
<>
<input
value={num}
onChange={(e) => {
setNum(+e.target.value || 0);
}}
/>
<p>num is {num}</p>
<WorkBenchChild />
</>
);
}
export default WorkBench;
我們可以使用 React.memo()
包裹 WorkBenchChild
組件,在其 diff
的過(guò)程中 props
改為淺對(duì)比的方式達(dá)到性能優(yōu)化的目的,通過(guò)斷點(diǎn)可以知道 通過(guò) memo
包裹的組件在 diff
時(shí) oldProps
和 newProps
仍然不等,進(jìn)入了 updateSimpleMemoComponent
中了,而 updateSimpleMemoComponent
中有個(gè) shallowEqual
淺比較方法是結(jié)果相等的,因此沒(méi)有觸發(fā)更新,而是復(fù)用了組件。
狀態(tài)隔離(將狀態(tài)隔離到子組件中)
function ExchangeComp() {
const [num, setNum] = useState<number>(1);
console.log('render ExchangeComp');
return (
<>
<input
value={num}
onChange={(e) => {
setNum(+e.target.value || 0);
}}
/>
<p>num is {num}</p>
</>
);
};
// Demo WorkBench
function WorkBench() {
// const [num, setNum] = useState<number>(1);
console.log('render WorkBench');
return (
<>
<ExchangeComp />
<WorkBenchChild />
</>
);
}
export default WorkBench;
上面 Demo1 的結(jié)論,父組件更新,會(huì)觸發(fā)子組件更新,就因?yàn)?WorkBench
狀態(tài)改變,導(dǎo)致 WorkBenhChild
也更新了,這個(gè)時(shí)候可以手動(dòng)創(chuàng)造條件,讓 WorkBenchChild
的父組件也就是 WorkBench
組件剝離狀態(tài),沒(méi)有狀態(tài)改變,這種情況下 WorkBenchChild
滿足了 父組件不變的前提,且沒(méi)有 state
、props
、context
,那么也能夠達(dá)到性能優(yōu)化的結(jié)果。
對(duì)比
- 結(jié)果一樣,都是對(duì)
WorkBenchChild
進(jìn)行了優(yōu)化,在WorkBench
組件更新時(shí),WorkBenchChild
、WorkBenchGrandChild
沒(méi)有重新渲染 - 出發(fā)點(diǎn)不一樣,用
memo
性能優(yōu)化 API 是直接作用到子組件上面,而狀態(tài)隔離是在父組件上面操作,而受益的是其子組件
結(jié)論
- 只要結(jié)構(gòu)寫的好,性能不會(huì)太差
- 父組件不變,子組件可能不變
性能優(yōu)化方向
- 找到項(xiàng)目中性能損耗嚴(yán)重的組件(節(jié)點(diǎn))
在業(yè)務(wù)項(xiàng)目中,找到卡頓、崩潰 的組件(節(jié)點(diǎn))
- 在根組件(節(jié)點(diǎn))上使用性能優(yōu)化 API
在根組件上使用的目的就是避免其祖先組件如果沒(méi)有做好組件設(shè)計(jì)會(huì)給根組件帶來(lái)無(wú)效的重復(fù)渲染,因?yàn)樯厦嫣岬降?,父組件更新,子組件也會(huì)更新
- 在其他節(jié)點(diǎn)上使用 狀態(tài)隔離的方式進(jìn)行優(yōu)化
優(yōu)化祖先組件,避免給子組件造成無(wú)效的重復(fù)渲染
總結(jié)
我們從 組件結(jié)構(gòu) 和 性能優(yōu)化 API 上介紹了性能優(yōu)化的兩種不同的優(yōu)化方式,在實(shí)際項(xiàng)目使用上,也并非使用某一種優(yōu)化方式,而是多種優(yōu)化方式結(jié)合著來(lái)以達(dá)到最好的性能
產(chǎn)品中的部分實(shí)踐
-
將狀態(tài)隔離到子組件內(nèi)部,避免引起不必要的更新
import React, { useCallback, useEffect, useState } from 'react'; import { connect } from 'react-redux'; import type { SelectProps } from 'antd'; import { Select } from 'antd'; import { fetchBranchApi } from '@/api/project/optionsConfig'; const BranchSelect = (props: SelectProps) => { const [list, setList] = useState<string[]>([]); const [loading, setLoading] = useState<boolean>(false); const { projectId, project, tenantId, ...otherProps } = props; const init = useCallback(async () => { try { setLoading(true); const { code, data } = await fetchBranchApi(params); if (code !== 1) return; setList(data); } catch (err) { } finally { setLoading(false); } }, []); useEffect(() => { init(); }, [init]); return ( <Select showSearch optionFilterProp="children" filterOption={(input, { label }) => { return ((label as string) ?? '') ?.toLowerCase?.() .includes?.(input?.toLowerCase?.()); }} options={list?.map((value) => ({ label: value, value }))} loading={loading} placeholder="請(qǐng)選擇代碼分支" {...otherProps} /> ); }; export default React.memo(BranchSelect);
比如在中后臺(tái)系統(tǒng)中很多表單型組件
Select
、TreeSelect
、Checkbox
,其展示的數(shù)據(jù)需要通過(guò)接口獲取,那么此時(shí),如果將獲取數(shù)據(jù)的操作放到父組件,那么每次請(qǐng)求數(shù)據(jù)不僅會(huì)導(dǎo)致需要數(shù)據(jù)的那個(gè)表單項(xiàng)組件更新,同時(shí),其他的表單項(xiàng)也會(huì)更新,這就有一定的性能損耗,那么按照上面的例子這樣將其狀態(tài)封裝到內(nèi)部,避免請(qǐng)求數(shù)據(jù)影響其他組件更新,就可以達(dá)到性能優(yōu)化的目的,一般建議在外層再加上memo
性能優(yōu)化 API,避免因?yàn)橥獠拷M件影響內(nèi)部組件更新。 -
Canvas render & Svg render
// 畫一個(gè)小十字 export function createPlus( point: { x: number; y: number }, { radius, lineWidth, fill }: { radius: number; lineWidth: number; fill: string } ) { // 豎 橫 const colWidth = point.x - (1 / 2) * lineWidth; const colHeight = point.y - (1 / 2) * lineWidth - radius; const colTop = 2 * radius + lineWidth; const colBottom = colHeight; const rowWidth = point.x - (1 / 2) * lineWidth - radius; const rowHeight = point.y - (1 / 2) * lineWidth; const rowRight = 2 * radius + lineWidth; const rowLeft = rowWidth; return ` <path d="M${colWidth} ${colHeight}h${lineWidth}v${colTop}h-${lineWidth}V${colBottom}z" fill="${fill}"></path> <path d="M${rowWidth} ${rowHeight}h${rowRight}v${lineWidth}H${rowLeft}v-${lineWidth}z" fill="${fill}"></path> `; } renderPlusSvg = throttle(() => { const plusBackground = document.getElementById(`plusBackground_${this.randomKey}`); const { scrollTop, scrollLeft, clientHeight, clientWidth } = this._container || {}; const minWidth = scrollLeft; const maxWidth = minWidth + clientWidth; const minHeight = scrollTop; const maxHeight = minHeight + clientHeight; const stepping = 30; const radius = 3; const fillColor = '#EBECF0'; const lineWidth = 1; let innerHtml = ''; try { // 根據(jù)滾動(dòng)情況拿到容器的四個(gè)坐標(biāo)點(diǎn), 只渲染當(dāng)前滾動(dòng)容器內(nèi)的十字,實(shí)時(shí)渲染 for (let x = minWidth; x < maxWidth; x += stepping) { for (let y = minHeight; y < maxHeight; y += stepping) { // 畫十字 innerHtml += createPlus({ x, y }, { radius, fill: fillColor, lineWidth }); } } plusBackground.innerHTML = innerHtml; } catch (e) {} });
問(wèn)題源于在大數(shù)據(jù)情況下,由 canvas 渲染的 小十字背景渲染失敗,經(jīng)測(cè)試,業(yè)務(wù)數(shù)據(jù)在 200條左右 canvas 畫布繪制寬度就已經(jīng)達(dá)到了 70000px,需要渲染的小十字 數(shù)量級(jí)在 10w 左右,canvas 不適合繪制尺寸過(guò)大的場(chǎng)景(超過(guò)某個(gè)閥值就會(huì)出現(xiàn)渲染失敗,具體閥值跟瀏覽器有關(guān)系),而 svg 不適合繪制數(shù)量過(guò)多的場(chǎng)景,目前的業(yè)務(wù)場(chǎng)景卻是 畫布尺寸大,繪制元素多,后面的解決方式就是 采用 svg 渲染,將 畫布渲染出來(lái),同時(shí)監(jiān)聽(tīng)容器的滾動(dòng)事件,同時(shí)只渲染滾動(dòng)容器中可視區(qū)域內(nèi)的背景,實(shí)時(shí)渲染,渲染數(shù)量在 100 左右,實(shí)測(cè)就無(wú)卡頓現(xiàn)象,問(wèn)題解決
參考:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-711034.html
- React 性能優(yōu)化的一切
- React 源碼解析之 Fiber渲染
- 魔術(shù)師卡頌
最后
歡迎關(guān)注【袋鼠云數(shù)棧UED團(tuán)隊(duì)】~
袋鼠云數(shù)棧UED團(tuán)隊(duì)持續(xù)為廣大開(kāi)發(fā)者分享技術(shù)成果,相繼參與開(kāi)源了歡迎star文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-711034.html
- 大數(shù)據(jù)分布式任務(wù)調(diào)度系統(tǒng)——Taier
- 輕量級(jí)的 Web IDE UI 框架——Molecule
- 針對(duì)大數(shù)據(jù)領(lǐng)域的 SQL Parser 項(xiàng)目——dt-sql-parser
- 袋鼠云數(shù)棧前端團(tuán)隊(duì)代碼評(píng)審工程實(shí)踐文檔——code-review-practices
- 一個(gè)速度更快、配置更靈活、使用更簡(jiǎn)單的模塊打包器——ko
到了這里,關(guān)于關(guān)于 React 性能優(yōu)化和數(shù)棧產(chǎn)品中的實(shí)踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!