MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 高阶组件的性能优化策略

2022-02-014.3k 阅读

React 高阶组件基础回顾

在深入探讨 React 高阶组件(Higher - Order Components,HOC)的性能优化策略之前,让我们先简要回顾一下 HOC 的基本概念。

高阶组件本身不是 React API 的一部分,而是一种基于 React 的组合特性而形成的设计模式。简单来说,高阶组件是一个函数,它接受一个组件作为参数,并返回一个新的组件。

import React from'react';

// 高阶组件函数
function withLogging(WrappedComponent) {
    return class extends React.Component {
        componentDidMount() {
            console.log('Component mounted');
        }

        componentWillUnmount() {
            console.log('Component will unmount');
        }

        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

// 被包装的组件
class MyComponent extends React.Component {
    render() {
        return <div>My Component</div>;
    }
}

// 使用高阶组件包装 MyComponent
const LoggedComponent = withLogging(MyComponent);

export default LoggedComponent;

在上述代码中,withLogging 是一个高阶组件,它接受 MyComponent 作为参数,并返回一个新的组件 LoggedComponent。新组件添加了组件挂载和卸载时的日志记录功能,同时保留了 MyComponent 的原有功能。

高阶组件对性能的潜在影响

  1. 额外的渲染次数 当使用高阶组件时,很容易引入额外的渲染。例如,考虑以下情况:
import React from'react';

function withExtraProps(WrappedComponent) {
    return class extends React.Component {
        state = {
            extraProp: 'default value'
        };

        componentDidMount() {
            setTimeout(() => {
                this.setState({ extraProp: 'new value' });
            }, 2000);
        }

        render() {
            return <WrappedComponent {...this.props} extraProp={this.state.extraProp} />;
        }
    };
}

class InnerComponent extends React.Component {
    render() {
        console.log('InnerComponent rendering');
        return <div>{this.props.extraProp}</div>;
    }
}

const EnhancedComponent = withExtraProps(InnerComponent);

export default class App extends React.Component {
    render() {
        return <EnhancedComponent />;
    }
}

在这个例子中,InnerComponent 会在初始渲染时打印一次 “InnerComponent rendering”,然后在两秒后,withExtraProps 中的状态更新,导致 InnerComponent 再次渲染,即使 InnerComponentprops 实际上可能并没有改变它需要渲染的逻辑。

  1. props 传递和浅比较 React 在判断组件是否需要重新渲染时,默认会对 props 进行浅比较。高阶组件可能会干扰这种浅比较。假设我们有一个高阶组件,它向被包装组件传递了一个新的函数作为 prop
import React from'react';

function withNewFunction(WrappedComponent) {
    return class extends React.Component {
        newFunction = () => {
            console.log('New function called');
        };

        render() {
            return <WrappedComponent {...this.props} newFunction={this.newFunction} />;
        }
    };
}

class ChildComponent extends React.Component {
    shouldComponentUpdate(nextProps) {
        return this.props.value!== nextProps.value;
    }

    render() {
        console.log('ChildComponent rendering');
        return <div>{this.props.value}</div>;
    }
}

const EnhancedChild = withNewFunction(ChildComponent);

export default class App extends React.Component {
    state = {
        value: 'initial'
    };

    componentDidMount() {
        setTimeout(() => {
            this.setState({ value: 'updated' });
        }, 2000);
    }

    render() {
        return <EnhancedChild value={this.state.value} />;
    }
}

ChildComponent 中,我们通过 shouldComponentUpdate 方法来控制组件的更新,只有当 value prop 改变时才更新。然而,由于高阶组件每次渲染都会生成一个新的 newFunction,浅比较会认为 props 发生了变化,即使 value 没有改变,ChildComponent 也可能会不必要地重新渲染。

性能优化策略

1. 使用 React.memo 或 PureComponent

  • React.memo 用于函数式组件 对于函数式组件,可以使用 React.memo 来包裹,它会对 props 进行浅比较,只有当 props 发生变化时才会重新渲染。
import React from'react';

function MyFunctionalComponent(props) {
    return <div>{props.value}</div>;
}

const MemoizedComponent = React.memo(MyFunctionalComponent);

function withExtraProp(WrappedComponent) {
    return class extends React.Component {
        state = {
            extraProp: 'default'
        };

        componentDidMount() {
            setTimeout(() => {
                this.setState({ extraProp: 'new' });
            }, 2000);
        }

        render() {
            return <WrappedComponent {...this.props} extraProp={this.state.extraProp} />;
        }
    };
}

const EnhancedMemoizedComponent = withExtraProp(MemoizedComponent);

export default class App extends React.Component {
    state = {
        mainProp: 'initial'
    };

    componentDidMount() {
        setTimeout(() => {
            this.setState({ mainProp: 'updated' });
        }, 4000);
    }

    render() {
        return <EnhancedMemoizedComponent mainProp={this.state.mainProp} />;
    }
}

在这个例子中,MemoizedComponent 只会在 props 真正改变时才会重新渲染。即使 withExtraProp 中的 extraProp 发生变化,但如果 mainProp 没有改变,MemoizedComponent 也不会重新渲染。

  • PureComponent 用于类组件 对于类组件,可以继承 React.PureComponent,它和 React.memo 类似,会对 propsstate 进行浅比较。
import React from'react';

class MyClassComponent extends React.PureComponent {
    render() {
        return <div>{this.props.value}</div>;
    }
}

function withAnotherProp(WrappedComponent) {
    return class extends React.Component {
        state = {
            anotherProp: 'old'
        };

        componentDidMount() {
            setTimeout(() => {
                this.setState({ anotherProp: 'new' });
            }, 2000);
        }

        render() {
            return <WrappedComponent {...this.props} anotherProp={this.state.anotherProp} />;
        }
    };
}

const EnhancedClassComponent = withAnotherProp(MyClassComponent);

export default class App extends React.Component {
    state = {
        mainValue: 'init'
    };

    componentDidMount() {
        setTimeout(() => {
            this.setState({ mainValue: 'update' });
        }, 4000);
    }

    render() {
        return <EnhancedClassComponent mainValue={this.state.mainValue} />;
    }
}

MyClassComponent 继承自 React.PureComponent,只有当 mainValue 或者 props 中其他直接引用的对象发生变化时,它才会重新渲染,避免了因高阶组件中无关 props 变化而导致的不必要渲染。

2. 减少不必要的 prop 传递

在高阶组件中,尽量避免传递不必要的 props 给被包装组件。例如,假设我们有一个高阶组件用于添加权限控制:

import React from'react';

function withPermission(WrappedComponent) {
    return class extends React.Component {
        state = {
            hasPermission: true,
            userInfo: {
                name: 'John',
                age: 30
            }
        };

        render() {
            if (!this.state.hasPermission) {
                return <div>You don't have permission</div>;
            }
            // 这里传递了 userInfo,即使 WrappedComponent 可能不需要
            return <WrappedComponent {...this.props} userInfo={this.state.userInfo} />;
        }
    };
}

class ContentComponent extends React.Component {
    render() {
        return <div>{this.props.content}</div>;
    }
}

const ProtectedContent = withPermission(ContentComponent);

export default class App extends React.Component {
    render() {
        return <ProtectedContent content="Some important content" />;
    }
}

在这个例子中,ContentComponent 只关心 content prop,但高阶组件 withPermission 传递了 userInfo,这可能会导致不必要的渲染。我们可以优化如下:

import React from'react';

function withPermission(WrappedComponent) {
    return class extends React.Component {
        state = {
            hasPermission: true
        };

        render() {
            if (!this.state.hasPermission) {
                return <div>You don't have permission</div>;
            }
            return <WrappedComponent {...this.props} />;
        }
    };
}

class ContentComponent extends React.Component {
    render() {
        return <div>{this.props.content}</div>;
    }
}

const ProtectedContent = withPermission(ContentComponent);

export default class App extends React.Component {
    render() {
        return <ProtectedContent content="Some important content" />;
    }
}

这样,我们避免了传递不必要的 props,减少了因 props 变化导致的不必要渲染。

3. 稳定的 prop 传递

如前文提到,高阶组件传递不稳定的 props(如每次渲染都生成新的函数)会导致被包装组件不必要的渲染。我们可以通过多种方式解决这个问题。

  • 使用静态方法
import React from'react';

function withAction(WrappedComponent) {
    const action = () => {
        console.log('Action called');
    };

    return class extends React.Component {
        static action = action;

        render() {
            return <WrappedComponent {...this.props} action={withAction.action} />;
        }
    };
}

class ButtonComponent extends React.Component {
    render() {
        return <button onClick={this.props.action}>Click me</button>;
    }
}

const EnhancedButton = withAction(ButtonComponent);

export default class App extends React.Component {
    render() {
        return <EnhancedButton />;
    }
}

通过将函数定义为高阶组件的静态方法,我们确保每次传递给 ButtonComponentaction 函数都是同一个引用,避免了因函数引用变化导致的不必要渲染。

  • 使用 useCallback(针对函数式高阶组件) 在函数式高阶组件中,可以使用 useCallback 来缓存函数。
import React, { useCallback } from'react';

function withToggle(WrappedComponent) {
    return (props) => {
        const [isOn, setIsOn] = React.useState(false);

        const toggle = useCallback(() => {
            setIsOn(!isOn);
        }, [isOn]);

        return <WrappedComponent {...props} isOn={isOn} toggle={toggle} />;
    };
}

function SwitchComponent(props) {
    return (
        <div>
            <input type="checkbox" checked={props.isOn} onChange={props.toggle} />
        </div>
    );
}

const EnhancedSwitch = withToggle(SwitchComponent);

export default class App extends React.Component {
    render() {
        return <EnhancedSwitch />;
    }
}

useCallback 会在依赖项(这里是 isOn)没有变化时返回相同的函数引用,从而避免了 SwitchComponenttoggle 函数变化而导致的不必要渲染。

4. 合并高阶组件

如果有多个高阶组件作用于同一个组件,合理合并高阶组件可以减少嵌套和潜在的性能问题。

假设我们有两个高阶组件,一个用于添加日志记录,另一个用于权限控制:

import React from'react';

function withLogging(WrappedComponent) {
    return class extends React.Component {
        componentDidMount() {
            console.log('Component mounted');
        }

        componentWillUnmount() {
            console.log('Component will unmount');
        }

        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

function withPermission(WrappedComponent) {
    return class extends React.Component {
        state = {
            hasPermission: true
        };

        render() {
            if (!this.state.hasPermission) {
                return <div>You don't have permission</div>;
            }
            return <WrappedComponent {...this.props} />;
        }
    };
}

class MyComponent extends React.Component {
    render() {
        return <div>My Component Content</div>;
    }
}

// 原始方式
const LoggedAndProtected1 = withPermission(withLogging(MyComponent));

// 合并高阶组件
function combineHOCs(...hocs) {
    return (WrappedComponent) => hocs.reduceRight((acc, hoc) => hoc(acc), WrappedComponent);
}

const LoggedAndProtected2 = combineHOCs(withLogging, withPermission)(MyComponent);

export default class App extends React.Component {
    render() {
        return (
            <div>
                <LoggedAndProtected1 />
                <LoggedAndProtected2 />
            </div>
        );
    }
}

在上述代码中,combineHOCs 函数用于合并多个高阶组件。这种方式可以使高阶组件的应用更加清晰,同时减少不必要的嵌套,在一定程度上优化性能。

5. 避免在 render 方法中使用高阶组件

在组件的 render 方法中动态使用高阶组件会导致每次渲染都创建新的组件,这会破坏 React 的优化机制,导致不必要的渲染。

import React from'react';

function withBorder(WrappedComponent) {
    return class extends React.Component {
        render() {
            return (
                <div style={{ border: '1px solid black' }}>
                    <WrappedComponent {...this.props} />
                </div>
            );
        }
    };
}

class InnerComponent extends React.Component {
    render() {
        return <div>{this.props.value}</div>;
    }
}

export default class App extends React.Component {
    state = {
        value: 'initial'
    };

    componentDidMount() {
        setTimeout(() => {
            this.setState({ value: 'updated' });
        }, 2000);
    }

    // 反例:在 render 中使用高阶组件
    render() {
        const EnhancedInner = withBorder(InnerComponent);
        return <EnhancedInner value={this.state.value} />;
    }
}

在上述代码中,每次 App 组件渲染时,都会重新创建 EnhancedInner 组件,即使 InnerComponentprops 没有变化,也会导致额外的渲染开销。正确的做法是在组件外部定义高阶组件的应用:

import React from'react';

function withBorder(WrappedComponent) {
    return class extends React.Component {
        render() {
            return (
                <div style={{ border: '1px solid black' }}>
                    <WrappedComponent {...this.props} />
                </div>
            );
        }
    };
}

class InnerComponent extends React.Component {
    render() {
        return <div>{this.props.value}</div>;
    }
}

const EnhancedInner = withBorder(InnerComponent);

export default class App extends React.Component {
    state = {
        value: 'initial'
    };

    componentDidMount() {
        setTimeout(() => {
            this.setState({ value: 'updated' });
        }, 2000);
    }

    render() {
        return <EnhancedInner value={this.state.value} />;
    }
}

这样,EnhancedInner 只在组件加载时创建一次,避免了因在 render 中动态创建组件而导致的性能问题。

6. 使用 React.lazy 和 Suspense 处理高阶组件中的异步加载

当高阶组件涉及异步加载资源(如数据获取或加载子组件)时,可以使用 React.lazySuspense 来优化性能。

假设我们有一个高阶组件用于加载远程数据并传递给被包装组件:

import React, { lazy, Suspense } from'react';

function withData(WrappedComponent) {
    return class extends React.Component {
        state = {
            data: null
        };

        componentDidMount() {
            setTimeout(() => {
                this.setState({ data: 'Loaded data' });
            }, 2000);
        }

        render() {
            if (!this.state.data) {
                return <div>Loading...</div>;
            }
            return <WrappedComponent {...this.props} data={this.state.data} />;
        }
    };
}

const LazyComponent = lazy(() => import('./LazyComponent'));

const EnhancedLazyComponent = withData(LazyComponent);

export default class App extends React.Component {
    render() {
        return (
            <Suspense fallback={<div>Loading...</div>}>
                <EnhancedLazyComponent />
            </Suspense>
        );
    }
}

在这个例子中,LazyComponent 是通过 React.lazy 异步加载的。withData 高阶组件在数据加载完成后将数据传递给 LazyComponentSuspense 组件提供了加载时的占位 UI,确保用户体验的流畅性,同时也优化了性能,避免在数据未加载完成时进行不必要的渲染。

总结常见性能问题及优化方向

  1. 额外渲染问题
    • 问题表现:高阶组件可能因自身状态变化或不必要的 props 传递,导致被包装组件额外渲染。
    • 优化方向:使用 React.memoPureComponent 控制组件更新,减少不必要的 props 传递。
  2. 不稳定 prop 问题
    • 问题表现:高阶组件传递的函数或对象引用每次渲染都变化,导致被包装组件不必要渲染。
    • 优化方向:使用静态方法、useCallback 等确保 props 的稳定性。
  3. 高阶组件嵌套问题
    • 问题表现:过多的高阶组件嵌套可能增加复杂度和渲染开销。
    • 优化方向:合并高阶组件,减少嵌套层级。
  4. 动态高阶组件使用问题
    • 问题表现:在 render 方法中动态使用高阶组件破坏 React 优化机制。
    • 优化方向:在组件外部定义高阶组件的应用。
  5. 异步加载问题
    • 问题表现:高阶组件中异步加载资源可能导致性能问题和不良用户体验。
    • 优化方向:使用 React.lazySuspense 处理异步加载。

通过深入理解高阶组件对性能的影响,并运用上述性能优化策略,开发者可以有效地提升 React 应用中高阶组件的性能,打造更加流畅和高效的用户体验。在实际项目中,需要根据具体场景灵活选择和组合这些优化方法,以达到最佳的性能优化效果。同时,持续关注 React 官方文档和社区动态,以便及时应用新的性能优化技术和理念。