Всем привет! Хочу поделиться своим переводом статьи React is Slow, React is Fast: Optimizing React Apps in Practice автора Francois Zaninotto. Надеюсь, это кому-то будет полезным.


Краткое содержание:


  1. Измерение производительности React
  2. Почему ты обновился?
  3. Оптимизация через разбиение на компоненты
  4. shouldComponentUpdate
  5. Recompose
  6. Redux
  7. Reselect
  8. Остерегайтесь объектных литералов в JSX
  9. Заключение

React может быть медленным. Я хочу сказать, что любое React приложение среднего размера может оказаться медленным. Но прежде, чем искать ему замену, вы должны знать, что и любое среднее приложение на Angular или Ember может также оказаться медленным.


Хорошая новость в том, что если вы действительно заботитесь о производительности, то сделать React приложение очень быстрым довольно легко. Об этом — далее в статье.


Измерение производительности React


Что я подразумеваю под "медленным"? Позвольте привести пример:


Я работаю над одним open-source проектом, который называется admin-on-rest, использующий material-ui и Redux для предоставления графического интерфейса (GUI) админ-панели для любого API. В этом приложении есть страница, отображающая список записей в виде таблицы. Когда пользователь изменяет порядок сортировки, или переходит на следующую страницу, или фильтрует вывод, интерфейс не так отзывчив, как хотелось бы.


На следующем анимированном скринкасте, замедленном в 5 раз, показано, как происходит обновление:


Анимированный скринкаст, замедленный в 5 раз, показывает, как происходит обновление

Чтобы понять, что происходит, я добавляю в конце URL ?react_perf. Это активирует возможность профилирования компонентов, которая доступна с версии React 15.4. Сначала я жду начальной загрузки таблицы с данными. Далее, я открываю вкладку Timeline в инструментах разработчика в Chrome, кликаю на кнопку "Запись" и нажимаю на заголовок таблицы на странице, чтобы обновить порядок сортировки.


После обновления данных, я снова кликаю на кнопку записи, чтобы остановить её. Chrome отобразит жёлтый график под меткой "User Timing".


Chrome отображает жёлтый график под меткой User Timing

Если вы никогда не видели этого графика, он может показаться пугающим, но, на самом деле, им очень просто пользоваться. Этот график показывает время работы каждого из ваших компонентов. Он не показывает время внутренних компонентов React (вы всё равно не можете их оптимизировать), таким образом он позволяет вам сфокусироваться на оптимизации своего собственного кода.


Временная шкала выводит этапы записи работы приложения и позволяет приблизить тот момент, когда я кликнул на заголовке таблицы:


Временная шкала выводит этапы записи работы приложения

Похоже, что моё приложение перерисовывает компонент <List> сразу после клика на кнопку сортировки, и перед получением данных через REST. Это занимает более 500 мс. Приложение просто обновляет иконку сортировки в заголовке таблицы и отображает серый экран, обозначающий загрузку данных.


Иначе говоря, приложение занимает 500 мс, чтобы визуально отобразить ответную реакцию на клик. Полсекунды это значительный показатель — эксперты по UI говорят, что пользователи считают реакцию приложения мгновенной, только когда она меньше 100 мс. Реакция приложения более 100 мс — вот то, что я называю "медленным".


Почему ты обновился?


На графике выше можно увидеть много крошечных ямок. Это плохой знак, так как это означает, что множество компонентов перерисовались. На графике видно, что обновление <Datagrid> занимает больше всего времени. Почему приложение обновило всю таблицу с данными прежде, чем получило новые данные? Давайте разбираться.


Попытки понять причины перерисовки часто подразумевают добавление console.log() в render() функцию. Для функциональных компонентов вы можете использовать следующий компонент высшего порядка (HOC):


// src/log.js
const log = BaseComponent => props => {
 console.log(`Rendering ${BaseComponent.name}`);
 return <BaseComponent {…props} />;
}
export default log;

// src/MyComponent.js
import log from ‘./log’;
export default log(MyComponent);

Совет: стоит также отметить why-did-you-update — ещё один инструмент для эффективности React. Этот npm пакет заставляет React выводить в консоль предупреждения всякий раз, когда компонент перерисовывается с теми же props. Предупреждаю: вывод в консоли довольно подробный и он не работает с функциональными компонентами.

В примере, когда пользователь кликает на заголовке столбца, приложение производит действие, изменяющее state: порядок сортировки списка (currentSort) обновлён. Это изменение state запускает перерисовку страницы <List>, которая в свою очередь перерисовывает весь компонент <Datagrid>. Мы хотим, чтобы заголовок таблицы немедленно отрисовал изменение иконки сортировки, как ответ на действия пользователя.


Обычно, React становится медленным не из-за одного медленного компонента (что будет отображено на графике, как одна большая яма). В большинстве же случаев, React становится медленным из-за бесполезной перерисовки множества компонентов.


Вы возможно читали, что VirtualDOM в React очень быстрый. Это правда, но в приложении среднего размера полная перерисовка может легко содержать в себе отрисовку сотни компонентов. Даже самый быстрый шаблонизатор VirtualDOM не может сделать это меньше, чем за 16 ms.


Оптимизация через разбиение на компоненты


Вот метод render() компонента <Datagrid>:


// Datagrid.js
render() {
    const { 
        resource,
        children,
        ids,
        data,
        currentSort
    } = this.props;

    return (
        <table>
            <thead>
                <tr>
                    {Children.map(children, (field, index) =>
                        <DatagridHeaderCell
                            key={index}
                            field={field}
                            currentSort={currentSort}
                            updateSort={this.updateSort}
                        />
                    )}
                </tr>
            </thead>
            <tbody>
                {ids.map(id => (
                    <tr key={id}>
                        {Children.map(children, (field, index) =>
                            <DatagridCell
                                record={data[id]}
                                key={`${id}-${index}`}
                                field={field}
                                resource={resource}
                            />
                        )}
                    </tr>
                ))}
            </tbody>
        </table>
    );
}

Кажется, что это очень простая реализация табличных данных, но она крайне неэффективна. Каждый <DatagridCell> вызывает отрисовку как минимум двух или трёх компонентов. Как вы можете увидеть на анимированном скринкасте интерфейса в начале статьи, список содержит 7 столбцов и 11 строк, и это означает, что 7*11*3 = 231 компонент перерисовывается. И всё это пустая трата времени, так как изменению подвергается только currentSort. Несмотря на то, что React не обновляет реальный DOM (при условии, что VirtualDOM не изменился), то это всё равно занимает около 500 мс для обработки всех компонентов.


Чтобы избежать бесполезной перерисовки тела таблицы, для начала я должен *извлечь* его:


// Datagrid.js
render() {
    const { 
        resource,
        children,
        ids,
        data,
        currentSort
    } = this.props;

    return (
        <table>
            <thead>
                <tr>
                    {React.Children.map(children, (field, index) =>
                        <DatagridHeaderCell
                            key={index}
                            field={field}
                            currentSort={currentSort}
                            updateSort={this.updateSort}
                        />
                    )}
                </tr>
            </thead>
            <DatagridBody resource={resource} ids={ids} data={data}>
                {children}
            </DatagridBody>
            </table>
        );
    );
}

Я создал новый <DatagridBody> компонент путём извлечения логики из тела таблицы:


// DatagridBody.js
import React, { Children } from 'react';
const DatagridBody = ({ resource, ids, data, children }) => (
    <tbody>
        {ids.map(id => (
            <tr key={id}>
                {Children.map(children, (field, index) =>
                    <DatagridCell
                        record={data[id]}
                        key={`${id}-${index}`}
                        field={field}
                        resource={resource}
                    />
                )}
            </tr>
        ))}
    </tbody>
);

export default DatagridBody;

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


shouldComponentUpdate


Документация React очень чётко описывает способ избежать бесполезной перерисовки путём использования shouldComponentUpdate(). По умолчанию, React всегда отображает компонент в VirtualDOM. Иными словами, ваша работа как разработчика, проверять, не изменились ли props компонента, и если нет, то пропустить его перерисовку.


В случае с компонентом <DatagridBody>, в нём не должно быть перерисовки пока props не изменится.


Поэтому компонент должен выглядеть так:


import React, { Children, Component } from 'react';

class DatagridBody extends Component {
    shouldComponentUpdate(nextProps) {
        return (nextProps.ids !== this.props.ids
             || nextProps.data !== this.props.data);
    }

    render() {
        const { resource, ids, data, children } = this.props;
        return (
            <tbody>
                {ids.map(id => (
                    <tr key={id}>
                        {Children.map(children, (field, index) =>
                            <DatagridCell
                                record={data[id]}
                                key={`${id}-${index}`}
                                field={field}
                                resource={resource}
                             />
                        )}
                    </tr>
                ))}
            </tbody>
        );
    }
}

export default DatagridBody;

Совет: Вместо того, чтобы прописывать shouldComponentUpdate() вручную, я мог бы наследовать этот класс от PureComponent вместо Component. PureComponent будет сравнивать все props используя строгое сравнение (===) и перерисовывать, только если props изменились. Но я знаю, что resource и children не могут измениться в данном контексте, поэтому мне не нужно их сравнивать.

Благодаря этой оптимизации перерисовка <Datagrid> после клика на заголовке таблицы пропускает её содержимое и все 231 компонент. Это уменьшило время обновления с 500 мс до 60 мс. Это чистое повышение производительности более чем на 400 мс!


После оптимизации

