React + Redux Performance Optimizations (1) : The Theory of React + Redux

In this long article, we will first discuss the justification of using Immutable Data; Then the necessity of using Immutablejs technology is studied from the function and performance

I guess you’re more concerned about whether it’s worth using Immutablejs, so here’s the conclusion: recommended; But it doesn’t have to be used. If the recommended index has a minimum of one point and a maximum of ten points, then it is a six.

About Pure

Pure is a very important concept both in React and Redux. Understanding what pure is helps us understand why we need Immutablejs

First we will introduce what is Pure function, from Wikipedia:

In programming, a function may be considered pure if it meets the following requirements:

  • This function needs to produce the same output at the same input value. The output of a function is independent of any hidden information or state other than the input value, or of the external output generated by the I/O device.
  • This function cannot have semantically observable function side effects, such as “firing events,” making output devices output, or changing the contents of objects other than output values.

In simple terms, there are two characteristics of pure functions: 1) For the same input, there is always the same output; 2) The function does not depend on external variables, nor does it have external effects (such effects are called “side effects”)

Reducer

Redux states that reducer is a pure function. It takes the previous state and action as arguments and returns the next state:

(previousState, action) => newState
Copy the code

It is important to keep the “pure” from the reducer. There are three things you should never do on the reducer:

  • Modify the parameters
  • Perform any actions that have side effects, such as calling apis
  • Call any function that is not pure, such asMath.random()orDate.now()

So you can see that the reducer returns the state using Object.assign({}, state). For asynchronous or “side effect” operations such as API calls, you can resort to redux-thunk or redux-saga.

Pure Component

In the last post we talked about PureComponent, which is strictly a react. PureComponent. In the broad sense, Pure Compnoent refers to Stateless Component, also known as Dumb Component, Presentational Component. Code-wise it is characterized by 1) not maintaining its own state, and 2) only having the render function:

const HelloUser = ({userName}) = > {
  return <div>{`Hello ${userName}`}</div>
}
Copy the code

Obviously, this form of “pure component” and “pure function” have the same charm, that is, the component always outputs a unique result to the same attribute passed in.

Of course, components in this form also lose some of their capabilities, such as no longer having lifecycle functions.

performance

An important lesson we learned from the previous article was that whenever the component’s props or state changes, the component will perform the render function to re-render. Unless you override the shouldComponentUpdate periodic function to prevent this from happening by returning false; Alternatively, the component can inherit PureComponent directly.

Inheriting PureComponent is very simple. It just implements shouldComponentUpdate instead of you: Perform a “shallow comparision” between the current and past props/state within the function (shallow comparision that only compares references to objects rather than the value of each property of the object), and do not execute the render function to rerender the component if the object has not changed from front to back

A similar logic is used many times in Redux, where data is “shallow compared.”

The first is in React-Redux

We usually use the connect function in React-Redux to inject program state into the component, for example:

import {conenct} from 'react-redux'

