React Native - 在使用 React 钩子时减少渲染时间以优化性能

Posted

技术标签:

【中文标题】React Native - 在使用 React 钩子时减少渲染时间以优化性能【英文标题】:React Native - reduce render times to optimize performance while using React hooks 【发布时间】: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()

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

【问题讨论】:

【参考方案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>
  );
;

【讨论】:

非常感谢您回答这个问题,也感谢您指出我可以做的其他一些可能的优化。欣赏!

以上是关于React Native - 在使用 React 钩子时减少渲染时间以优化性能的主要内容,如果未能解决你的问题,请参考以下文章

使用 react-native-firebase 在 React Native 上自定义通知

Flipper 不会在任何断点处暂停 - react-native - react-native-reanimated

在带有 wix/react-native-navigation 的模态中使用 react-native-gesture-handler (RNGH)

使用 React-Native-Router-Flux 在 React Native 中嵌套场景

使用 react-native-pdf 在 react-native 上获取准确的 x 和 y 坐标

React-native:如何在 React-native 中使用(和翻译)带有 jsx 的 typescript .tsx 文件?