So fast it’s already far away now

React is Slow, React is Fast: Optimizing React Apps in Practice

François Zaninotto
Feb 6, 2017 · 14 min read

Measuring React Performance

Why Did You Update?

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

Cutting Components To Optimize Them

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

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;

Recompose

// in 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);
// in 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);
// in DatagridBody.js
import React, { Children } from 'react';
import onlyUpdateForKeys from ‘recompose/onlyUpdateForKeys’;const DatagridBody = ({ resource, ids, data, children }) => (
    ...
);export default onlyUpdateForKeys([‘ids’, ‘data’])(DatagridBody);

Redux

// in 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) {
            // inverse sort order
            return {
                ...previousState,
                order: oppositeOrder(previousState.order),
                page: 1,
            };
        }
        // replace sort field
        return {
            ...previousState,
            sort: payload,
            order: SORT_ASC,
            page: 1,
        };    // ...    default:
        return previousState;
    }
};
// don't do this at home
export default (previousState = initialState, { type, payload }) => {
    switch (type) {
    case SET_SORT:
        if (payload === previousState.sort) {
            // never do this
            previousState.order= oppositeOrder(previousState.order);
            return previousState;
        }
        // never do that either
        previousState.sort = payload;
        previousState.order = SORT_ASC;
        previousState.page = 1;
        return previousState;    // ...    default:
        return previousState;
    }
};

Reselect

// in 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);
{
   23: { id: 23, title: “Hello, World”, /* … */ },
   45: { id: 45, title: “Lorem Ipsum”, /* … */ },
   67: { id: 67, title: “Sic dolor amet”, /* … */ },
}
{
    23: { id: 23, title: “Hello, World”, /* … */ },
    67: { id: 67, title: “Sic dolor amet”, /* … */ },
}
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].dataconst 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);

Beware of Object Literals in JSX

import React from 'react';
import MyTableComponent from './MyTableComponent';const Datagrid = (props) => (
    <MyTableComponent style={{ marginTop: 10 }}>
        ...
    </MyTableComponent>
)
import React from 'react';
import MyTableComponent from './MyTableComponent';const tableStyle = { marginTop: 10 };
const Datagrid = (props) => (
    <MyTableComponent style={tableStyle}>
        ...
    </MyTableComponent>
)
// bad
const MyComponent = (props) =>
    <div>{React.cloneElement(Foo, { bar: 1 })}</div>;// good
const additionalProps = { bar: 1 };
const MyComponent = (props) =>
    <div>{React.cloneElement(Foo, additionalProps)}</div>;
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;
// in Toolbar.js
import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';const Toolbar = ({ basePath, refresh }) => (
    ...
);export default onlyUpdateForKeys(['basePath', 'refresh'])(Toolbar);

Conclusion

DailyJS

JavaScript news and opinion.

François Zaninotto

Written by

I'm French, CEO at marmelab. I tweet three times a day about #LeanStartup #Agile #NodeJs #ReactJs #Symfony2 #d3js #WebPerf #security #OSS

DailyJS

DailyJS

JavaScript news and opinion.