React Native - сокращение времени рендеринга для оптимизации производительности при использовании перехватчиков React - PullRequest
4 голосов
/ 26 мая 2020

Фон


После выпуска 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: Функциональный компонент с крючками

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: Компонент, обернутый HO C

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

Но пока я нажимаю кнопку «Toggle», состояние меняется, и вот результат:

Случай 1: Функциональный компонент с хуками

render Login Screen
Button render
Button render

Случай 2: Компонент, обернутый HO C

render Login Screen

Вопросы


Хотя компонент Button не представляет собой большой набор кодов, учитывая время повторного рендеринга между двумя случаями, Case 2 должен иметь лучшую производительность, чем Case 1. Однако, учитывая читаемость кода, я определенно люблю использовать хуки больше, чем использовать HO C. (Особенно функции: appUser.login() и login())

Итак, вот вопрос. Есть ли какое-нибудь решение, в котором я могу сохранить преимущества обоих размеров, уменьшив время повторного рендеринга при использовании хуков? Спасибо.

Ответы [ 2 ]

2 голосов
/ 11 июня 2020

Причина, по которой обе кнопки повторно отрисовываются, даже если вы используете 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>
  );
};
2 голосов
/ 11 июня 2020

Причина в функциональном компоненте, когда компонент повторно рендерится, новый 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 функции + обертывания Button компонента с React.memo (как и вы), чтобы предотвратить нежелательную повторную визуализацию:

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 компонент не перерисовывается.

Это отличная статья , чтобы взглянуть на useCallback + memo

Примечание. Когда вы используете Context, memo не может предотвратить повторную визуализацию компонента, который является Consumer, если значения контекстного провайдера изменились. Например: если вы вызываете setUser в UserContext => UserContext re-render => value={{value: user, login: login, logout: logout}} change => LoginScreen re-render. Вы не можете использовать shouldComponentUpdate (компонент класса) или memo (функциональный компонент) для предотвращения повторного рендеринга, потому что он не обновляется через props, он обновляется через значение Context Provide

...