React组件性能优化

408 阅读7分钟

前言✨

React中,当父组件调用了子组件的时候,父组件的state发生变化,会导致父组件的更新,而子组件就算没有发生改变,也会进行更新。如果这个页面非常复杂且模块很多的情况下,只要父组件某一处发生改变就会导致所有的模块进行刷新,显然是损耗性能且没有意义的。

如下所示,子组件Son完全没有和父组件有数据往来,但是父组件的count改变时仍然会导致子组件的render。

ezgif-2-60485ff923.gif

// 父组件
class Father extends React.Component {
    state = {
        count: 0
    }
    addClick = () => {
        this.setState({
            count: this.state.count + 1
        })
    }
    render() {
        return (
            <div>
                父组件:count: {this.state.count}
                <button onClick={this.addClick}>点击</button>
                <Son />
            </div>
        )
    }
}

// 子组件
class Son extends React.Component{
    constructor(props: any) {
        super(props)
    }
    render() {
        console.log('子组件,执行了');
        return (
            <h2>子组件</h2>
        )
    }
}

class组件解决方式

1.使用shouldComponentUpdate周期

当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。首次渲染或使用 forceUpdate() 时不会调用该方法。

shouldComponentUpdate() 会对 props 和 state 进行浅层对比,并减少了跳过必要更新的可能性。不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能。

class Father extends React.Component {
    state = {
        name: "JayShen",
        count: 0
    }
    addClick = () => {
        this.setState({
            count: this.state.count + 1
        })
    }
    render() {
        return (
            <div>
                父组件:count: {this.state.count}
                <button onClick={this.addClick}>点击</button>
                <Son name={this.state.name} />
            </div>
        )
    }
}

interface Iprops {
    name?: string
}
class Son extends React.Component<Iprops>{
    constructor(props: Iprops) {
        super(props)
    }
    shouldComponentUpdate(nextProps: any, nextState: any) {
        return this.props.name !== nextProps.name // 比较新旧props.name是否相等
    }
    render() {
        console.log('子组件,执行了'); // 仅首次渲染时触发render
        return (
            <h2>子组件</h2>
        )
    }
}

2.React.PureComponent

React.PureComponentReact.Component 很相似。两者的区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数。

interface Iprops {
    name?: string
}
class Son extends React.PureComponent<Iprops>{
    constructor(props: Iprops) {
        super(props)
    }
    render() {
        console.log('子组件,执行了'); // 仅首次渲染时触发render
        return (
            <h2>子组件</h2>
        )
    }
}

ezgif.com-gif-maker.gif

函数式组件解决方式

1.React.memo

React.memo(component, areEqual) 是一个高阶组件(HOC),接受两个参数,一个是自定义函数,一个是比较函数。该方法类似class组件的shouldComponentUpdate以及pureComponent, 其中第二个参数用来判断该组件需不需要重新渲染,第二个参数省略的情况下,默认会对传到该组件的Props进行浅层对比

注意: 与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。

// 父组件
const Home: React.FC = () => {
    const [count, setCount] = useState<number>(0);
    const [text, setText] = useState<string>('静态props')
    const btnClick = () => {
        setCount((number) => number + 1);
    }
    return (
        <div>
            <h3>count: {count}</h3>
            <button onClick={btnClick}>按钮</button>
            <SonMemo text={text} />
            <SonMemo2 text={text} />
        </div>
    )
}

type PropsType = {
   text?: string
}
// 写法一
const Son: React.FC<PropsType> = ({ text }) => {
    console.log('重新渲染子组件1'); // 仅首次进入渲染1次
    return (
        <div>使用React.memo的子组件1</div>
    )
}
const SonMemo = React.memo(Son) // 或 const SonMemo = React.memo(Son, areEqual)

// 写法二 
const SonMemo2: React.FC<PropsType> = React.memo((props) => {
    console.log('重新渲染子组件2', props);// 仅首次进入渲染1次
    return <div>使用React.memo且自定义比较函数的子组件2</div>
}, areEqual) // areEqual选填

// 自定义比较函数
function areEqual(prevProps: PropsType, nextProps: PropsType) {
    console.log('areEqual对比函数触发返回:', prevProps.text === nextProps.text);
    if (prevProps.text === nextProps.text) {
        return true;
    }
    return false
}

reactMemo.gif

2.useMemo

useMemo接收2个参数,一个创建函数和一个依赖数组,仅当依赖项之一发生改变才会重新计算记忆值(memoized),这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

使用:

  • 接收一个函数作为参数
  • 同样接收第二个参数作为依赖列表(类似useEffect、useLayoutEffect)
  • 返回的是一个值。返回值可以是任何,函数、对象等都可以 场景一:父组件state改变导致子组件多余渲染,即使是引用类型也不会重复渲染了!

写法一:useMemo直接包裹子组件

