React/Redux Performance
Bettina Helgenlechner
hallo@bettina.tech
24.01.2018
REACT
Production
Build
Virtualize
Long
Lists
https://github.com/bvaughn/react-virtualized
Avoid
Component
Updates
REDUX
Normalized
State
[
{
id : "post1",
author : {username : "user1", name : "User 1"},
body : "......",
comments : [
{
id : "comment1",
author : {username : "user2", name : "User 2"},
comment : ".....",
},
{
id : "comment2",
author : {username : "user1", name : "User 1"},
comment : ".....",
}
]
},
...
];
{
posts : {
byId : {
"post1" : {
id : "post1",
author : "user1",
body : "......",
comments : ["comment1", "comment2"]
},
...
}
},
comments : {
byId : {
"comment1" : {
id : "comment1",
author : "user2",
comment : ".....",
},
...
},
allIds : ["comment1", ...]
},
users : {
byId : {
"user1" : {
username : "user1",
name : "User 1",
},
...
}
}
}
Many
Connected
Components
const mapStateToProps = (state) => ({
authors: state.authors,
comments: state.comments,
posts: state.blogPosts,
});
class BlogComponent extends React.Component {
render() {
return this.props.posts.map(post => (
<Post
post={post}
author={this.props.authors.find(author => author.id === post.author)}
comments={this.props.comments
.filter(comment => post.comments.includes(comment.id))}
/>
)
);
}
}
export const Blog = connect(mapStateToProps)(BlogComponent);
const mapStateToProps = (state) => ({
posts: state.blogPosts
});
class BlogComponent extends React.Component {
render() {
return this.props.posts.map(post => (
<Post
id={post.id}
/>
)
);
}
}
export const Blog = connect(mapStateToProps)(BlogComponent);
Selectors
const mapStateToProps = (state) => ({
posts: state.posts
});
class BlogComponent extends React.Component {
render() {
return this.props.posts
.filter(post => post.comments.length > 0)
.map(post => (
<Post
id={post.id}
/>
)
);
}
}
const mapStateToProps = (state) => ({
posts: getPostsWithComments(state)
});
class BlogComponent extends React.Component {
render() {
return this.props.posts
.map(post => (
<Post
id={post.id}
/>
)
);
}
}
const getPosts = (state) => state.posts;
export const getPostsWithComments = (state) =>
state.posts.filter(post => post.comments.length > 0);
Memoized
Selectors
const getPosts = (state) => state.posts;
export const getPostsWithComments = createSelector(
[getPosts],
(posts) => {
return posts.filter(post => post.comments.length > 0);
}
);
github.com/reactjs/reselect
export const getPostsWithComments = (state) =>
state.posts.filter(post => post.comments.length > 0);
Batch
Store
Updates
-
redux-batched-actions: a higher-order reducer that lets you dispatch several actions as if it was one and “unpack” them in the reducer
-
redux-batched-subscribe: a store enhancer that lets you debounce subscriber calls for multiple dispatches
-
redux-batch: a store enhancer that handles dispatching an array of actions with a single subscriber notification
DIAGNOSING
BOTTLENECKS
React DevTools:
Highlight Updates
componentWillReceiveProps(newProps) {
Object.keys(newProps).map(key => {
if (newProps[key] !== this.props[key]) {
console.log(key)
}
})
}
Chrome DevTools: Performance Tab
Changes Made
- Use PureComponent
- Connect each component directly to store
- Use memoized selectors throughout
render() {
const { todos, views, actions } = this.props
const { filter } = this.state
const filteredTodos = todos
.filter(todo => TODO_FILTERS[filter])
const completedCount = Object.values(views)
.reduce((count, view) =>
view.completed ? count + 1 : count,
0
)
return (
<section className="main">
{this.renderToggleAll(completedCount)}
<ul className="todo-list">
{filteredTodos.map(todo =>
<TodoItem
key={todo.id}
todo={todo}
view={views[todo.id]}
scrollObservable={this.scrollObservable}
{...actions} />
)}
</ul>
{this.renderFooter(completedCount)}
</section>
)
}
render() {
const {
filteredTodos,
numberOfCompletedTodos
} = this.props
return (
<section className="main">
{this.renderToggleAll(numberOfCompletedTodos)}
<ul className="todo-list">
{filteredTodos.map(todo =>
<TodoItem
key={todo.id}
id={todo.id}
scrollObservable={this.scrollObservable}
/>
)}
</ul>
{this.renderFooter(numberOfCompletedTodos)}
</section>
)
}
Before
After
Todo List Component
const mapStateToProps = state => ({
filteredTodos: getFilteredTodos(state),
numberOfCompletedTodos:
getNumberOfCompletedTodos(state),
})
export const getNumberOfCompletedTodos = createSelector(
[getViews],
(views) => {
return Object.values(views).reduce((count, view) =>
view.completed ? count + 1 : count,
0
)
}
)
const mapStateToProps = () => {
const getIsActiveById = makeGetIsActiveById();
const getIsCompletedById = makeGetIsCompletedById();
const getIsSeenById = makeGetIsSeenById();
const getTextById = makeGetTodoTextById();
return (state, props) => ({
isActive: getIsActiveById(state, props),
isCompleted: getIsCompletedById(state, props),
isSeen: getIsSeenById(state, props),
text: getTextById(state, props),
})
}
render() {
const { isCompleted, text, id,
isSeen, isActive } = this.props
return (
<li className={classnames({
completed: isCompleted,
editing: this.state.editing,
seen: isSeen,
active: isActive,
})}>
<div className="view">
<input type="checkbox"
checked={isCompleted} />
<label>{text}</label>
</div>
</li>
)
}
render() {
const { todo, view } = this.props
return (
<li className={classnames({
completed: view && view.completed !== undefined
? view.completed : false,
editing: this.state.editing,
seen: view && view.seen,
active: view && view.active,
})}>
<div className="view">
<input type="checkbox"
checked={view && view.completed !== undefined
? view.completed : false} />
<label>{todo.text}</label>
</div>
</li>
)
}
Todo Item Component
Before
After
const getId = (state, props) => props.id
const getViewById = createSelector(
[getViews, getId],
(views, id) => views[id]
)
export const makeGetIsCompletedById = () => (
createSelector(
[getViews, getId],
(views, id) => {
if (views[id]) {
return views[id].completed
}
return false
}
)
)
In Closing
- Identify bottlenecks and measure changes
- The same techniques won't help all apps
- Real improvements are possible: ~600ms to ~7ms
Resources
Redux
React
React/Redux Performance
By Bettina Helgenlechner
React/Redux Performance
- 378