Совет: Не обманывайтесь шириной графика, он приближен даже больше, чем на предыдущем графике. Это определённо лучше!

Метод shouldComponentUpdate удалил множество ямок на графике и сократил общее время отрисовки. Я могу использовать этот же способ, чтобы избежать ещё больших перерисовок (например, не перерисовывать боковую панель, кнопки действий, не изменившиеся заголовки таблицы, пагинацию). Примерно, после часа возни со всем этим, вся страница отрисовывается всего за 100 мс после клика на заголовок столбца. Это достаточно быстро — даже если осталось ещё что оптимизировать.


Добавление shouldComponentUpdate метода может показаться громоздким, но если вы заботитесь о производительности, то большинство компонентов должны содержать его.


Но не вставляйте его везде, где только можно — выполнение shouldComponentUpdate в довольно простых компонентах может порой замедлить его отрисовку. Не делайте этого слишком рано в жизненном цикле приложения. Добавляйте этот метод лишь по мере роста приложения, когда вы сможете определить проблемы производительности в ваших компонентах.


Recompose


Я не особо доволен предыдущими изменениями в <DatagridBody>: из-за shouldComponentUpdate я должен был трансформировать простой, функциональный компонент в класс. Это добавляет много строк кода, каждая из которых имеет свою цену — в виде написания, отладки и поддержки.


К счастью, вы можете реализовать логику shouldComponentUpdate в компоненте высшего порядка (HOC), благодаря recompose. Это функциональный инструментарий для React, предоставляющий, например, HOC функцию pure():


// DatagridBody.js
import React, { Children } from 'react';
import pure from 'recompose/pure';

const DatagridBody = ({ resource, ids, data, children }) => (
    <tbody>
        {ids.map(id => (
            <tr key={id}>
                {Children.map(children, (field, index) =>
                    <DatagridCell
                        record={data[id]}
                        key={`${id}-${index}`}
                        field={field}
                        resource={resource}
                    />
                )}
            </tr>
        ))}
    </tbody>
);

export default pure(DatagridBody);

Единственное отличие между этим кодом и начальной реализацией — в последней строчке: я экспортирую pure(DatagridBody) вместо DatagridBody. pure похожа на PureComponent, но без лишнего бойлерплейта.


Я даже могу быть более конкретным и ориентироваться только на те props, о которых я точно знаю, что они могут измениться, используя shouldUpdate() вместо pure():


// DatagridBody.js
import React, { Children } from 'react';
import shouldUpdate from ‘recompose/shouldUpdate’;

const DatagridBody = ({ resource, ids, data, children }) => (
    ...
);

const checkPropsChange = (props, nextProps) =>
 (nextProps.ids !== props.ids ||
  nextProps.data !== props.data);

export default shouldUpdate(checkPropsChange)(DatagridBody);

checkPropsChange — это чистая функция, и я даже могу экспортировать её для unit-тестирования.


Библиотека recompose предлагает более эффективные HOC, такие как onlyUpdateForKeys(), который совершает ту же проверку, что я делал в своей checkPropsChange:


// DatagridBody.js
import React, { Children } from 'react';
import onlyUpdateForKeys from ‘recompose/onlyUpdateForKeys’;

const DatagridBody = ({ resource, ids, data, children }) => (
    ...
);

export default onlyUpdateForKeys([‘ids’, ‘data’])(DatagridBody);

Я горячо рекомендую recompose. Помимо оптимизации производительности, она помогает вам извлекать логику выборки данных, составлять HOC и работать с props в функциональном и тестируемом стиле.


Redux


Если для управления состоянием приложения вы используете Redux (который я также рекомендую), тогда подключенные к нему компоненты уже чистые. Нет нужды в каком-либо другом HOC.


Просто запомните, если изменилось всего одно свойство, то подключенный компонент перерисуется, — и все его потомки тоже. Поэтому, если вы используете Redux для компонентов страницы, вам следует использовать pure() или shouldUpdate() для нижележащих по дереву компонентов.


Но также помните, что Redux использует строгое сравнение для props. Поскольку Redux связывает state c props компонента, то если вы будете изменять объект в state, то Redux просто проигнорирует это. И вот по этой причине вы должны использовать иммутабельность в ваших reducers.


К примеру, в admin-on-rest, клик по заголовку таблицы диспатчит SET_SORT action. Reducer, который слушает этот action, должен заменить объект в state, а не обновить его:


// listReducer.js
export const SORT_ASC = 'ASC';
export const SORT_DESC = 'DESC';

const initialState = {
    sort: 'id',
    order: SORT_DESC,
    page: 1,
    perPage: 25,
    filter: {},
};

export default (previousState = initialState, { type, payload }) => {
    switch (type) {
    case SET_SORT:
        if (payload === previousState.sort) {
            // обратим порядок сортировки
            return {
                ...previousState,
                order: oppositeOrder(previousState.order),
                page: 1,
            };
        }
        // заменим поле sort
        return {
            ...previousState,
            sort: payload,
            order: SORT_ASC,
            page: 1,
        };
    // ...
    default:
        return previousState;
    }
};

