前端性能优化-react渲染
console.log("console.log 在这里")
前言
本文针对已有一定React
基础的朋友. React的优化着重点, 无疑是render
.
一是利用分析工具, 介绍如何分析实际项目中需要渲染优化的点.
二是通过一些简单的例子讲解memo
, useMemo
, useCallback
等用法.
实际分析
分析工具
React Developer Tools 谷歌商店地址
该插件核心是利用React.Profile
, 可自行点击跳转移步官网阅读.
他会跑出下面这种组件火焰图, 方便查看每个组件的re-render
触发原因与时间
hooks讲解
why memo
父组件的渲染必定引起子组件的渲染, 哪怕子组件并没有从父组件继承有变化的props. 如下例子:
const { createRoot } = ReactDOM
const renderDom = document.getElementById('no-memo-demo')
const root = createRoot(renderDom)
// 因 console.log会输出在最上方引入react的地方, 这里给render结果插入一下log内容, 方便查看
const logText = (text) => {
console.log(text)
const newDiv = document.createElement('div')
newDiv.innerHTML = text
renderDom.appendChild(newDiv)
}
// 子组件
const Child = () => {
logText('render child')
return (
<div>child component</div>
)
}
// 爹组件
const Parent = () => {
const [state, setState] = React.useState(0)
React.useEffect(() => {
logText('render parent first time')
}, [])
const handleClick = () => {
logText('-----------------------分隔符----------------------')
setState(state + 1)
}
logText('render parent')
return (
<div>
<div>state: {state} </div>
<button onClick={handleClick}>
add
</button>
<Child />
</div>
)
}
root.render(<Parent />);
在此点击add
按钮查看父子组件渲染输出:
上个memo试试
给上面的例子中的child
, 加上memo
试试:
如下例子:
const { createRoot } = ReactDOM
const renderDom = document.getElementById('memo-demo')
const root = createRoot(renderDom)
// 因 console.log会输出在最上方引入react的地方, 这里给render结果插入一下log内容, 方便查看
const logText = (text) => {
console.log(text)
const newDiv = document.createElement('div')
newDiv.innerHTML = text
renderDom.appendChild(newDiv)
}
// 子组件
const Child = React.memo(() => { // ====================>这里!!!!!!
logText('render child')
return (
<div>child component</div>
)
})
// 爹组件
const Parent = () => {
const [state, setState] = React.useState(0)
React.useEffect(() => {
logText('render parent first time')
}, [])
const handleClick = () => {
logText('-----------------------分隔符----------------------')
setState(state + 1)
}
logText('render parent')
return (
<div>
<div>state: {state} </div>
<button onClick={handleClick}>
add
</button>
<Child />
</div>
)
}
root.render(<Parent />);
在此点击add
按钮查看父子组件渲染输出:
memo应用场景
如果子组件的渲染很重的话, 比如是一个有大量样式的styled-components
(css-in-js框架)组件, 额外的性能消耗无疑是明显且必要的.
why useCallback
每一次组件的re-redner
必将引起组件内部函数的re-const
.
只不过这种re-const
开销非常非常小, 往往在高刷新render
和构建复杂函数
的时候, 才显得比较必要.
详情请看以下例子, 我每点击一次button都会通过setState引起一次re-render
:
const { createRoot } = ReactDOM
const renderDom = document.getElementById('useCallback-demo')
const root = createRoot(renderDom)
// 因 console.log会输出在最上方引入react的地方, 这里给render结果插入一下log内容, 方便查看
const logText = (text) => {
console.log(text)
const newDiv = document.createElement('div')
newDiv.innerHTML = text
renderDom.appendChild(newDiv)
}
const DemoComponent = () => {
const [state, setState] = React.useState(0)
React.useEffect(() => {
logText('render parent first time')
window.addEventListener('click', handleLog)
return () => {
window.removeEventListener('click', handleLog)
}
}, [])
const handleClick = () => {
logText('-----------------------分隔符----------------------')
setState(state + 1)
}
logText('render')
const functionWithoutUseCallback = () => {
logText('const functionWithoutUseCallback')
return 123
}
const functionWithUseCallback = React.useCallback(() => {
logText('const functionWithUseCallback')
return 123
},[])
React.useEffect(() => {
logText('functionWithoutUseCallback changed')
}, [functionWithoutUseCallback])
React.useEffect(() => {
logText('functionWithUseCallback changed')
}, [functionWithUseCallback])
const handleLog = () => {
logText(123)
}
return (
<div>
<div>state: {state} </div>
<button onClick={handleClick}>
add
</button>
</div>
)
}
root.render(<DemoComponent />);
应用场景
如果此时你有一个很重的Child
组件, props需要绑定继承functionWithoutUseCallback
, 同时父级又疯狂re-render
, 咱优化的价值这不就来了吗?
拓展讲一个bug
useCallback都讲到这了, 顺带讲一个bug
帮助理解re-const
.
可以尝试下面的demo
,
- 绑定
click to bind
- 随便click
- 点击
click re-render
- 再点击
click to unBind
结果和你预期的结果一样吗? 如果一样, 证明你已经理解了.
const { createRoot } = ReactDOM
const renderDom = document.getElementById('useCallback-bug-demo')
const root = createRoot(renderDom)
// 因 console.log会输出在最上方引入react的地方, 这里给render结果插入一下log内容, 方便查看
const logText = (text) => {
console.log(text)
const newDiv = document.createElement('div')
newDiv.innerHTML = text
renderDom.appendChild(newDiv)
}
const DemoComponent = () => {
const [state, setState] = React.useState(0)
React.useEffect(() => {
logText('render parent first time')
}, [])
const handleClick = () => {
logText('-----------------------分隔符----------------------')
setState(state + 1)
}
const handleLog = () => {
logText(123)
}
const handleLogWithUseCallback = React.useCallback(() => {
logText(666)
}, [])
const handleBindFunc = () => {
window.addEventListener('click', handleLog)
window.addEventListener('click', handleLogWithUseCallback)
}
const handleUnBindFunc = () => {
window.removeEventListener('click', handleLog)
window.removeEventListener('click', handleLogWithUseCallback)
}
return (
<div>
<div>state: {state} </div>
<button onClick={handleClick}>
click to re-render
</button>
<button onClick={handleBindFunc}>
click to bind
</button>
<button onClick={handleUnBindFunc}>
click to unBind
</button>
</div>
)
}
root.render(<DemoComponent />);
why useMemo
一般useMemo
和useCallback
都会放在一起讲, 只不过前者返回的是value
, 后者返回的是function
.
简单举个例子说明:
const { createRoot } = ReactDOM
const renderDom = document.getElementById('useMemo-demo')
const root = createRoot(renderDom)
// 因 console.log会输出在最上方引入react的地方, 这里给render结果插入一下log内容, 方便查看
const logText = (text) => {
console.log(text)
const newDiv = document.createElement('div')
newDiv.innerHTML = text
renderDom.appendChild(newDiv)
}
const DemoComponent = () => {
const [state1, setState1] = React.useState(0)
const [state2, setState2] = React.useState(0)
const val1 = state1 + state2
const val2 = React.useMemo(() => {
return state1 + state2
}, [state1])
React.useEffect(() => {
logText('render parent first time')
}, [])
const handleClick = () => {
logText('-----------------------分隔符----------------------')
setState(state + 1)
}
return (
<div>
<div>state1: {state1} </div>
<div>state2: {state2} </div>
<div>val1(state1 + state2): {val1} </div>
<div>val2(memoState1 + state2): {val2} </div>
<button onClick={() => setState1(state1 + 1)}>
addState1
</button>
<button onClick={() => setState2(state2 + 1)}>
addState2
</button>
</div>
)
}
root.render(<DemoComponent />);
聪明的你, 看明白了吗? 肯定看明白了, 我就不讲了.
useMemo
官话说的是缓存复杂的计算值
, 从而减少re-render
消耗.
然而useMemo
的[]
依赖项里, 加了东西之后, 依赖本身的比较也是一种性能消耗, 我尚未遇到比较合适的场景, 去横向比较性能的差异, 等以后遇到了再来补充吧.