background

The company recently used the Gantt chart feature in its project, so it integrated an open source Gantt chart plug-in.

The main function of gantt chart is project management. It can graphically represent the sequence and duration of activities of any particular project through the activity list and time scale, as shown in the figure below

Those of you who have played the Gantt chart know that the front end of the Gantt chart is basically drawing. Drawing is a very high requirement for front-end development and performance. Frequent interaction also leads to more stringent performance requirements for development.

The phenomenon of

The underlying phenomenon is simple. When I drag the View area of the Gantt chart, I can feel the lag and drag. As you all know, when it comes to drawing related animation operations, it takes 60fps to get to the smooth stage. 30fps is slow, 20fps is slow.

Therefore, I used the Frame Rendering Stats tool to observe the Frame rate value with naked eyes. In addition to observing the FPS of the current page operation, it can also monitor the MEMORY usage of the GPU. The location of the tool is also easy to find. In Chrome Devtools Rendering, check and turn on


When I use the tool to observe the FPS while the view area is sliding steadily and at a constant speed, I can feel a noticeable lag and drag. Its detection value is only 31fps at the highest and 26fps at the lowest, which basically belongs to the severe level of the stuck. If you change a low-end device, then its display effect is certainly unimaginable.

Analysis of the

Now that we’ve found the problem, let’s analyze what the problem is. Then open the Performance tool and start recording. At the same time, slide the view area at a steady and uniform speed. After sliding for a few seconds, stop recording and get an analysis report like this:

The Gantt chart plugin and the main technology stack are react. In react 16’s latest Fiber architecture, subtasks are broken down to allow responses to be fed back faster. Based on the average monitor refresh rate (60Hz) and the highest refresh rate supported by current browsers, the average task time per frame is only 16.6ms. When the duration of a single frame task exceeds 16.6ms, stalling and frame dropping occur.

However, according to our analysis, most of the tasks in the scroll above are larger than 40ms, and even produce a LongTask (the official definition of a longTask is greater than 50ms, or 20fps). So let’s expand and see what events in a single task take longer to execute.

Then click on one of the tasks to zoom in. It can be seen that the number one selftime (self execution time) is an anonymous function. Continue clicking on the code stack on the right to see which line of code takes longer to execute.

When clicked, it automatically jumps us to the Source module in Devtools and marks the execution time of the code on the left side of the function. As you can see below, line 74 toLocaleDateString is very time-consuming. Since the rendering generation of function/class components is synchronous, taking a long time slows down the efficiency of render and thus the overall frame rate.

Time zone conversion pot

Date. Prototype. ToLocaleDateString () function is the time of different language text. For example,

const event = new Date(Date.UTC(2012.11.20.3.0.0));
const options = { weekday: 'long'.year: 'numeric'.month: 'long'.day: 'numeric' };

console.log(event.toLocaleDateString('de-DE', options));
// expected output: Donnerstag, 20. Dezember 2012
console.log(event.toLocaleDateString('ar-EG', options));
// Expected output: ال ع planet خم س، ٢٠ د سم planet ٠ س، ٢ س، ٢ planet emitted emit
console.log(event.toLocaleDateString(undefined, options));
// expected output: Thursday, December 20, 2012 (varies according to default locale)
Copy the code

But how could such a seemingly harmless process take so long?

Since looking directly at this part of v8’s source code is rather hardcore, we chose to look at toLocaleDateString’s Polyfill — FormatJS. This library has always existed as an internationalization polyfill for Date, including time zone internationalization and time text internationalization.

We find the toLocaleString method in Packages/Intl-datetimeFormat/SRC/to_LOCALe_string. ts in FormatJS. This method creates a time formatting object:

Following the implementation of the DateTimeFormat class, you can see that there is a variable called localeData. This variable is the text content of each language when we do internationalization. Again, there is a variable called tzData above, which is the contents of the time zone database:

As you follow along, the ResolveLocale method is the core method for handling the currently selected time zone. Where all internationalized text is algorithmatically filtered to match the currently selected language text (especially the indexOf time-consuming method shown below).

The solution

The only solution to this call time problem is to cache the memo on existing execution results. The solution is very simple: convert the time into a timestamp as the cache key, store it in the cache, and then directly read from the cache:

After optimization, we used performance again for analysis. Not only did FPS have a significant increase in visual and numerical values, but longTask no longer existed and the average task time was reduced to 23ms. Basically achieved fluency and solved the problem of laton.

However, we will continue to work on toLocaleDateString’s sibling API: Intl.dateTimeFormat.

About the Intl. DateTimeFormat

Intl.DateTimeFormat is a relatively new time formatting API. It is the biggest difference from toLocaleDateString in the use of the same time, supports the format of any date object, API design biased to the constructor, more conducive to cache design. For example:

console.log(new Intl.DateTimeFormat('en-US').format(date));
// expected output: "12/20/2020"

console.log(new Intl.DateTimeFormat('en-GB', { dateStyle: 'full'.timeStyle: 'long' }).format(date));
// Expected output "Sunday, 20 December 2020 at 14:23:16 GMT+11"
Copy the code

Similarly, Intl.DateTimeFormat, which ranks next in react time, is worth dealing with after performance tuning toLocaleDateString above. Continuing to look at the code time:

The discovery time of this method is also not low: 7.1ms, there is room for improvement. The polyfill implementation of this method, like toLocaleDateString above, only instantiates the DateTimeFormat object. The only difference is that there is a manual instantiation, and there is an instantiation for you:

So let’s go ahead and cache intl.dateTimeFormat.

Final optimization result

As with toLocaleDateString, we just need to optimize the intl.dateTimeFormat instance. Caches are still used, but the key has been changed to the locale + transform option as the only parameter:

After optimization, we collected a performance sample again.

Through the detection, the FPS has reached a minimum of 45 and a maximum of 50. Basically smooth (because Devtools is a performance drain when it’s on, the actual frame rate is higher). Compared with before optimization, the improvement was 61%. The long task does not exist

At the end

Of course, this optimization process is only a preliminary optimization. As you can see, while the time spent on individual tasks has dropped significantly, there is still room for improvement. It needs to be as low as 16.6ms for full fluency.

To summarize: use intl.dateTimeFormat instead of toLocaleDateString, and cache the constructor to improve performance. Be aware of this in other internationalized scenarios, such as numbers

In addition, this performance optimization solution has been submitted to the upstream open source project and has been consolidated into the repository as of August 15: github.com/MaTeMaTuK/g…