Image for post
Image for post
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
Image for post
Image for post

Measuring React Performance

Image for post
Image for post
Image for post
Image for post
Image for post
Image for post

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;
Image for post
Image for post

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

Image for post
Image for post
Image for post
Image for post

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.

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.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store