function mapStateToProps(state) {
  return {
    todos: state.todos,
    visibleTodos: getVisibleTodos(state),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)
Copy the code

React-redux assumes that the App is a Pure Component that has a unique render result for unique props and state. React-redux will index the root state (state, the first parameter to mapStateToProps) and perform a shallow comparison. If the comparison results are consistent, the component will not be re-rendered. Otherwise, mapStateToProps will continue to be called. It also continues to index the value of each property in the props object returned by mapStateToProps (that is, the state.todos value and getVisibleTodos(state) value in the code above, but not the whole object returned). Like shouldComponentUpdate, the wrapped component is rerendered only if the shallow contrast fails, i.e. the index changes

In the example above, as long as the values of state.todos and getVisibleTodos(state) do not change, the App component will never be rendered again. But watch out for the following trap pattern:

function mapStateToProps(state) {
  return {
    data: {
      todos: state.todos,
      visibleTodos: getVisibleTodos(state),
    }
  }
}
Copy the code

Even though state.todos and getVisibleTodos(state) don’t change either, because each time mapStateToProps returns the result {data: {… }} data creates new (literal) objects, so shallow comparisons always fail and the App still renders again

The second is in combineReducers.

We all know that the Redux Store encourages us to divide state objects into different slices or domains, and write reducer functions for these different domains to manage their state. Finally, the combineReducers function provided by the government is used to associate these fields and their Reducer function to assemble an overall state

For example

combineReducers({ todos: myTodosReducer, counter: myCounterReducer })
Copy the code

In the above code, the state of the program is composed of {todos, counter} two domain models, and myTodosReducer and myCounterReducer are reducer functions of their respective domains respectively

CombineReducers traverses each “pair” domain (key is domain name and value is domain reducer function). For each traversal:

  • It creates a reference to the current shard data
  • Call the Reducer function to calculate the new state of the fragmented data and return
  • A new reference is created for the new fragmented data returned by the Reducer function, and a shallow comparison is made between the new reference and the current data reference. If the comparison fails (which also means that the two references are inconsistent, which means that the reducer returned a new object), the bits are identifiedhasChangedSet totrue

After traversal, the combineReducer gets a new state object, hasChanged flag bit we can determine whether the overall state hasChanged, if true, the new state will be returned to the downstream, If false, the old current state is returned downstream. By downstream, I mean React-Redux and interface components further downstream.

We already know that react-Redux will do a shallow comparison of the root state and only re-render the component if the reference changes. So when the state needs to change, make sure that the corresponding Reducer function always returns a new object! Modifying the property value of the original object and returning it does not trigger a re-rendering of the component!

Therefore, the reducer function we often see is written to finally copy the original state Object through object. assign and return a new Object:

function myCounterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case "add":
      return Object.assign({}, state, { count: state.count + 1 });
    default:
      returnstate; }}Copy the code

The error is to simply modify the original object:

function myCounterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case "add":
      state.count++
      return state
    default:
      returnstate; }}Copy the code

The interesting thing is that if you print the result of state after state.count++ at this point, you’ll see that state.count does increment after each add, but the component never renders

Immutable Data and Immutablejs

Combining the above two knowledge points, both from the definition of reducer and the working mechanism of Redux, we have embarked on the same object. assign mode, that is, the original state is not modified and only the new state is returned. So state is inherently Immutable.

However, the object. assign method is not elegant, and may even be a hack. After all, the original purpose of object. assign is to copy the attributes of one Object to another. So here we introduce Immutablejs, which implements several classes of “immutable” data structures, such as maps and lists, for example.

For example, we need to create an empty object, using Immutablejs’s Map data structure:

import {Map} from 'immutable'
const person = Map(a)Copy the code

There doesn’t seem to be anything special. Next we want to add the age attribute to the Person instance, using the set method that comes with Map:

const personWithAge = person.set('age'.20)
Copy the code

Next we print out person and personWithAge:

console.log(person.toJS())
console.log(personWithAge.toJS())
Copy the code

Note that you can’t print person directly, or you’ll get a wrapped data structure; Instead, you call the toJS method to transform the Map data structure into a plain native object. Here’s what you get:

console.log(person.toJS()) / / {}
console.log(personWithAge.toJS()) // { age: 20 }
Copy the code

See the problem? We want to change the attributes of Person, but the attributes of Person are not changed, and the result returned by the set method, personWithAge, is what we want.

That is, in Immutabejs data structures, when you want to change an object property, you always get a new object, and the original object never changes. This is consistent with our object. assign usage scenario. So when we need to change state and state is an Immutablejs data structure, we can change it and return it:

function myCounterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case "add":
      return state.set('count', state.get('count') + 1);
    default:
      returnstate; }}Copy the code

This is just the core functionality of Immutablejs. Based on its own encapsulated data structure, it also provides other useful functions, such as.getin or.setin methods, or the ability to constrain the Record type of data structure. The techniques for using Immutablejs are another story

Immutablejs implementation insider

When it comes to Immutablejs, the data structure used to implement it has to be mentioned, which is often cited as one of the arguments for its superior performance over native objects. This section is directly translated from Immutable. Js, persistent data structures and Structural Sharing, simplified and deleted

Suppose you have a Javascript structured object like this:

const data = {
  to: 7.tea: 3.ted: 4.ten: 12.A: 15.i: 11.in: 5.inn: 9
}
Copy the code

You can imagine it stored in Javscript memory like this:

But we can also organize a dictionary lookup tree based on the key used as an index:

In this data structure, whenever you want to access the value of any property of the object, you can access it from the root node

