React медленный, React быстрый: оптимизация React-приложения на практике

https://medium.com/dailyjs/react-is-slow-react-is-fast-optimizing-react-apps-in-practice-394176a11fba

Всем привет! Хочу поделиться своим переводом статьи React is Slow, React is Fast: Optimizing React Apps in Practice автора François 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, то вот список отличных статей по этой теме:


Поделиться публикацией
Комментарии 59
    +2

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

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

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

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

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

        +3

        Всем mobx!

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

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

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

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

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

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


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

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


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

                или


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

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

                  0

                  Да, спасибо.

                    0
                    Напишите пожалуйста как правильно нужно вызывать this.update в ваших примерах
                      0
                      Лично я пишу примерно так (для контролируемых полей):

                      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>
                            );
                         }
                      }
                      


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

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


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

                      0

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


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

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


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

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


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

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

                            –1

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

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

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

                                  +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

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


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

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

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

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

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


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

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

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

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

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

                                        Хочется решение для функциональных компонентов.

                                          0

                                          Обернуть в HoC, наверно.

                                            0
                                            Не совсем понимаю, как это. Непосредственно внутри SFC функцию не замемоизировать — каждый раз новая, даже с memoize. Значит нужно выносить — либо на уровень модуля и там мемоизировать. Либо наверх в стейтфул-компонент (класс).

                                            Плюс, SFC не имеет фазы unmount, так что утечки гарантированы.
                                              0
                                              const pure = (fn: Function) => {
                                                class Wrapper extends React.PureComponent {
                                                  componentWillMount() {
                                                    // this.onClick 
                                                  }
                                                  render () {
                                                    return fn(this.props, this.context)
                                                  }
                                                }
                                                Wrapper.displayName = `pure(${fn.name})`
                                                return Wrapper
                                              }
                                              
                                              const myComponent = pure(({ onClick }) => {
                                                return (<div onClick={onClick}>Hello!</div>)
                                              })

                                              На примере HoC для PureComponent. Как прицепить onClick к инстансу HoC. Только надо как-то мемоизировать.

                                    +1

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

                                      +1
                                      Утечки будут, если вы не контролируете количество первых аргументов в каррируемую мемоизирующую функцию, например
                                      class {
                                        @Memoize
                                        onClick = id => event => {
                                        }
                                      }
                                      

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

                                      Edit: но тут есть проблемы с react-hot-loader, так как он по факту заменяет все функции на обертки, и достучаться до clearer нет возможности. Хорошо хоть только в дев-режиме.
                                        +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);
                                          }
                                        }
                                        
                                  0

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


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

                                    Ещё нашёл такое:


                                    import memoize from 'lru-memoize';
                                    
                                    let multiply = (a, b, c) => a * b * c;
                                    
                                    multiply = memoize(10)(multiply); // with limit
                                    
                                    export default multiply;
                                      0
                                      Ну, кстати, чем не решение
                                0

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


                                При использовании связки 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>

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


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

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

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

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

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


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

                                            0

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

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

                                              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 превращается в тыкву.
                                              0

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


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


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

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

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

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


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

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

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

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

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

                                                          Не нужно!


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


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

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

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

                                    Не вижу чекбоксов Network, JS Profile, Paint. И нет разворота User Timing.

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

                                        Спасибо! С параметром ?react_perf появился разворот User Timing. Но по-прежнему не вижу чекбоксов: Network, JS Profile, Paint.

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

                                    Самое читаемое