Skip to content

Instantly share code, notes, and snippets.

@Akiyamka
Last active June 2, 2020 17:24
Show Gist options
  • Save Akiyamka/457fd24505a35d375a88130bae35ebcb to your computer and use it in GitHub Desktop.
Save Akiyamka/457fd24505a35d375a88130bae35ebcb to your computer and use it in GitHub Desktop.
Почему вызывается рендер компонента

Как и почему реакт обновляет компоненты и что с этим делать

Как работает реакт (без подробностей)

Известно что самая медленная операция в барузере - изменение DOM дерева. И что реакт стремится светси эти обновления к минимуму. Для этого он:

  1. На стадии рендера - строит дешевый вируатльный vDOM (который браузер не ренедерит)
  2. На стадии сравнения - находит разницу между VDOM и текущим DOM (для этого у него есть копия последнего примененного VDOM, т.е. по сути VDOM-ов два - current и new).
  3. На стадии обновления - вызывает браузерную апи для того чтобы исправить разницу в реальном DOM и новом VDOM

Например:

function App() {
  const [name, setName] = useState('world');
  return <div>
    <h1>Hello {name}</h1>
    <button onClick={() => setName('react')}> I am react </button>
  </div>
}
  1. Первая стадия:
div
    ├── h1
    │   └── Hello world
    └── button
        └── A am react
  1. Вторая стадия пропускается (старого стостояния еще нет),
  2. Третья стадия создает div, h1, button с соотвествующим текстом и слушателем событий.

После нажатия кнопки:

  1. Первая стадия:
div
    ├── h1
    │   └── Hello react
    └── button
        └── A am react
  1. Вторая стадия сравнивает новый vdom со старым и находит разницу - отличается текст внутри h1 тэга
  2. Третья стадия находит этот тэг и меняет innerText у него на 'Hello React'.

Понятно что третья стадия вызывается когда есть изменения между старым и новым vdom, а вторая сразу после первой. Но ...

Когда вызывается render стадия?

  1. Если вызывается сеттер из useState эффекта (в нашем примере setName) который устанавливает новое значение* (Однако есть один нюанс к которому вернусь позже.)

  2. Если встроенный или custom effect (useCallback например) возращает новое значение*. (useEffect не вызывает рендер т.к. ничего не возвращает)

  3. Если что-либо из выше-перечисленного произошло в родительском компоненте


  • новое оно или нет определяется сравнением с помощью метода Object.is(old, new)

Таким образом - изменения в верхушке дерева приводят к перезапуску render стадии у всех детей и детей его детей и так далее.

А теперь вернемся к нюансу о котором я говорил выше:
в случае установки того-же значения еще раз через сеттер useState - рендер выполнится один раз, но только для компонента в котором он поменял значение.

Почему так и что с этим делать

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

import { memo } from 'react';

function Hello({ name }) {
  return <h1>{ name }</h1>
}

export memo(Hello);

Здесь мы оберунли компонент в memo и тем самым создали компонент который запустит render стадию если:

  1. Если вызывается сеттер из useState эффекта (в нашем примере setName) который устанавливает новое значение*, (Однако есть один нюанс к которому вернусь позже.)

  2. Если встроенный или custom effect (useCallback например) возращает новое значение*. (useEffect не вызывает рендер т.к. ничего не возвращает)

3. Если что-либо из выше-перечисленного произошло в родительском компоненте

  1. Если у компонента изменится один из props

Это может ускорить отрисовку приложения, но только в том случае если в компоненте много jsx и мало props, в противном случае (много пропсов и мало jsx) расходы на сравнение старых и новых значений пропсов превысят время вызова хорошо оптимизированных функций построения vdom дерева.

Cтоит отметить, что в memo можно передать свою функцию сравнения что иногда бывает полезно.
Еще одно замечание - библиотека react-redux поставляет функцию connect(mapStateToProps, mapDispatchToProps), которая автоматически мемоизирует обернутый компонент.

Из весего этого можно сделать важный вывод - уменьшить количество вызовов функции render не равно ускорить приложение.

А как тогда ускорить приложение?

  • Не делать тяжелых вычислений в реакте, выносить их в store
  • Не делать тяжелых вычислений на фронтенде - выносить их в бекенд
  • Оборачивать компоненты в memo там где это необходимо
  • Если вычисления все-же необходимо делать в компоненте - делать это внутри useEffect который защищен от лишних рендеров

Так же стоит заметить что функции обернутые в memo требуют особого обращения:
Как знаем, новое значение в пропсах или нет по умолчанию определяется с помощью Object.is() - Довольно часто, например при написании своего эффекта - мы можем использовать конструктор обьекта или массива (в js - тоже обьекта :), например:

export function useConvertSomething(something = []) {
  const [converted, setConverted] = useState([]);
  
  useEffect(() => {
    const result = convert(something);
    setConverted(result);
  }, [something])
  
  return converted;
}

В данном случае мы использовали конструктор массива [] дважды - как значение по умолчанию для аругмента something и как значение по умолчанию для useState. С точки зрения Object.is([], []) === false, так что будет меньше рендеров если написать так:

export function useConvertSomething(something) {
  const [converted, setConverted] = useState([]); // useState установит значение по-умолчанию только при первом рендере
  
  useEffect(() => {
    if (Array.isArray(something)) {
      const result = convert(something);
      setConverted(result);
    }
  }, [something])
  
  return converted;
}

Теперь эффект не будет возвращать новый массив каждый раз когда в него передали undefined. А значит не будут вызыватся эффекты которые от него зависят и компоненты обернутые в memo которые получают этот реультат как props.

Еще один способ убить мемоизацию - стрелочные функции в jsx, например

<ul>
 { items.map(item => <Row onClick={() => selectItem(item.id)}> {item.name} </Row>) }
<ul>

Обычно в этом нет ничего плохого, но если <Row> это мемоизирвоанный компонент и/или в нем есть useEffect зависящий от onClick пропса, то в этом случае рендер/effect будет вызыватся каждый раз при рендере родителя по сколько мы каждый раз в цикле создаем новую функцию. В качестве оптимизации здесь можно применить useCallback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment