Нарушение производительности при создании обработчиков на каждом рендере с помощью перехватчиков реакции

В настоящее время я очень удивлен примерами использования нового API реагирующих хуков и тем, что вы можете с ним сделать.

Вопрос, который возник во время эксперимента, заключался в том, насколько дорого всегда создавать новую функцию-обработчик, чтобы просто выбросить ее при использовании useCallback.

Учитывая этот пример:

const MyCounter = ({initial}) => {
    const [count, setCount] = useState(initial);

    const increase = useCallback(() => setCount(count => count + 1), [setCount]);
    const decrease = useCallback(() => setCount(count => count > 0 ? count - 1 : 0), [setCount]);

    return (
        <div className="counter">
            <p>The count is {count}.</p>
            <button onClick={decrease} disabled={count === 0}> - </button>
            <button onClick={increase}> + </button>
        </div>
    );
};

Хотя я обертываю обработчик в useCallback чтобы не передавать новый обработчик каждый раз, когда он useCallback функцию встроенной стрелки по-прежнему нужно создавать только для исключения в большинстве случаев.

Вероятно, не имеет большого значения, если я рендую только несколько компонентов. Но насколько велико влияние на производительность, если я делаю это тысячи раз? Есть ли заметное снижение производительности? И как бы избежать этого? Возможно, фабрика статических обработчиков вызывается только тогда, когда нужно создать новый обработчик?

Ответы

Ответ 1

Ответы на часто задаваемые вопросы дают объяснение этому

Замедлены ли хуки из-за создания функций в рендере?

Нет. В современных браузерах сырая производительность замыканий по сравнению с классы существенно не отличаются, за исключением экстремальных сценариев.

Кроме того, учтите, что конструкция крючков более эффективна в пара способов:

Крючки избегают больших накладных расходов, которые требуют классы, таких как стоимость создания экземпляров класса и привязки обработчиков событий в Конструктор.

Идиоматическому коду, использующему Hooks, не нужно глубокое дерево компонентов Вложение, которое распространено в кодовых базах, которые используют более высокий порядок компоненты, рендеринг реквизита и контекст. С меньшими деревьями компонентов, У Реакта меньше работы.

Традиционно проблемы производительности вокруг встроенных функций в React были связаны с тем, как передача новых обратных вызовов при каждом разрыве рендеринга shouldComponentUpdate оптимизации в дочерних компонентах. крюки подойти к этой проблеме с трех сторон.

Таким образом, общие преимущества, предоставляемые хуками, намного превышают затраты на создание новых функций

Более того, для функциональных компонентов вы можете оптимизировать, используя useMemo, чтобы компоненты выполняли повторный рендеринг, когда их реквизиты не изменились.

Ответ 2

Но насколько велико влияние на производительность, если я делаю это тысячи раз? Есть ли заметное снижение производительности?

Это зависит от приложения. Если вы просто визуализируете 1000 строк счетчиков, это, вероятно, нормально, как видно из фрагмента кода ниже. Обратите внимание, что если вы просто изменяете состояние отдельного <Counter/>, только этот счетчик будет перерисован, остальные 999 счетчиков не будут затронуты.

Но я думаю, что вы обеспокоены несущественными вещами здесь. В реальных приложениях вряд ли будет отображаться 1000 элементов списка. Если ваше приложение должно отображать 1000 элементов, возможно, что-то не так с вашим дизайном.

  1. Вы не должны рендерить 1000 предметов в DOM. Это обычно плохо с точки зрения производительности и UX, с современными JavaScript-фреймворками или без них. Вы можете использовать методы управления окнами и отображать только те элементы, которые видите на экране, остальные элементы вне экрана могут находиться в памяти.

  2. shouldComponentUpdate (или useMemo), чтобы другие элементы не перерисовывались, если компонент верхнего уровня должен перерисовываться.

  3. Используя функции, вы избегаете накладных расходов на классы и некоторые другие связанные с классами вещи, которые происходят под капотом, о которых вы не знаете, потому что React делает это для вас автоматически. Вы теряете некоторую производительность из-за вызова некоторых хуков в функциях, но вы получаете некоторую производительность и в других местах.

  4. Наконец, обратите внимание, что вы вызываете хуки useXXX и не выполняете функции обратного вызова, которые вы передали в хуки. Я уверен, что команда React хорошо поработала над тем, чтобы облегчить вызовы с помощью ловушек, вызовы не должны быть слишком дорогими.

И как бы избежать этого?

Я сомневаюсь, что будет сценарий реального мира, в котором вам нужно будет создавать объекты с состоянием тысячу раз. Но если вам действительно нужно, было бы лучше поднять состояние до родительского компонента и передать обратный вызов значения и увеличения/уменьшения в качестве опоры для каждого элемента. Таким образом, ваши отдельные элементы не должны создавать обратные вызовы модификатора состояния и могут просто использовать реквизит обратного вызова от своего родителя. Кроме того, дочерние компоненты без сохранения состояния упрощают реализацию различных известных оптимизаций перфорации.

Наконец, я хотел бы повторить, что вам не следует беспокоиться об этой проблеме, потому что вы должны стараться избегать попадания в такую ситуацию, а не справляться с ней, опираясь на такие методы, как использование окон и разбиение на страницы - только загружая данные, которые вы нужно показать на текущей странице.

const Counter = ({ initial }) => {
  const [count, setCount] = React.useState(initial);

  const increase = React.useCallback(() => setCount(count => count + 1), [setCount]);
  const decrease = React.useCallback(
    () => setCount(count => (count > 0 ? count - 1 : 0)),
    [setCount]
  );

  return (
    <div className="counter">
      <p>The count is {count}.</p>
      <button onClick={decrease} disabled={count === 0}>
        -
      </button>
      <button onClick={increase}>+</button>
    </div>
  );
};

function App() {
  const [count, setCount] = React.useState(1000);
  return (
    <div>
      <h1>Counters: {count}</h1>
      <button onClick={() => {
        setCount(count + 1);
      }}>Add Counter</button>
      <hr/>
      {(() => {
        const items = [];
        for (let i = 0; i < count; i++) {
          items.push(<Counter key={i} initial={i} />);
        }
        return items;
      })()}
    </div>
  );
}


ReactDOM.render(
  <div>
    <App />
  </div>,
  document.querySelector("#app")
);
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

Ответ 3

Вы правы, в больших приложениях это может привести к проблемам с производительностью. Привязка обработчика до его передачи компоненту исключает возможность повторного рендеринга дочерним компонентом.

<button onClick={(e) => this.handleClick(e)}>click me!</button>
<button onClick={this.handleClick.bind(this)}>click me!</button>

Оба эквивалентны. Аргумент e, представляющий событие React, в то время как с функцией стрелки мы должны передать его явно, с помощью bind любые аргументы автоматически передаются.

Ответ 4

Одним из способов может быть запоминание обратных вызовов для предотвращения ненужных обновлений дочерних компонентов.
Вы можете прочитать больше об этом здесь.

Кроме того, я создаю пакет npm useMemoizedCallback, который, я надеюсь, поможет любому, кто ищет решение, повысить производительность.