Следуя коду этого reducer-а, когда Redux проверяет state на изменения, используя тройное сравнение, он обнаруживает, что объект state изменился и перерисовывает таблицу с данными. Но если бы мы мутировали state, то Redux бы пропустил это изменение и соответственно ничего бы не перерисовал:


// не повторяйте это в домашних условиях
export default (previousState = initialState, { type, payload }) => {
    switch (type) {
    case SET_SORT:
        if (payload === previousState.sort) {
            // никогда так не делайте
            previousState.order= oppositeOrder(previousState.order);
            return previousState;
        }
        // и так тоже не делайте
        previousState.sort = payload;
        previousState.order = SORT_ASC;
        previousState.page = 1;
        return previousState;
    // ...
    default:
        return previousState;
    }
};

Чтобы писать иммутабельные reducers, некоторые разработчики используют библиотеку immutable.js, которая также от Facebook. Но с тех пор, как деструктуризация ES6 упростила выборочную замену в свойствах компонента, то я не считаю, что эта библиотека необходима. Кроме того, она тяжеловесная (60 kB), поэтому подумайте дважды, прежде чем добавить её в зависимости своего проекта.


Reselect


Для предотвращения ненужной отрисовки подключенных к Redux компонентов, вы также должны убедиться, что функция mapStateToProps не возвращает новые объекты при каждом её вызове.


Возьмём, к примеру, компонент <List> в admin-on-rest. Он берёт из state список записей для текущего ресурса (например, посты, комментарии, и т.д.) с помощью следующего кода:


// List.js
import React from 'react';
import { connect } from 'react-redux';

const List = (props) => ...
const mapStateToProps = (state, props) => {
    const resourceState = state.admin[props.resource];
    return {
        ids: resourceState.list.ids,
        data: Object.keys(resourceState.data)
            .filter(id => resourceState.list.ids.includes(id))
            .map(id => resourceState.data[id])
            .reduce((data, record) => {
                data[record.id] = record;
                return data;
            }, {}),
    };
};

export default connect(mapStateToProps)(List);

State содержит массив всех ранее загруженных записей, проиндексированных ресурсом. Например, state.admin.posts.data содержит список постов:


{
   23: { id: 23, title: “Hello, World”, /* … */ },
   45: { id: 45, title: “Lorem Ipsum”, /* … */ },
   67: { id: 67, title: “Sic dolor amet”, /* … */ },
}

Функция mapStateToProps фильтрует объект state и возвращает только те записи, которые фактически отображаются в списке. Что-то вроде этого:


{
    23: { id: 23, title: “Hello, World”, /* … */ },
    67: { id: 67, title: “Sic dolor amet”, /* … */ },}

Проблема в том, что при каждом запуске функции mapStateToProps, она возвращает новый объект, даже если внутренние объекты не изменились. Как следствие, компонент <List> каждый раз перерисовывается, когда в state что-то меняется — в то время, как, при изменении date или id, изменяться должны только id.


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


import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'

const List = (props) => ...

const idsSelector = (state, props) =>
    state.admin[props.resource].ids
const dataSelector = (state, props) =>
    state.admin[props.resource].data

const filteredDataSelector = createSelector(
  idsSelector,
  dataSelector
  (ids, data) => Object.keys(data)
      .filter(id => ids.includes(id))
      .map(id => data[id])
      .reduce((data, record) => {
          data[record.id] = record;
          return data;
      }, {})
)

const mapStateToProps = (state, props) => {
    const resourceState = state.admin[props.resource];
    return {
        ids: idsSelector(state, props),
        data: filteredDataSelector(state, props),
    };
};

export default connect(mapStateToProps)(List);

Теперь компонент <List> будет перерисовываться только при изменении подмножества state.


Что касается recompose, selectors — это чистые функции, лёгкие для тестирования и компоновки. Написание своего selector для подключенных к Redux компонентов — это хорошая практика.


Остерегайтесь объектных литералов в JSX


Однажды ваш компонент станет ещё более "чистым", и вы можете обнаружить у себя в коде плохие паттерны, приводящие к бесполезным перерисовкам. Наиболее общим примером этого является использование объектных литералов в JSX, которые мне нравится называть "Печально известные {{". Позвольте привести пример:


import React from 'react';
import MyTableComponent from './MyTableComponent';

const Datagrid = (props) => (
    <MyTableComponent style={{ marginTop: 10 }}>
        ...
    </MyTableComponent>
)