When you want to change the value, just create a new dictionary lookup tree and make the most of the existing nodes

Suppose you want to change the value of the tea property to 14, first you need to find the critical path to the tea node:

Then copy these nodes to build a tree with exactly the same structure, except that the other nodes of the new tree are all references to the original tree:

Finally, the root node of the newly constructed tree is returned

This is the basic implementation principle of Immutablejs Map, which is of course just one of the dark technologies in Immutablejs

Practical test

How much of a performance boost can such a data structure provide? Let’s actually test it out:

Suppose we have 100,000 toDOS data stored in native Javascript objects:

const todos = {
  '1': { title: `Task 1`.completed: false };
  '2': { title: `Task 2`.completed: false };
  '3': { title: `Task 3`.completed: false };
  / /...
  '100000': { title: `Task 1`.completed: false };
}
Copy the code

Or use the function to generate 100,000 todos:

function generateTodos() {
  let count = 100000;
  const todos = {};
  while (count) {
    todos[count.toString()] = { title: `Task ${count}`.completed: false };
    count--;
  }
  return todos;
}
Copy the code

Next we prepare a Reducer to switch the completed state of a single TODO based on its ID:

function toggleTodo(todos, id) {
  return Object.assign({}, todos, {
    [id]: Object.assign({}, todos[id], {
      completed: !todos[id].completed
    })
  });
}
Copy the code

Let’s test how long it takes to modify a single TOdo:

const startTime = performance.now();
const nextState = toggleTodo(todos, String(100000 / 2));
console.log(performance.now() - startTime);
Copy the code

On my PC (1700X configuration, 32GB, Chrome 64.0.3282.186) the execution time was 33ms

Next we change toggleTodo to the Immutablejs version (of course the data is also Immutablejs Map data type, Immutablejs provides methods fromJS can easily convert native Javacript data types to Immutablejs data types.

function toggleTodo(todos, id) {
  returntodos.set(id, ! todos.getIn([id,"completed"]));
}
const startTime = performance.now();
const nextState = toggleTodo(state, String(100000 / 2));
console.log(performance.now() - startTime);
Copy the code

Execution time is less than 1ms, 30 times faster!

But did you see the problem with this test:

  • Although there is a 30-fold difference between the two, the slowest is only 33ms, and the user will not feel it. If this is a bottleneck, it’s not a big problem
  • The 1ms vs 33ms score was tested on 100,000 toDos, but in the actual process, few scenarios would use such a large amount of data. What about native performance on a thousand data points? The native method also does not exceed 1ms
  • We have only observed that Immutablejs is efficient at changing properties, forgetting that when native data is converted to Immutablejs (fromJS) or from Immutablejs to native objects (toJS) comes at a price. If you are in thefromJSRecord the time before and after, you will find that the time is about 300ms. You can’t avoid conversions because there’s a good chance that third-party components or older code won’t support Immutablejs

To sum up, using Immutablejs provides a performance boost, but not a significant one, and there are compatibility issues

I also have some other performance tests on Github, and I’ve made some interesting discoveries that I won’t go into detail here. Interested friends can take it to run, because it is one-time will not be maintained, so the code is bad, please forgive me

Talk about the possible problems with using Immutablejs

  • Cost of learning. It’s not just your individual learning cost, it’s the whole team that needs to learn how to use it. The worst part is that it’s easy to introduce bad practices when no one is familiar with it but you have to use it. This creates a problem for your code
  • Compatibility issues, most third-party code doesn’t support this data structure, and you can’t adapt every component of your current project to fit it, so make sure you have compatibility and conversion between data formats. If using Immutablejs in a single component is fine, if you want to use it across the entire application and start using it from the Reducer initialState, there may be more issues to deal with, such as common onesreact-router-reduxImmutablejs is not supported, and you need more than thatfromJSandtoJSAdditional code is required to support it.

The last

There’s a lot more to talk about with Immutablejs, such as best practice considerations. I’ll stop here for space. I’ll continue when I get a chance

This article is also published in my zhihu front column, welcome your attention

Refer to the article

  • Immutable Data
  • React.PureComponent
  • Immutable.js, persistent data structures and structural sharing
  • Pure function
  • Reducers
  • A deep dive into Clojure’s data structures – EuroClojure 2015