【问题标题】:React Native - reduce render times to optimize performance while using React hooksReact Native - 在使用 React 钩子时减少渲染时间以优化性能
【发布时间】:2020-09-12 18:30:15
【问题描述】:

背景


在释放React v16.8 之后,现在我们可以在 React Native 中使用钩子了。
我正在做一些简单的测试来查看渲染时间和性能
挂钩的功能组件和类组件。这是我的示例:

@Components/Button.js

import React, { memo } from 'react';
import { TouchableOpacity, Text } from 'react-native';

const Button = memo(({ title, onPress }) => {
  console.log("Button render"); // check render times
  return (
    <TouchableOpacity onPress={onPress} disabled={disabled}>
      <Text>{title}</Text>
    </TouchableOpacity>
  );
});

export default Button;

@Contexts/User.js

import React, { createContext, useState } from 'react';
import User from '@Models/User';

export const UserContext = createContext({});
export const UserContextProvider = ({ children }) => {
  let [ user, setUser ] = useState(null);

  const login = (loginUser) => {
    if (loginUser instanceof User) { setUser(loginUser); }
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <UserContext.Provider value={{value: user, login: login, logout: logout}}>
      {children}
    </UserContext.Provider>
  );
};

export function withUserContext(Component) {
  return function UserContextComponent(props) {
    return (
      <UserContext.Consumer>
        {(contexts) => <Component {...props} {...contexts} />}
      </UserContext.Consumer>
    );
  }
}

案例


我们有以下两种情况来构建屏幕组件:

@Screens/Login.js

案例 1:带有 Hooks 的功能组件

import React, { memo, useContext, useState } from 'react';
import { View, Text } from 'react-native';

import Button from '@Components/Button';
import { UserContext } from '@Contexts/User';

const LoginScreen = memo(({ navigation }) => {
  const appUser = useContext(UserContext);
  const [foo, setFoo] = useState(false);

  const userLogin = async () => {
    let response = await fetch('blahblahblah');
    if (response.is_success) {
      appUser.login(user);
    } else {
      // fail on login, error handling
    }
  };

  const toggleFoo = () => {
    setFoo(!foo);
    console.log("current foo", foo);
  };

  console.log("render Login Screen"); // check render times
  return (
    <View>
      <Text>Login Screen</Text>
      <Button onPress={userLogin} title="Login" />
      <Button onPress={toggleFoo} title="Toggle Foo" />
    </View>
  );
});

export default LoginScreen;

案例 2:使用 HOC 封装的组件

import React, { Component } from 'react';
import { View, Text } from 'react-native';

import Button from '@Components/Button';
import { withUserContext } from '@Contexts/User';
import UserService from '@Services/User';

class LoginScreen extends Component {
  state = { foo: false };

  userLogin = async () => {
    let response = await UserService.login();
    if (response.is_success) {
      login(user);      // function from UserContext
    } else {
      // fail on login, error handling
    }
  };

  toggleFoo = () => {
    const { foo } = this.state;
    this.setState({ foo: !foo });
    console.log("current foo", foo);
  };

  render() {
    console.log("render Login Screen"); // check render times
    return (
      <View>
        <Text>Login Screen</Text>
        <Button onPress={userLogin} title="Login" />
        <Button onPress={toggleDisable} title="Toggle" />
      </View>
    );
  }
}

结果


两种情况开始时的渲染时间相同:

render Login Screen
Button render
Button render

但是当我按下“切换”按钮时,状态发生了变化,结果如下:

案例 1:带有 Hooks 的功能组件

render Login Screen
Button render
Button render

案例 2:使用 HOC 封装的组件

render Login Screen

问题