Свойство style компонента <MyTableComponent> получает новое значение каждый раз, когда компонент <Datagrid> отрисовывается. Таким образом, даже если <MyTableComponent> чистый, он всё равно будет перерисовываться при перерисовке <Datagrid>. По факту, каждый раз вы передаёте объектный литерал как свойство в дочерний компонент, вы нарушаете чистоту. Решение простое:


import React from 'react';
import MyTableComponent from './MyTableComponent';

const tableStyle = { marginTop: 10 };
const Datagrid = (props) => (
    <MyTableComponent style={tableStyle}>
        ...
    </MyTableComponent>
)

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


Следующий подозреваемый в краже чистоты компонента это React.CloneElement(). Если вы передаёте свойство в качестве значения второго параметра, то склонированный элемент будет получать новые props при каждой отрисовке.


// плохо
const MyComponent = (props) =>
    <div>{React.cloneElement(Foo, { bar: 1 })}</div>;

// хорошо
const additionalProps = { bar: 1 };
const MyComponent = (props) =>
    <div>{React.cloneElement(Foo, additionalProps)}</div>;

Я обжёгся на этом пару раз с material-ui на примере следующего кода:


import { CardActions } from 'material-ui/Card';
import { CreateButton, RefreshButton } from 'admin-on-rest';

const Toolbar = ({ basePath, refresh }) => (
    <CardActions>
        <CreateButton basePath={basePath} />
        <RefreshButton refresh={refresh} />
    </CardActions>
);

export default Toolbar;

Хотя компонент <CreateButton> чистый, он отрисовывается каждый раз, когда отрисовывается <Toolbar>. Это всё потому, что компонент <CardAction> из material-ui добавляет специальный стиль первому потомку для размещения на полях — и делает он это с помощью объектного литерала. Поэтому <CreateButton> получает разный объект style каждый раз. Я смог решить это с помощью HOC функции onlyUpdateForKeys() из recompose.


// Toolbar.js
import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';

const Toolbar = ({ basePath, refresh }) => (
    ...
);

export default onlyUpdateForKeys(['basePath', 'refresh'])(Toolbar);

Заключение


Есть ещё много вещей, которые нужно сделать, чтобы поддерживать приложение на React быстрым (использовать ключи, ленивую загрузку тяжёлых маршрутов, пакет react-addons-perf, ServiceWorkers для кэширования состояния приложения, добавить изоморфности, и т.д.), но корректная реализация shouldComponentUpdate — это первый и самый действенный шаг.


Сам по себе, React — не быстрый, но он предлагает все инструменты, чтобы сделать быстрым приложение любого размера.


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


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


Если вы хотите узнать больше об оптимизации производительности React, то вот список отличных статей по этой теме:


Поделиться с друзьями
-->

