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].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);

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.