虽然按钮组件不是一大堆代码,但考虑到两种情况之间的重新渲染时间,Case 2 的性能应该比Case 1 更好。 但是,考虑到代码的可读性,我绝对喜欢使用钩子而不是使用 HOC。 (特别是函数:appUser.login()login()

所以问题来了。有没有什么解决方案可以保持两种尺寸的好处,减少使用钩子时的重新渲染时间?谢谢。

【问题讨论】:

    标签: reactjs react-native react-hooks react-context


    【解决方案1】:

    原因是在功能组件中,每当组件重新渲染时,新的userLogin created => Button 组件被重新渲染。

    const userLogin = async () => {
        const response = await fetch("blahblahblah")
        if (response.is_success) {
          appUser.login(user)
        } else {
          // fail on login, error handling
        }
     }
    

    您可以使用 useCallback 来记忆 userLogin 函数 + 用 React.memo 包装 Button 组件(就像您所做的那样)防止不必要的重新渲染:

    const userLogin = useCallback(async () => {
        const response = await fetch("blahblahblah")
        if (response.is_success) {
          appUser.login(user)
        } else {
          // fail on login, error handling
        }
    }, [])
    

    在类组件中没有发生这种情况的原因是当类组件重新渲染时,只有render函数被触发(当然还有一些其他生命周期函数,例如shoudlComponentUpdate,componentDidUpdate触发)。 ==> userLogin 不改变 ==> Button 组件不重新渲染。

    这里是great article 看看useCallback + memo

    注意:当你使用Context时,memo不能阻止组件,也就是Consumer,如果Context Provider的值改变了,则重新渲染。 例如: 如果你在UserContext 中调用setUser => UserContext 重新渲染 => value={{value: user, login: login, logout: logout}} 更改 => LoginScreen 重新渲染。您不能使用shouldComponentUpdate(类组件)或memo(功能组件)来防止重新渲染,因为它不是通过props 更新的,而是通过Context Provide 的值更新的

    【讨论】:

    • 非常感谢您回答这个问题。 ;)
    【解决方案2】:

    即使您在功能组件的情况下使用memo,两个按钮都会重新呈现的原因是因为函数引用在每次重新呈现时都会更改,因为它们是在功能组件中定义的。

    如果在类组件的渲染中使用arrow functions 也会发生类似情况

    在类的情况下,函数引用不会随着您定义它们的方式而改变,因为函数是在您的渲染方法之外定义的

    要优化重新渲染,您应该使用useCallback 挂钩来记忆您的函数引用

    const LoginScreen = memo(({ navigation }) => {
      const appUser = useContext(UserContext);
      const [foo, setFoo] = useState(false);
    
      const userLogin = useCallback(async () => {
        let response = await fetch('blahblahblah');
        if (response.is_success) {
          appUser.login(user);
        } else {
          // fail on login, error handling
        }
      }, []); // Add dependency if need i.e when using value from closure
    
      const toggleFoo = useCallback(() => {
        setFoo(prevFoo => !prevFoo); // use functional state here
      }, []);
    
      console.log("render Login Screen"); // check render times
      return (
        <View>
          <Text>Login Screen</Text>
          <Button onPress={userLogin} title="Login" />
          <Button onPress={toggleFoo} title="Toggle Foo" />
        </View>
      );
    });
    
    export default LoginScreen;
    

    另请注意,React.memo 无法防止由于上下文值更改而重新渲染。另请注意,在将值传递给上下文提供者时,您也应该使用useMemo

    export const UserContextProvider = ({ children }) => {
      let [ user, setUser ] = useState(null);
    
      const login = useCallback((loginUser) => {
        if (loginUser instanceof User) { setUser(loginUser); }
      }, []);
    
      const logout = useCallback(() => {
        setUser(null);
      }, []);
    
      const value = useMemo(() => ({
         value: user,
         login: login,
         logout: logout,
      }), [user, login, logout]); 
      /*
         Note that login and logout functions are implemented using `useCallback` and 
         are created on initial render only and hence adding them as dependency here 
         doesn't make a difference and will definitely not lead to new referecne for 
          value. Only `user` value change will create a new object reference
      */
      return (
        <UserContext.Provider value={value}>
          {children}
        </UserContext.Provider>
      );
    };
    

    【讨论】:

    • 非常感谢您回答这个问题,也感谢您指出我可以做的其他一些可能的优化。欣赏!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-06-13
    • 2021-07-06
    • 1970-01-01
    • 1970-01-01
    • 2023-02-13
    • 2019-04-14
    • 1970-01-01
    相关资源
    最近更新 更多