Комментарии (52)


  1. vintage
    25.04.2017 20:15
    +2

    Беда почти всех фреймворков: легко сделать медленное приложение, сложно сделать быстрое. А что если существует такой фреймворк, на котором сделать быстрое приложение проще, чем медленное?


    1. napa3um
      25.04.2017 20:23
      +2

      Без фреймворков эта беда никуда не исчезает.


      1. vintage
        25.04.2017 20:31

        Конечно, он только усугубится.


    1. lifeart
      26.04.2017 10:07

      glimmerjs — не нужно использовать ShouldComponentUpdate, т.к. свойства трекаются автоматически, нужно просто указать что оно изменяемое.


      1. vintage
        26.04.2017 10:46

        Но он по прежнему будет рендерить всё подряд, а не то, что попадает в видимую область?


  1. Strate
    26.04.2017 00:24
    +3

    Всем mobx!


  1. vtvz_ru
    26.04.2017 04:55

    Браво! По огромному спасибо автору и переводчику этой статьи. Легко читать, но сколько много полезной информации узнал.
    Только я так и не понял, что мне делать с компонентами, которые привязаны через connect к redux? Что нужно проверять и нужно ли что либо проверять?


    1. KarafiziArthur
      26.04.2017 10:51

      Благодарю! Рад оказаться полезным.
      И спасибо за вопрос. Действительно, автор статьи не стал раскрывать тему глубоко, но сделал одно замечание, что Redux компоненты (функциональные, без state) уже чистые. Поправьте меня, если я ошибаюсь, но как я понял, имеется ввиду, что функция высшего порядка connect передаёт из store приложения данные в props компонента, и за счёт этого и достигается чистота компонента. Но, если Redux используется для компонентов (например, компонентов отдельных страниц), которые содержат другие компоненты, то к дочерним компонентам нужно при экспорте применять HOC функции для проверки на изменения их props — shouldUpdate или pure из recompose, чтобы они не перерендеривались каждый раз при изменении родительского компонента.


    1. dark_ruby
      26.04.2017 12:16

      так вроде написано же в статье что для connect-нутых компонент надо использовать reselect, он мемоизирует данные поступаемые из стора через connect, (т.е. возвращает тот же самыйх обьект, сравнимый через ===) — и это предотвращает от перерисовки компонента


      1. raveclassic
        26.04.2017 12:57

        connect уже содержит в себе shouldComponentUpdate, так что ничего перепроверять не нужно.

        reselect используется для других вещей, хоть про мемоизацию вы и правильно написали.

        connect запускает ф-ию mapStateToProps на каждое изменение стора, и не трудно догадаться, что селекторы, описанные в ней, запускаются так же на каждое изменение стора. Если селекторы у вас дорогие (например, фильтрации массивов, сборки больших объектов и т.п.), то нужен способ избежать ненужных операций. Для этого и придумали reselect с его одноуровневым кэшем. Базовый селектор запускается так же каждый раз при изменении стора, только вот на вход он в большинстве случаев принимает другие селекторы поменьше. И трюк тут в том, что, если при выполнении этих селекторов поменьше возвращаемое от них значение не изменилось, то компонующий селектор выполняться не будет, а вернет старое значение.


  1. Finom
    26.04.2017 12:05

    Я бы еще к антипаттернам добавил очевидный литерал массива и не очень очевидный, но распространенный антипаттерн возврата обработчика при каждом рендеринге.


    const onClickMe = (name) => (event) => doSomething(name);
    //...
    <SomeComponent something={['foo', 'bar']} onClick={onClickMe('baz')} />


    1. Aingis
      26.04.2017 17:31

      Скорее стоит добавить что-то вроде


      <Input onChange={e => this.update(e)}/>

      или


      <Input onChange={this.update.bind(this)}/>

      — на каждый вызов рендера создаётся новая функция-обработчик.


      1. Finom
        26.04.2017 17:32

        Да, спасибо.


      1. axelsheva
        26.04.2017 19:27

        Напишите пожалуйста как правильно нужно вызывать this.update в ваших примерах


        1. KarafiziArthur
          26.04.2017 19:42

          Лично я пишу примерно так (для контролируемых полей):

          class SomeClass extends Component {
          
            state = {
                input: ''
            }
            ... 
          
            onChangeHandler = (event) => {
                this.setState({
                    [event.target.name]: event.target.value
                });
            };
          
            render() {
                return (
                    <div className="SomeClass">
                        <input name="input" onChange={this.onChangeHandler} value={this.state.input} />
                    </div>
                );
             }
          }
          


          Возможно есть способ лучше.


          1. zzzevaka
            26.04.2017 21:26

            Именно так и рекомендуется в офф документации… ну или через переопределение обработчика в конструкторе.


            спасибо за перевод.


      1. comerc
        02.05.2017 02:18

        Ваши примеры известно как лечить, а что с этим делать?


        const onClickMe = (name) => (event) => doSomething(name)
        console.log(onClickMe('baz') === onClickMe('baz'))

        Я пока не придумал ничего лучше:


        const onClickMeBaz = (event) => doSomething('baz')
        
        <SomeComponent onClick={onClickMeBaz} />


        1. ookami_kb
          02.05.2017 07:37

          Можно как-нибудь так, если с lodash:


          const onClickMe = _.memoize(name => event => doSomething(name));
          
          <SomeComponent onClick={onClickMe('baz')} />


          1. vintage
            02.05.2017 09:26
            +1

            Не боитесь утечек памяти?


            1. ookami_kb
              02.05.2017 09:31
              -1

              А что мешает чистить кэш memoize, например, при размонтировании родительского компонента?


              1. raveclassic
                02.05.2017 10:42

                Тот же memoize можно завернуть в декоратор метода, пишущий эту очистку в прототип текущего класса. Тогда и руками очищать не нужно.


                1. comerc
                  03.05.2017 19:00

                  Приведите пример, пожалуйста.


                  1. raveclassic
                    04.05.2017 01:29
                    +1

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

                    const storage = Symbol();
                    
                    const Memoize = () => (target, propertyKey, descriptor) => {
                        const cwm = target.prototype.componentWillMount;
                    
                        target.prototype.componentWillMount = function () {
                            if (!this[storage]) {
                                this[storage] = {};
                            }
                    
                            if (!this[storage][propertyKey]) {
                                this[storage][propertyKey] = {};
                            }
                    
                            const fnStorage = this[storage][propertyKey];
                    
                            descriptor.value = function () {
                                const args = Array.prototype.slice.call(arguments);
                                const key = serialize(args);
                                if (typeof fnStorage[key] === 'undefined') {
                                    fnStorage[key] = descriptor.value.apply(this, args);
                                }
                                return fnStorage[key];
                            }.bind(this);
                    
                            cwm && cwm.call(this);
                        };
                    };
                    
                    function serialize(args) {
                    	const argsAreValid = args.every(arg => {
                    		return typeof arg === 'number' || typeof arg === 'string';
                    	});
                    	if (!argsAreValid) {
                    		throw Error('Arguments to memoized function can only be strings or numbers');
                    	}
                    	return JSON.stringify(args);
                    }
                    


                    Суть в том, чтобы заинжектиться в метод в прототипе класса и создать там хранилище на его инстансе, поэтому почти все через function. Вроде бы даже получилось без componentWillUnmount, так как хранилище — обычное поле на инстансе, и очистится вместе с самим инстансом. Ну а даже если нет, можно руками delete'нуть в componentWillUnmount, прописанным в прототип.
                    Так же можно поиграться с полями в дексрипторе и добавить обработку initializer и get, все-равно все в замыкании болтается.


                    1. mayorovp
                      04.05.2017 06:15
                      +1

                      Поправка: descriptor.value надо в переменную сохранять, иначе вечная рекурсия будет.


                      Ну и раз предполагается что в языке есть декораторы — то и вместо slice.call(arguments) надо ...args использовать.


                      1. raveclassic
                        04.05.2017 10:23

                        рекурсия
                        Да, конечно, упустил.

                        надо ...args использовать.
                        По поводу args — сделано умышленно, так как во время написания прообраза нужен был es2015, а там babel переводит эту конструкцию в цикл.
                        for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
                            args[_key2] = arguments[_key2];
                        }
                        

                        Мелочь конечно, но ради перфоманса и, раз уж это все-равно закрытый сервисный код, выбран был call. Apply же используется из-за того, что нужен контекст this.


                        1. mayorovp
                          04.05.2017 10:34

                          Вы так пишите, как будто использованный вами Array.prototype.slice.call(arguments) не содержит внутри такой же цикл!


                          Кстати, вы в курсе что передача объекта arguments наружу (даже в slice.call) может выключить оптимизацию? Babel не просто так цикл использует...


                          1. raveclassic
                            04.05.2017 10:43

                            Кстати, вы в курсе
                            Хм, теперь в курсе :)

                            Edit: но ведь наружу-то ничего не утекает, arguments передаются на 1 уровень глубже и все?


                            1. raveclassic
                              04.05.2017 10:46

                              Ну ок, в serialize еще. Но там, только stringify, тоже ничего не протекает.


                            1. mayorovp
                              04.05.2017 10:48

                              "Наружу" означает "за пределы единицы оптимизации". Оптимизатор же не знает что за значение лежит в Array.prototype.slice и что делает его метод call.


                              1. raveclassic
                                04.05.2017 10:54

                                Логично, надо переписать, как руки дойдут.


            1. comerc
              04.05.2017 01:06
              +1

              Поясните, каким образом возникают утечки памяти?


              1. raveclassic
                04.05.2017 01:33
                +1

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

                class {
                  @Memoize
                  onClick = id => event => {
                  }
                }
                

                Если у вас потенциально неограниченное количество айдишников, то на каждый из них будет бесконтрольно выделяться по функции. Как вариант, прописать функцию очистки в дексриптор (через тот же символ) и в самой задекорированном методе вызывать ее «на себе».
                Вот тут есть пример этого случая, а не описанного выше.

                Edit: но тут есть проблемы с react-hot-loader, так как он по факту заменяет все функции на обертки, и достучаться до clearer нет возможности. Хорошо хоть только в дев-режиме.


                1. raveclassic
                  04.05.2017 10:31
                  +1

                  Ссылку на декоратор скинул, а пример-то зажал:

                  class Foo extends React.Component {
                    render() {
                      const {list} = this.props;
                      return (
                        <div>
                          {list.map(item => (
                            <button onClick={this.onClick(item.id)}>{item.name}</button>
                          ))}
                        </div>
                      );
                    }
                    
                    @Memoize
                    onClick = id => event => {
                      //memory leak - need to cleanup
                      this.onClick[MEMOIZE_CLEAR_FUNCTION](id);
                    }
                  }
                  


        1. Aingis
          02.05.2017 11:55

          Собственно, да, связывать предварительно чтобы иметь ссылку на одну и ту же сущность. Ранее была рекомендация делать это в конструкторе:


          constructor(props) {
              super(props);
              /* ... */
              this.update = this.update.bind(this);
          }


  1. mayorovp
    26.04.2017 15:37

    Может, тут мне ответят...


    При использовании связки react-mobx можно написать вот так:


    const SubComponent = observer((props) => props.fn(...props.args));
    const sub = fn => React.createElement(SubComponent, { fn : fn, args : [] });
    const subFn = fn => (...args) => React.createElement(SubComponent, { fn : fn, args : args });
    
            <thead>
                <tr>
                    {Children.map(children, subFn((field, index) =>
                        <DatagridHeaderCell
                            key={index}
                            field={field}
                            currentSort={this.props.currentSort}
                            updateSort={this.updateSort}
                        />
                    ))}
                </tr>
            </thead>

    Это избавит родительский компонент от перерисовки при изменении параметров, влияющих на поддерево.


    Какие недостатки у такого решения?


    1. raveclassic
      26.04.2017 17:20

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


      1. mayorovp
        26.04.2017 17:29

        Э… вы сейчас про какие именно обертки?


        1. raveclassic
          26.04.2017 17:47

          SubComponent и subFn. Вместо того, чтобы просто передать field и index в SubComponent, его нужно обернуть в компонент, который принимает объект, чтобы вызвать на нем observable, потому как в противном случае, mobx с observable не будет трекать изменения field и index. Это кажется самым большим костылем.


          1. mayorovp
            26.04.2017 17:53

            Нет, вы ошибаетесь. По умолчанию mobx трекает все. Обертки я добавил не для того чтобы добавить трекинг — а чтобы изолировать лишний трекинг от родителя.


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


            1. raveclassic
              26.04.2017 17:58

              1. mayorovp
                26.04.2017 18:01

                Пожалуйста, прочитайте внимательнее тот пункт документации, на который дали ссылку.


                1. raveclassic
                  26.04.2017 18:09

                  Ну и что вы меня доки читать отправляете? Мне-то понятно, что имеется в виду под:

                  MobX can do a lot, but it cannot make primitive values observable (although it can wrap them in an object see boxed observables). So not the values that are observable, but the properties of an object. This means that observer actually reacts to the fact that you dereference a value. So in our above example, the Timer component would not react if it was initialized as follows:

                  React.render(<Timer timerData={timerData.secondsPassed} />, document.body)


                  It is the property secondsPassed that will change in the future, so we need to access it in the component. Or in other words: values need to be passed by reference and not by value.


                  Вы вынуждены спускать в компоненты observable-объекты, вместо обычных значений, потому что в противном случае mobx превращается в тыкву.


              1. mayorovp
                26.04.2017 18:09

                А, я понял причину недопонимания. Там документация кривая...


                Если передать дочернему компоненту значение — он и правда не сможет за ним наблюдать (что логично!).


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


                1. raveclassic
                  26.04.2017 18:12

                  Ну вот.

                  Если передать дочернему компоненту значение — он и правда не сможет за ним наблюдать (что логично!).
                  Т.е. появляются какие-то крайние случаи, где что-то идет не так, как ожидается, об этом нужно помнить, тогда как mobx продается под маркой «сел и поехал, и не думай ни о чем».

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


                  1. mayorovp
                    26.04.2017 18:17

                    Да, но это не выходит за рамки обычных для React принципов! props заполняются родителем, и он же должен обеспечивать их актуальность.


                    Если мы передали дочернему компоненту число 5, а потом оно стало не 5 а 6 — то мы должны уведомить его об этом изменении.


                    1. raveclassic
                      26.04.2017 18:23

                      А если это число лежит в объекте, должны ли мы его (дочерний компонент) об этом уведомлять? Или положимся на магию, что попытка доступа в нужное поле выполнит подписку на изменения и запустит перерендер дочернего компонента? Где та грань, когда нужно что-то спускать простым значением, а что-то объектом? Все спускать объектом? Ну так вот он и костыль.


                      1. mayorovp
                        26.04.2017 18:28

                        Все просто же. Есть декоратор observer — магия включена (для текущего компонента, но не для его родителей или детей). Нет observer — магия выключена.


                        1. raveclassic
                          26.04.2017 19:01
                          -1

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

                          Те же контейнеры в редаксе можно плодить на каждый чих для короткого замыкания потока данных в подкомпоненты для предотвращения перерисовок родителя с сохранением инкапсуляции.


                          1. mayorovp
                            26.04.2017 19:07

                            Если мне в родительском компоненте нужно знать, сможет ли дочерний отреагировать на императивное изменение в глубине спускаемой в него структуры

                            Не нужно!


                            Если дочерний компонент принимает в качестве свойства мутабельный объект некоторого типа — то реагирование на изменения в нем является его ответственностью. Точка. И не важно, будет ли он использовать магию mobx-react чтобы достичь желаемого эффекта.


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


                            1. raveclassic
                              26.04.2017 22:50
                              +1

                              Знаете, я тут подумал еще, и, да, вы правы, никаких нарушений нет. Видимо, полностью перестроенные на схему state => ui мозги не дают покоя. И, действительно, никогда не знаешь внутри компонента, принимающего observable, что именно произойдет при установке конкретного свойства, и кого это поаффектит во всем приложении. Да, гибкость выше, но и последствия могут быть серьезней, и гораздо труднее отлавливать источник изменений.

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


  1. comerc
    04.05.2017 00:49

    Что со мной не так?


    1. KarafiziArthur
      05.05.2017 19:32

      Вы проверяете на localhost, с ?react_perf в конце URL и версией React не ниже 15.4?