Сделайте ваши функциональные React компоненты на 45% быстрее уже сейчас (перевод)

[Перевод статьи 45% Faster React Functional Components, Now автора Philippe Lehoux]

Вы можете перейти к TL;DR в конце статьи, если хотите сразу увидеть то одно небольшое изменение в вашем React коде, которое ускорит его. Но если вы хотите подробностей, то читайте дальше.

Я работаю в Missive, это клиентское React приложение для обмена email/сообщениями между командой. Оно использует многоколоночный макет для отображения диалогов. При нажатии на клавиши ⬆︎⬇ приложение переходит к предыдущему и следующему диалогу соответственно. Нужно, чтобы этот переход длился всего несколько миллисекунд, чтобы пользователь не заметил задержки между переходами.

В окне диалога может находится сотни комментариев, email и событий, каждый из которых состоит из отдельных компонентов. Для оптимизации навигации между диалогами, мы начали переписывать state-содержащие (stateful) компоненты в функциональные.

Для примера, Avatar компонент превратился из:

class Avatar extends React.Component {
render() {
return <img src={this.props.url} />;
}
}

… в:

// упрощённая версия нашего реального компонента
const Avatar = (props) => {
return <img src={props.url} />;
}

Как видете, функциональный компонент, это простая JS функция, которая возвращает элемент. Функциональные компоненты ещё также называют stateless (не содержащими состояние) компонентами.

Мы думали, что это изменение мгновенно улучшит производительность, потому что при переходе между диалогами в Missive React производит unmount и mount для сотен компонентов. Если вы думаете, что функциональные компоненты могут избежать эти монтирования / размонтирования, а также любые события жизненного цикла, просто потому, что они всего лишь функции, то вы ошибаетесь.

Оригинальные заметки релиза React 0.14 подтверждают это (выделено мной):

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

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

А что, если бы могли пропустить внутренние вычисления React для этих компонентов? Вместо того, чтобы монтировать их как компоненты, давайте просто вызовем их как обычные JS функции, чем собственно они и являются. Другими словами, нам нужно просто изменить JSX тег Avatar на фигурные скобки и вызвать функцию напрямую. Например, так:

ReactDOM.render(
<div>
- <Avatar url={avatarUrl} />
+ {Avatar({ url: avatarUrl })}
<div>{commentBody}</div>
</div>,
mountNode);
// Скомпилированный JavaScript
ReactDOM.render(React.createElement(
'div',
null,
- React.createElement(Avatar, { url: avatarUrl }),
+ Avatar({ url: avatarUrl }),
React.createElement(
'div',
null,
commentBody
)
), mountNode);

Мы просто устраняем один большой вызов React.createElement и все события жизненного цикла, идущие вместе с ним! Нет необходимости ждать, пока команда React реализует внутреннюю оптимизацию.

Чтобы измерить изменения, я создал этот тест, результаты довольно ошеломляющие! Просто при конвертировании классов в функциональные компоненты, мы получили прирост производительности в 6%. Но при обычном вызове компонента как функцию, мы получили около 45% прироста общей производительности.

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

TL;DR

Прямой вызов функционального компонента как функцию намного быстрее, чем монтирование его через React.createElement. Вы можете использовать это в JSX сегодня, например, так:

// упрощённая версия реального компонента
const Avatar = (props) => {
return <img src={props.url} />;
}
ReactDOM.render(
<div>
- <Avatar url={avatarUrl} />
+ {Avatar({ url: avatarUrl })}
<div>{commentBody}</div>
</div>,
mountNode);
// Скомпилированный JavaScript
ReactDOM.render(React.createElement(
'div',
null,
- React.createElement(Avatar, { url: avatarUrl }),
+ Avatar({ url: avatarUrl }),
React.createElement(
'div',
null,
commentBody
)
), mountNode);