React Perf

Some patterns

React.Component

Always re-renders

React.PureComponent

Renders with shallow-equal comparison 

  • Иммутабельный стейт (редакс, компонента)
  • Иммутабельные пропсы

=>

  • Примитивы можно сравнивать по значению
  • Объекты можно сравнивать по ссылке

=>

Используем shallow equal

(сравнение свойств через ===)

/* @flow */

import React from 'react';

const Counter = ({count}) => (
    <div>
        {count}
    </div>
)

export default Counter


Было

/* @flow */

import React from 'react';
import {pure} from 'recompose'


const Counter = ({count}) => (
    <div>
        {count}
    </div>
)

export default pure(Counter)


Стало

Pure Anti-patterns

Keep your links

const Controls = ({updateData}) => (
    <section>
        <Filters>
            <Label text="Обновить данные">
                <Button onClick={() => updateData()}>click me!</Button>
            </Label>
        </Filters>
    </section>
);
Keep your links
const Controls = ({updateData}) => (
    <section>
        <Filters>
            <Label text="Обновить данные">
                <Button onClick={() => updateData()}>click me!</Button>
            </Label>
        </Filters>
    </section>
);

Было

Стало

import {withHandlers} from 'recompose'

const enhance = withHandlers({
    onClick: ({updateData}) => () => updateData(),
})

export const Controls = ({onClick}) => (
    <section>
        <Filters>
            <Label text="Обновить данные">
                <Button onClick={onClick}>click me!</Button>
            </Label>
        </Filters>
    </section>
);

export default enhance(Controls)
const Item = ({updateItem, id}) => (
    <article>
        <Button onClick={() => updateItem(id)}>click me!</Button>
    </article>
);
Keep your links
const Item = ({updateItem, id}) => (
    <article>
        <Button onClick={() => updateItem(id)}>click me!</Button>
    </article>
);

Было

Стало

import {withHandlers} from 'recompose'

const enhance = withHandlers({
    onClick: ({updateItem, id}) => () => updateItem(id),
})

const Item = ({onClick}) => (
    <article>
        <Button onClick={onClick}>click me!</Button>
    </article>
);

export default enhance(Item)
const Items = ({updateItem, items}) => (
    <article>
        {items.map(({id}) => (
            <Button onClick={() => updateItem(id)}>click me!</Button>
        ))}
    </article>
);
Keep your links
const Items = ({updateItem, items}) => (
    <article>
        {items.map(({id}) => (
            <Button onClick={() => updateItem(id)}>click me!</Button>
        ))}
    </article>
);

Было

Стало

import {withHandlers} from 'recompose'

const enhance = withHandlers({
    onClick: ({updateItem}) => event => updateItem(event.target.value),
})

const Items = ({onClick, items}) => (
    <article>
        {items.map(({id}) => (
            <Button key={id} value={id} onClick={onClick}>click me!</Button>
        ))}
    </article>
);

export default enhance(Items)

Не подходит стандартный html-тэг? (value, name и т.д.)

Используй dataset

import {withHandlers} from 'recompose'

const enhance = withHandlers({
    onClick: ({updateItem}) => event => updateItem(event.target.dataset.id),
})

const Items = ({onClick, items}) => (
    <article>
        {items.map(({id}) => (
            <Button key={id} data-id={id} onClick={onClick}>click me!</Button>
        ))}
    </article>
);

export default enhance(Items)
import {connect} from 'react-redux'

import {updateUser} from '~/actions/users'


export const UsersSection = ({updateUser, users}) => (
    <section>
        <UserList users={users} updateUser={updateUser} />
    </section>
)


export default connect(
    ({users, currentGender}) => ({
        users: users.filter(({gender}) => gender === currentGender)
    }),
    {updateUser},
)(UsersSection)
Keep your links
import {connect} from 'react-redux'
import {createSelector} from 'reselect'

import {updateUser} from '~/actions/users'


export const UsersSection = ({updateUser, users}) =>
    <UserList users={users} updateUser={updateUser} />

const getUsersByGender = createSelector(
    ({users}) => users, 
    ({currentGender}) => currentGender,
    (users, currentGender) => users.filter(({gender}) => gender === currentGender),
)

/* with ramda helpers
const getUsersByGender = createSelector(
    prop('user'),
    prop('currentGender'),
    (users, currentGender) => filter(propEq('gender', currentGender), users),
)
*/


export default connect(
    state => ({users: getUsersByGender(state)}),
    {updateUser},
)(UsersSection)

Стало

const Header = () => (
    <section>
        <Filters>
            <Label text="Даты">
                <DatePickerDropdown
                    presets={[]}
                    range
                    placeholder="Выберите даты"
                    format="YYYY-MM-DD"
                    name={{from: 'fromDate', to: 'toDate'}}
                />
            </Label>
        </Filters>
    </section>
);
Keep your links

Стало

const presets = []
const name = {from: 'fromDate', to: 'toDate'}

const Header = () => (
    <section>
        <Filters>
            <Label text="Даты">
                <DatePickerDropdown
                    presets={presets}
                    range
                    placeholder="Выберите даты"
                    format="YYYY-MM-DD"
                    name={name}
                />
            </Label>
        </Filters>
    </section>
)
const Header = ({presets = [], name = {from: 'fromDate', to: 'toDate'}}) => (
    <section>
        <Filters>
            <Label text="Даты">
                <DatePickerDropdown
                    presets={presets}
                    range
                    placeholder="Выберите даты"
                    format="YYYY-MM-DD"
                    name={name}
                />
            </Label>
        </Filters>
    </section>
);
Keep your links
import {defaultProps} from 'recompose'

const enhance = defaultProps({
    presets: [],
    name: {from: 'fromDate', to: 'toDate'}
})

const Header = ({name, presets}) => (
    <section>
        <Filters>
            <Label text="Даты">
                <DatePickerDropdown
                    presets={presets}
                    range
                    placeholder="Выберите даты"
                    format="YYYY-MM-DD"
                    name={name}
                />
            </Label>
        </Filters>
    </section>
)

export default enhance(Header)

Стало

const List = ({children, className}) => (
    <div className={className}>
        {React.cloneElement(children, {'name': 'list'})}
    </div>
)
Keep your links
const List = ({children, className}) => (
    <div className={className}>
        {React.cloneElement(children, {'name': 'list'})}
    </div>
)

Было

Стало

const customProps = {'name': 'list'}

const List = ({children, className}) => (
    <div className={className}>
        {React.cloneElement(children, customProps)}
    </div>
)
const List = ({children, className, onChange}) => (
    <ul className={className}>
        {Children.map(children, ((child, i) => (
            <li key={i}>{React.cloneElement(child, {onChange, 'data-index': i})}</li>
        )))}
    </ul>
)
Keep your links

Решения присылайте на enload@yandex-team.ru

Итого

  • Декомпозиция
  • Иммутабельные стейты и пропсы
  • Сравниваем пропсы через:
    • pure
    • shouldUpdate
    • onlyUpdateForKeys
    • и т.д.
  • Храним ссылки
    • defaultProps, withProps и пр.
    • для колбэков: withHandlers, withStateHandlers и т.д.
    • reselect
  • Мемоизация вычислений через reselect
  • Следим за производительностью через:
  • Внимательно относимся к проверкам, не стоит допускать несколько сравнений
    • Например, в connect уже есть сравнение, соотв. либо не нужно компонент делать  pure, либо нужно использовать connect без сравнения
  • Не все компоненты обязаны быть pure. Важнее делать такими целые ветки

React Perf

By Artur Kenzhaev