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