const Father: React.FC = () => {
    const [count, setCount] = useState<number>(0)
    const addClick = () => {
        setCount(count => count + 1)
    }
     const userInfo = {
        name: "JayShen",
        age: 20
    }
    return (
        <div>
            <h2>父组件,count:{count}</h2>
            <button onClick={addClick}>按钮</button>
            {
                useMemo(() => {
                    return <Son userInfo={userInfo} />
                }, []) // 根据业务需求写入依赖,[]仅首次执行
            }
        </div>
    )
}

interface Iprops {
    userInfo?: {
        name: string
    }
}

const Son: React.FC<Iprops> = () => {
    console.log('子组件,执行了'); // 仅首次进入执行1次
    return (
        <div>
            <h3>子组件</h3>
        </div>
    )
}

写法二useMemo + React.memo,解决props为引用类型下使用react.memo无效问题

const Father: React.FC = () => {
    const [count, setCount] = useState<number>(0)
    const addClick = () => {
        setCount(count => count + 1)
    }
    const userInfo = useMemo(() => {
        return {
            name: "JayShen",
            age: 20
        };
    }, []);

    return (
        <div>
            <h2>父组件,count:{count}</h2>
            <button onClick={addClick}>按钮</button>
            <SonMemo userInfo={userInfo} />
        </div>
    )
}

interface Iprops {
    text?: String
    userInfo?: {
        name: string
    }
}

const Son: React.FC<Iprops> = () => {
    console.log('子组件,执行了'); // 仅首次进入执行1次
    return (

        <div>
            <h3>子组件</h3>
        </div>
    )
}
const SonMemo = React.memo(Son) 

reactUseMemo.gif

场景二:复杂数据计算

不使用useMemo

这个demo每次点击按钮,组件都会重新渲染一次,方法也会重新渲染一次。但是实际上expensiceFn中是一个花费比较高的函数,且函数返回值是恒定的,这样就造成了不必要的性能浪费。 这时候就可以用useMemo将计算后的值缓存起来

const Father: React.FC = () => {
    const [count, setCount] = useState<number>(0)
    const expensiveFn = () => {
        console.log('方法执行了');
        let result = 0;
        for (let i = 0; i < 10000; i++) {
            result += i;
        }
        return result;
    };
    const total = expensiveFn()
    return (
        <div>
            <h2>父组件,count:{count}</h2>
            <button onClick={() => { setCount(count + total) }}>
            按钮
            </button>
        </div>
    )
}

noCalcUseMemo.gif

使用useMemo:

//  const total = expensiveFn() 上面这段函数使用useMemo包裹,如下:
 const total = useMemo(() => {
        return expensiveFn()
    }, [])

yesCalcUseMemo.gif

这样的话expensiveFn的值就只会在初始的时候计算一次。 useMemo的第一个参数是一个函数,这个函数返回的值会被缓存起来,同时这个值会作为useMemo的返回值,第二个参数是一个数组依赖,如果数组里的值有变化,那么就会重新执行第一个参数里面的函数,并将函数返回的值缓存起来作为useMemo的返回值。 如果没有提供依赖数组,useMemo在每次渲染时都会计算新的值。

3.useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

返回一个 memoized 回调函数。 把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

场景一:子组件已经使用React.memo包裹,子组件props接收父组件一个函数回调,父组件state发生改变,子组件仍然重新渲染。

原因:子组件使用了父组件的函数,父组件更新,函数就会更新,又由于React.memo是浅对比,所以作为props的函数更新了,子组件就会重新渲染。这时候我们就可以使用useCallback结合React.memo来优化性能。

不使用useCallback:

// 父组件
const Father: React.FC = () => {
    const [count, setCount] = useState(0)
    const addClick = () => {
        setCount((count) => count + 1)
    }
    return (
        <div>
            <h2>父组件:{count}</h2>
            <SonMemo onClick={addClick} />
        </div>
    )
}

type TProps = {
    onClick?: () => void,
}
// 子组件
const Son: React.FC<TProps> = ({ onClick }) => {
    console.log('子组件渲染');
    return (
        <div>子组件
            <div>
                <button onClick={onClick}>子组件按钮</button>
            </div>
        </div>
    )
}
const SonMemo = React.memo(Son)

noUseCallback.gif

使用useCallback:

 // 把上述addClick使用useCallback包裹
 const addClick = useCallback(() => {
        setCount((count) => count + 1)
    }, [])

效果如下:

yesUseCallback.gif

父组件使用了useCallback来包裹函数,使得父组件在更新使函数的引用不变,因此子组件不会更新,从而达到了性能优化的效果。如果参数是对象则使用useMemo。

注意❗

1.在不明确以上各种钩子函数、周期的作用下,不要在项目中使用,也许会带来更严重的性能问题。

2.不能认为“不管什么情况,只要用useMemo或useCallback处理一下,就能远离性能的问题。要认识到useMemo和useCallback也有一定的计算开销,例如useMemo会缓存一些值,在后续重新渲染,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回缓存的值。这个过程有一定的计算开销。

3.创建函数的成本相当低,在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的区别,所以性能优化我们从减少render次数减少计算量这两点出发。