This article will share a story about React performance optimization, which is a real story I encountered in my work. In the end, we improved React performance by dozens of times by modifying third-party library source code. This third party library is also well known and has 4.5K star on GitHub. It is called react-big-Calendar.
This work was not done by me alone, but by our team several months ago. I found it very interesting, so I summarized it and shared it with you.
In this article you can see:
- React Describes how to use performance analysis tools
- This section describes how to locate performance problems
- Common performance optimization methods and effects:
PureComponent
.shouldComponentUpdate
.Context
.On-demand rendering
, etc. - To solve the problem of the third party library
I’ve actually shared two previous posts about stories from my work:
-
Speed up hundreds of times, remember a data structure in the actual work of the application
-
Cross-project component sharing using Mono-repo
In particular, the speed of several hundred times, remember a data structure in the actual work of the application, this article in a platform single read more than 30,000, some friends also raised doubts. I think the problems mentioned in this article are unlikely to be encountered in reality, and the performance optimization in it is more theoretical and a bit alarmist. This view I basic is approved, I also mentioned in the article text may be a false demand, but had many technical problems is theoretical, we brush on leetcode topic or pure theory, the theory combined with the actual can play its real value, even if is alarmist, but did get a little bit performance, It also gives you another idea, which I think is also worth it.
In contrast, the problem mentioned in this article is not completely worryingly, but the real user demand, we through user research, found that the user does have so much data, demand can not be compressed, only technical optimization, this is also forced us to change the source of the third party library.
Demand background
As usual, to give you a quick understanding of the problem we are facing, I will briefly talk about the background of our requirements. I am still in that foreign company, not long ago we received a demand: to make a sports venue management Web App. One of the core functions is the venue schedule management, which is similar to the Calendar in Outlook. If you’ve ever used Outlook, you probably remember the Calendar. Basically, you can put our meetings and other schedules in it very easily. We’re going to do a similar thing here, where the owner of the stadium can use this calendar to manage the bookings for the space below him.
Suppose you are the owner of a badminton court and a client comes to you and says, hey boss, is the court free this Saturday? I’ll book an hour. The venue is heavily booked every day, and you can’t remember if it’s free on Saturday, so you go to our website and look at the calendar:
I’ve got two reservations for Friday, January 15th, and they’re all free for Saturday. The screenshot above is an official example of the React-big-Calendar, which we chose to use to build our own application.
The real scene
The above example is just to illustrate our application scenario, there are only two reservations and only one venue. However, our real clients are much larger than this. According to our research, our larger clients have hundreds of venues, and each venue may book 20 or 30 venues every day. In the example above, let’s change a boss with a good business. Suppose the boss has 20 badminton courts, and there are many customers every day. One day, a customer comes and says, “Hey, boss, is the court free this Saturday? But the owner was doing well, and the calendar he saw looked like this:
Venue 1 is full this week!! If the owner wants to find an available venue for the client, he needs to switch venues 1, 2… All the way to venue 20, my hands are sore… In order to reduce the burden on the boss’s hands, our product manager proposed a requirement to display 10 venue calendars on the page at the same time. Fortunately, react-big-Calendar itself supports this, which he called Resources.
The explosion performance
It looks like the basic functions we need for React-big-Calendar are available, and the future looks good until we render the actual data to the page… Our order is not just for display, but also to support a range of operations such as edit, copy, cut, paste, drag and drop, etc. Of course, the premise of all these operations is to select the reservation, the following screenshot is the time I select a reservation:
Just a simple click event, script execution takes 6827ms, render takes 708ms, total takes about 7.5s, which is TM! You want to sell this thing for money? Give it to me! I don’t even want to use it!
This is Chrome’s own performance tool, the basic steps are:
- To open the Chrome debug tool, click to
Performance
A bar - Click on the dot in the upper left corner to start recording
- To do what you want, in this case click on a reservation
- When you get the result you want, here I click on the order to burn
- Click on the dot in the upper left corner to see it after recording
To give you a better view, I’ve recorded a GIF of the action that shows how long it took for the click to respond and how long it took Chrome to load the performance data:
Test data volume
Is it because I’m deliberately using a lot of data that a single click above takes seven or eight seconds? No! The amount of data in my test is calculated according to the user’s real scenario: 10 venues are displayed at the same time, and 20 reservations are made for each venue every day. The weekly view is used above, that is, the data of 7 days can be seen at the same time, so the total number of reservations displayed is:
10 * 20 * 7 = 1400, 1400 reservations are displayed on the page.
For comparison, I’ll drop the optimized GIF to give you an idea of what this long talk will look like:
Location problem
React isn’t that slow. If it’s slow, it’s probably because the person who wrote the code didn’t write it well. React has a virtual tree. When a state changes, we only need to update the nodes associated with the state. To solve this problem, we installed the React Developer Tools. This is a Chrome plugin, Chrome plugin market can be downloaded, after successful installation, Chrome debugging tool below will be two more tabs:
Under the Components Tab, there is a setting to see which component updates are triggered each time you perform an action, and this is where we found a little surprise:
To see which updates are triggered by click events, let’s first reduce the amount of data, save only one or two reservations, and open this setting to see:
Well, that’s kind of interesting… I just clicked on a reservation and you updated all the components of the entire calendar for me! How many components does the calendar have? As can be seen from the above picture, there is a large grid between 10:00am and 10:30am. In fact, there is a dividing line in the middle of this large grid, but the color is lighter and it is not obvious. That is to say, there is a grid every 15 minutes. This 15 minutes is configurable, you can also set it to 1 minute, but there are more grids and performance is worse! According to our requirements, we provide users with three options: 15 minutes, 30 minutes and 1 hour. When the user selects 15 minutes, the most grids are rendered and the performance is the worst.
So if one cell is 15 minutes, how many cells are there? A day is 24 * 60 = 1440 minutes, 15 minutes per grid, 96 grids in total. Our weekly view will be displayed for a maximum of 7 days, which is 7 * 96 = 672 squares. We can display a maximum of 10 venues, which is 672 * 10 = 6720 squares. This is not counting the components occupied by the date and time themselves, so let’s round it up to **7000 squares **.
I just click on the reservation, and you update all the 7,000 boxes in the background to me, no wonder the performance is poor!
Take a closer look at the GIF above, I clicked on the smaller event, and when I clicked on it, notice that the larger event was also updated. There is also a blue box outside, which is not obvious, but it is updated, and this is confirmed later when I debug the Log. So out of the real 1,400, another 1,399 events were updated, which was unnecessary.
The event I mentioned here is the same thing as the reservation mentioned above. In the React-big-Calendar, it is called an event, which corresponds to the meaning of our business is reservation.
There was a commercial break, and I was inNuggets annual ranking eventEverybody see officer vote for me bai ~ thank everybody!
Why is that?
This is a familiar performance problem: put a state on the top layer and then pass it down layer by layer, and when one of the elements below updates the state, it causes the root node to update, which triggers all the child nodes below to update. The update does not necessarily involve re-rendering the DOM node, but runs the render function for each child node and then diff the result to see if the DOM node needs to be updated. React eliminates unnecessary DOM manipulation in this step, but the render function is required, and running the render function thousands of times can consume a lot of performance.
Speaking of this, I recall a source I read before, also about this problem, he used a list of 10,000 lines as an example, the original text is here: high-performance redux. Here’s an example from the article:
function itemsReducer(state = initial_state, action) {
switch (action.type) {
case 'MARK':
return state.map((item) = >action.id === item.id ? {... item,marked: !item.marked } :
item
);
default:
returnstate; }}class App extends Component {
render() {
const { items, markItem } = this.props;
return (
<div>
{items.map(item =>
<Item key={item.id} id={item.id} marked={item.marked} onClick={markItem} />
)}
</div>); }};function mapStateToProps(state) {
return state;
}
const markItem = (id) = > ({type: 'MARK', id});
export default connect(
mapStateToProps,
{markItem}
)(App);
Copy the code
This is an App that takes an items parameter and renders it as an Item component. Then you can click on a single Item to change its selected state. This works like this:
This code keeps all the data in items, and this parameter is passed in from the top App. When you click on an Item, it changes the items data to update the entire list. This running result has a similar problem with our Calendar above. When the status of a single Item changes, other items that are not involved will also be updated. The reason is the same: the items parameter at the top level has changed.
To be honest, I’ve seen this written a lot, if not from App, then from other large component nodes, causing similar problems. When the amount of data is small, this problem is not obvious and is often ignored, as in the figure above, even if there are ten thousand data items, because each Item is very simple, you will not be able to perceive it obviously by running render ten thousand times, which is just over one hundred milliseconds on the console. However, the Calendar we were faced with was much more complicated, and the calculation logic of each child node was more complicated, which eventually slowed our response speed down to seven or eight seconds.
Optimization scheme
Let’s start with the list of 10,000 items. The original author not only raised the problem, but also proposed a solution: the top-level App only transmits ID, and the data rendered by Item can be obtained by connecting to Redux Store. The following code is also from the same article:
// index.js
function items(state = initial_state, action) {
switch (action.type) {
case 'MARK':
const item = state[action.id];
return{... state, [action.id]: {... item,marked: !item.marked}
};
default:
returnstate; }}function ids(state = initial_ids, action) {
return state;
}
function itemsReducer(state = {}, action) {
return {
// Note here that the data has an additional IDS
ids: ids(state.ids, action),
items: items(state.items, action),
}
}
const store = createStore(itemsReducer);
export default class NaiveList extends Component {
render() {
return (
<Provider store={store}>
<App />
</Provider>); }}Copy the code
// app.js
class App extends Component {
static rerenderViz = true;
render() {
// The App component only uses IDS to render the list and does not care about the specific data
const { ids } = this.props;
return (
<div>
{
ids.map(id => {
return <Item key={id} id={id} />; })}</div>); }};function mapStateToProps(state) {
return {ids: state.ids};
}
export default connect(mapStateToProps)(App);
Copy the code
// Item.js
// The Item component connects itself to Redux for data
class Item extends Component {
constructor() {
super(a);this.onClick = this.onClick.bind(this);
}
onClick() {
this.props.markItem(this.props.id);
}
render() {
const {id, marked} = this.props.item;
const bgColor = marked ? '#ECF0F1' : '#fff';
return (
<div
onClick={this.onClick}
>
{id}
</div>); }}function mapStateToProps(_, initialProps) {
const { id } = initialProps;
return (state) = > {
const { items } = state;
return {
item: items[id], }; }}const markItem = (id) = > ({type: 'MARK', id});
export default connect(mapStateToProps, {markItem})(Item);
Copy the code
This code is optimized in the following areas:
- Remove data from pure
items
Split intoids
anditems
. - The top-level components
App
useids
To render the list,ids
There was onlyid
, so as long as the state of a single piece of data changes instead of adding and deleting,ids
It doesn’t have to change, soApp
No updates. Item
Components connect to the data they need and update it when the data they care about changes. Data changes of other components do not trigger updates.
Unpack third-party library source code
Above through the use of debugging tools I saw a familiar phenomenon, and guess the reason for his slow, but it is only a guess, specific is not this reason to see his source code to confirm. Fortunately, I looked at his documentation before looking at his source code and found this:
The react-big-Calendar receives two arguments onSelectEvent and selected. Selected represents the currently selected event (scheduled). OnSelectEvent can be used to change the value of selected. This means that when we select a reservation, we change the selected value. Since this parameter is passed down from the top, it causes all of the children below to be updated, which in our case is approximately 7000 background cells + 1399 other events, thus causing components that do not need to be updated to be updated.
The top layer selected, Context?
It is understandable that the act-big-calendar has a selected parameter at the top level, because the user can change this value to control the selected events. There are two ways to select an event:
- Users click on an event to change it
selected
The value of the - Developers can modify it externally
selected
To select an event
From the previous 10,000 list optimization experience, we know the solution to this problem: use the selected component to connect to Redux itself to get the value, rather than passing it in from the top. ** Unfortunately, the React-big-Calendar doesn’t use Redux or any other state management libraries. ** If he was using Redux, we could also consider adding an action for the external modifier selected, but he didn’t. Can’t play without Redux? Of course not! React actually comes with a global state sharing function called Context. React Context API the React Context API is described in detail in my previous article. I will not cover its basic usage here, but I would like to mention another feature: When using the Context Provider package, if the value you pass in changes, the render function on all of the following nodes is run, which is the same as the normal props mentioned earlier. However, if the child node under the Provider is a PureComponent, you can run the grandchild node using the value without running the render function of the child node.
What do I mean by that? Let me simplify the problem that we’re facing. Suppose we have only three layers, the first is the top container Calendar, the second is the blank grid for the background (son), and the third is the event that actually needs to use Selected (grandson) :
Example code is as follows:
// SelectContext.js
// A simple Context
import React from 'react'
const SelectContext = React.createContext()
export default SelectContext;
Copy the code
// Calendar.js
// Use the Context Provider package to receive the selected argument and render the Background Background
import SelectContext from './SelectContext';
class Calendar extends Component {
constructor(. args) {
super(... args)this.state = {
selected: null
};
this.setSelected = this.setSelected.bind(this);
}
setSelected(selected) {
this.setState({ selected })
}
componentDidMount() {
const { selected } = this.props;
this.setSelected(selected);
}
render() {
const { selected } = this.state;
const value = {
selected,
setSelected: this.setSelected
}
return (
<SelectContext.Provider value={value}>
<Background />
</SelectContext.Provider>)}}Copy the code
// Background.js
// Inherit from PureComponent, render the background grid and the Event
class Background extends PureComponent {
render() {
const { events } = this.props;
return (
<div>
<div>There are 7,000 background cells in there</div>Below is rendering 1400 events {events.map(event =><Event event={event}/>)}
</div>)}}Copy the code
// Event.js
// Select selected from Context to determine your render style
import SelectContext from './SelectContext';
class Event extends Component {
render() {
const { selected, setSelected } = this.context;
const { event } = this.props;
return (
<div className={ selected= = =event ? 'class1' : 'class2'} onClick={()= > setSelected(event)}>
</div>
)
}
}
Event.contextType = SelectContext; / / connection Context
Copy the code
What is PureComponent?
We know that if we want to prevent the render function of a component from running, we can return false in shouldComponentUpdate. If the new props is still the same as the old props, we don’t need to run Render. ShouldComponentUpdate:
shouldComponentUpdate(nextProps) {
const fields = Object.keys(this.props)
const fieldsLength = fields.length
let flag = false
for (let i = 0; i < fieldsLength; i = i + 1) {
const field = fields[i]
if (
this.props[field] ! == nextProps[field] ) { flag =true
break}}return flag
}
Copy the code
This code simply compares the new Props to the old props, and returns false if the same, without rendering. The PureComponent is a shouldComponentUpdate implemented by the React app. So our Background component above inherits from the PureComponent and comes with this optimization. If the Background parameter itself does not change, it will not update, whereas the Event itself is connected to the SelectContext, so the Event will update when the value of the SelectContext changes. This implements what I said earlier: if the child node under the Provider is a PureComponent, you can run the render function of the child node and run the grandchild node that uses the value.
PureComponent doesn’t work
Ideal is beautiful, reality is skeletal… Theoretically, if I had changed the middle son layer to PureComponent, the 7000 background cells wouldn’t have been updated and the performance would have been much better. But when I tested it, it didn’t work. The 7000 boxes were still updated. What the hell? This is a problem with the PureComponent itself: shallow comparisons. Pay attention to this. Props [field]! == nextProps[field], what if this. Props[field] is a reference object, such as an object, an array, or something like that? Since it is a shallow comparison, even if the content of the attribute before and after is the same, but the reference address is changed, the two are not the same, which will cause the component to update!
In the React-big-calendar, there are a lot of operations that return a new object after the calculation. For example, in the top-level calendar, there are operations like this:
Code address: github.com/jquense/rea…
This code recalculates the state state every time the props changes.
Code address: github.com/jquense/rea…
Note that its return value is a new object, and properties in that object, such as mergeWithDefaults, return a new object each time:
Code address: github.com/jquense/rea…
As a result, the props received by the middle child node are the same each time, but because it is a new object, the running result of the node needs to be updated even if PureComponent is used. This operation is abundant in his source code, and is actually understandable from a functional point of view, as I do it sometimes… Sometimes, when a property is updated, we are not sure whether to update the following component or not. We simply return a new object to trigger the update.
The crooked way should be updated
If only one or two attributes return the new object in this way, I can also consider reconstructing it, but after debugging a large number of existing attributes are like this, I am not his author, and I do not know whether it will change the function, I did not dare to move. But do not move the performance also stretched ah, want to think about it, or in the son shouldComponentUpdate move some hands and feet. Simple this.props[field]! NextProps [field] == nextProps[field] == props [field] The depth of the two objects are need to use recursion, or see the React diff algorithm for performance optimization, but no matter how you optimize this algorithm, the worst performance of the time when two objects, because they are the same, you need to traverse to the most deep place to be sure they are the same, if the object is deep, This recursive algorithm is not necessarily faster than running render, and most of the cases we face are worst-case scenarios. So the recursive comparison is not very good, and in fact if you know the data, you don’t have a circular reference or anything, you can think about just converting two objects to a string to compare them, which is, right
JSON.stringify(this.props[field]) ! = =JSON.stringify(nextProps[field])
Copy the code
Note that this method only works if you know the props data, no cyclic references, no changing symbols, functions, etc., because json.stringify will drop symbols and functions when it executes, so I call it a bad way to optimize performance.
ShouldComponentUpdate, which translates to a string comparison, was added to the components of the background grid and the performance was significantly improved, with the corresponding click speed dropping from 7.5 seconds to around 5.3 seconds.
On-demand rendering
Above we blocked 7000 background grid updates with shouldComponentUpdate and the response time dropped by more than 2 seconds, but it still takes more than 5 seconds which is also unacceptable and needs further optimization. As we said earlier, it would be nice if we could block another 1399 updates, but after analyzing his data structure, we found that his data structure is not the same as the list example we mentioned earlier. In our list example, all the data is in items, so checking is an attribute of item. In the react-big-Calendar data structure, event and selectedEvent are two different attributes. Each event determines whether it is selected by determining whether its event is equal to a selectedEvent. The result of this is that every time we select an event, the value of selectedEvent will change, and the properties of each event will change, i.e., update, and run the render function. Without changing this data structure, another 1,399 events could not be prevented. But changing this data structure is a big change, and for a third-party library, we don’t want to change so much, what should we do?
This way is not going to work, we can completely change the idea, the background of 7000 grid, plus 1400 events, is the user screen that big, can see it all? There’s no way to finish it. Since there’s no way to finish it, let’s just render the part he can see! In this way, we find a library: react-visibility sensor. The library is also simple to use:
function MyComponent (props) {
return (
<VisibilitySensor>
{({isVisible}) =>
<div>I am {isVisible ? 'visible' : 'invisible'}</div>
}
</VisibilitySensor>
);
}
Copy the code
Combined with what we said above, we can cover VisibilitySensor on Background:
class Background extends PureComponent {
render() {
return (
<VisibilitySensor>
{({isVisible}) =>
<Event isVisible={isVisible}/>
}
</VisibilitySensor>)}}Copy the code
Then the Event component, if it finds itself invisible, does not render and only renders when it is visible:
class Event extends Component {
render() {
const { selected } = this.context;
const { isVisible, event } = this.props;
return (
{ isVisible ? (
<div className={ selected= = =event ? 'class1' : 'class2'} >Complex content</div>
) : null}
)
}
}
Event.contextType = SelectContext;
Copy the code
We changed it along these lines and found that the performance improved again and the overall time dropped to about 4.1 seconds:
Taking a closer look at the drawing above, we see that Rendering event Rendering time has decreased from around 1 second to 43 milliseconds, which is more than 20 times faster due to the reduction in Rendering content, but Scripting time (i.e., script execution time) is still 4.1 seconds, which needs to be optimized.
Cut the Mousedown event
There’s not much left to do with rendering, except to look at Scripting, where we find the mouse events in the performance graphics a bit harsh:
A single click triggers three click events simultaneously: mousedown, mouseup, and Click. If we can kill Mousedown, it will save mouseup half the time. Let’s see what he does when he registers these two events. You can search for mousedown globally in your code and find it in selection. js. You can read the class code and see that it is a typical observer pattern. Mouseup is used to implement drag-and-drop functions for events. Mousedown marks the start of drag-and-drop and mouseup marks the end of drag-and-drop. If I take that away, the drag-and-drop function is gone. After communicating with the product manager, we need to drag and drop it later, so this cannot be deleted.
At this point, I’m running out of options, but the response time is still four seconds, which is mind-boggling
There was nothing else I could do, so I started playing around and suddenly I noticed something wrong with mousedown’s call stack:
I split the call stack into three numbers:
- There are a lot of familiar function names in there, like what
performUnitOfWork
.beginWork
It’s not all meDid you mention it in the React Fiber article?So these are function calls inside React itself render
Function, which is a rendering function for a component- this
render
It’s called againrenderEvents
Function, which appears to be used to render the list of events, where most of the time is spent
I can’t kill the mousedown listener itself, but can the execution inside be optimized? RenderEvents is already written by the library itself, so you can search globally to see where it is executed. ShouldComponentUpdate () {shouldComponentUpdate () {shouldComponentUpdate ();}} Then look at the performance data:
We saw the Scripting drop to around 3.2 seconds, about 800 milliseconds less than before, and the Mousedown time dropped from a few hundred milliseconds to 50 milliseconds. You can hardly see it in the image, and mouseup events are not visible at all
Painful castration function
So far, none of our performance optimizations have castration, and the response time has dropped from 7.5 seconds to just over 3 seconds, almost doubling the optimization. However, the current speed is still more than three seconds, which I can’t stand as a user, let alone an engineer. Do how? We’re really running out of ideas…
Looking at the performance graph above, there are two major time wasters, one is the Click event and the other is the timer. I don’t know where timer came from, but we do know about the Click event, which is when the user clicks on an event, changes the Selected property of the SelectContext, and then the selected property is passed in from the top node to trigger the update of the component below, The middle child skiches the update with shouldComponentUpdate, and the grandson directly connects to the SelectContext to get the Selected property to update its state. This is the process we optimized earlier, but wait, this seems to be a bit of a problem.
In our scenario, the middle son node actually contains up to 7000 background grids, although we skipped the render execution with shouldComponentUpdate, but 7000 shouldComponentUpdate should take time to execute. Is there a way to skip shouldComponentUpdate as well? This seems to be a new idea, but after our discussion, we found that it could not be done while maintaining the function, but it could be done by moderately neutering a function. Which function is neutering? That’s the selected property exposed to the outside world!
As mentioned earlier, there are two ways to select an event:
- Users click on an event to change it
selected
The value of the - Developers can modify it externally
selected
To select an event
The selected property is placed on the top-level component for the second function, which allows external developers to change the selected event via the managed Selected property. However, after our evaluation, external modification of selected is not our requirement. All our requirements are that users click to select, that is to say, we can not use the function of external modification of selected.
The selected value does not need to be placed on the top layer of the event, but only on the container surrounding the event. Changing the selected value will only trigger the update of the event. Add another layer of EventContainer to our previous Calendar — Background — Event model, making it Calendar — Background — EventContainer — Event. The SelectContext.Provider also doesn’t wrap Calendar, it just wraps EventContainer. The code might look something like this:
// Calendar.js
// Calendar is simple. You don't need to accept the selected argument or the selectContext.provider package
class Calendar extends Component {
render() {
return (
<Background />)}}Copy the code
// Background.js
// shouldComponentUpdate should be used to block Background updates and see if any other parameters have changed since the selected is removed from the top layer
// Changing selected does not trigger Background updates
// Background will render EventContainer instead of a single event
class Background extends PureComponent {
render() {
const { events } = this.props;
return (
<div>
<div>There are 7,000 background cells in there</div>Below is a rendering of 1400 events<EventContainer events={events}/>
</div>)}}Copy the code
// EventContainer.js
// EventContainer requires the selectContext.provider package
// The code is similar to the previous Calendar
import SelectContext from './SelectContext';
class EventContainer extends Component {
constructor(. args) {
super(... args)this.state = {
selected: null
};
this.setSelected = this.setSelected.bind(this);
}
setSelected(selected) {
this.setState({ selected })
}
render() {
const { selected } = this.state;
const { events } = this.props;
const value = {
selected,
setSelected: this.setSelected
}
return (
<SelectContext.Provider value={value}>
{events.map(event => <Event event={event}/>)}
</SelectContext.Provider>)}}Copy the code
// Event.js
// Event is the same as before, select selected from Context to determine your render style
import SelectContext from './SelectContext';
class Event extends Component {
render() {
const { selected, setSelected } = this.context;
const { event } = this.props;
return (
<div className={ selected= = =event ? 'class1' : 'class2'} onClick={()= > setSelected(event)}>
</div>
)
}
}
Event.contextType = SelectContext; / / connection Context
Copy the code
The biggest change in this structure is that when the selected changes, the updated node is EventContainer instead of the top Calendar, so that updates to other Calendar nodes are not triggered. The downside is that Calendar can’t receive selected from the outside.
One thing to note is that if you render the Event list directly under EventContainer like we did, the Selected will be used as the state of EventContainer. However, if there is a layer between EventContainer and Event that needs to be passed through, the Context is still needed. The middle layer is similar to the previous one, and shouldComponentUpdate is used to prevent updates.
ShouldComponentUpdate on Background should also be deleted because the selected update is no longer on the top layer.
When we optimized it this way, the performance improved again:
The Scripting time has now dropped directly from 3.2 seconds to 800 milliseconds, and the Click event is only 163 milliseconds. The lag is now noticeable from my use, so let’s record a GIF for comparison:
You can hardly see the lag in the GIF above, but why do we still have 800 milliseconds on our performance graph and a long Timer Fired. As we went through it, it turned out to be a mistake, a Timer Fired. The performance came up as soon as I started recording, when I was still switching pages before I could click, If we click on it, we’ll see that it’s actually a timed task to check the visibility of elements introduced by the React-visibility Sensor on demand, not the response time of our click events. Remove this, and the response time for our click event is actually less than 200 milliseconds.
From 7 seconds to less than 200 milliseconds, more than 30 times the performance optimization, finally can cross, ha ha 😃
conclusion
This article shares a case that I actually met in my work. The effect is to optimize the response time of about 7 seconds to less than 200 milliseconds, which is more than 30 times. The cost of optimization is to sacrifice a rarely used function.
I thought if I optimized it I’d give this guy a PR for everyone. But optimization is a little tricky:
- Using the
JSON.stringify
To carry outshouldComponentUpdate
For the function,Symbol
Property changes cannot be monitored and are not suitable for open use, but can only be used on a small scale under the control of the data itself. - Sacrificing a controlled property that is exposed to the outside world
selected
, broke the function.
Based on these two points, we didn’t mention PR and put the modified code into our own private NPM repository.
The following is a summary of the problems and optimization ideas faced by this paper:
Problems encountered
We need to make a management calendar for the stadium, so we use the library of React-big-Calendar. The amount of data we need is to render 7000 background cells, and then render 1400 events on that cell. After rendering the nearly 10,000 components, we found that a single click took more than 7 seconds and was completely unusable. After careful investigation, we found that the reason for the slowness was that clicking the event would change a selected attribute. This property is passed down from the top level and when changed causes all components to update, meaning that all components run the Render function.
Step 1 Optimization
To prevent unnecessary render runs, we introduce Context and put selected on Context for pass-through. ShouldComponentUpdate (); shouldComponentUpdate (); shouldComponentUpdate (); shouldComponentUpdate ();
The first step is optimizing the effect
Response time dropped from just over 7 seconds to just over 5 seconds.
The first step is optimization
There are still 1400 underlying events, and after getting the Selected property, 1400 component updates still take a lot of time.
Step 2 Optimization
To reduce the number of events updated after clicking, we introduced on-demand rendering for events, rendering only the event components visible to the user. We also optimized mouseDown and Mouseup using shouldComponentUpdate to prevent unnecessary updates.
The second step is to optimize the effect
Response time dropped from just over 5 seconds to just over 3 seconds.
The second step is to optimize the problem
The response time is still more than three seconds, and it is found that shouldComponentUpdate should take a long time to run the render function 7000 times.
Step 3 Optimization
To make the 7000 background grid shouldComponentUpdate not even work, we emasculated the top level of the selected property and put it directly on the event container. Its update should no longer trigger the background grid update. ShouldComponentUpdate is not even running.
The third step is to optimize the effect
The response time dropped from more than three seconds to less than 200 milliseconds.
The third step is to optimize the problem
Function is emasculated, otherwise perfect!
References:
The react – big – calendar warehouse
high-performance-redux
At the end of this article, thank you for your precious time to read this article. If this article gives you a little help or inspiration, please do not spare your thumbs up and GitHub stars. Your support is the motivation of the author’s continuous creation.
Welcome to follow my public numberThe big front end of the attackThe first time to obtain high quality original ~
“Front-end Advanced Knowledge” series:Juejin. Cn/post / 684490…
“Front-end advanced knowledge” series article source code GitHub address:Github.com/dennis-jian…