2023年5月31日
By: Chase

前端性能优化-react渲染


        console.log("console.log 在这里")
    

前言

本文针对已有一定React基础的朋友. React的优化着重点, 无疑是render.

一是利用分析工具, 介绍如何分析实际项目中需要渲染优化的点.

二是通过一些简单的例子讲解memo, useMemo, useCallback等用法.

实际分析

分析工具

React Developer Tools 谷歌商店地址

该插件核心是利用React.Profile, 可自行点击跳转移步官网阅读.

他会跑出下面这种组件火焰图, 方便查看每个组件的re-render触发原因与时间

图-0

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按钮查看父子组件渲染输出:

no-memo-demo

上个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-demo

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 />);

useCallback-demo

应用场景

如果此时你有一个很重的Child组件, props需要绑定继承functionWithoutUseCallback, 同时父级又疯狂re-render, 咱优化的价值这不就来了吗?

拓展讲一个bug

useCallback都讲到这了, 顺带讲一个bug帮助理解re-const.

可以尝试下面的demo,

  1. 绑定click to bind
  2. 随便click
  3. 点击click re-render
  4. 再点击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 />);
useCallback-bug-demo

why useMemo

一般useMemouseCallback都会放在一起讲, 只不过前者返回的是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-demo

聪明的你, 看明白了吗? 肯定看明白了, 我就不讲了.

useMemo官话说的是缓存复杂的计算值, 从而减少re-render消耗.

然而useMemo[]依赖项里, 加了东西之后, 依赖本身的比较也是一种性能消耗, 我尚未遇到比较合适的场景, 去横向比较性能的差异, 等以后遇到了再来补充吧.

Tags: 前端 性能 React