• React Performance forward
  • Alex Reardon
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: hexiang
  • Proofread by: WZnonstop, zephyrJS

Head shot by James Padolsey in Unsplash

I wrote a drag and drop library for React -beautiful-dnd šŸŽ‰. Atlassian created this library to provide a beautiful and easy-to-use drag-and-drop experience for lists on the site. You can read the introduction: Reflections on drag and drop. The library is completely state-driven — user input causes state changes and then updates what the user sees. This conceptually allows dragging with any input type, but too much state-driven dragging can lead to performance drawbacks. šŸ¦‘

We recently released the fourth version of React-beautiful-DND, Version 4, which includes massive performance improvements.

The data in the list is based on a configuration with 500 draggable cards, recorded with meters enabled in the development version, both of which slow things down. But at the same time, we used a very capable machine for this record. The exact performance improvement will depend on data set size, device performance, etc.

If you look closely, we see a 99% performance improvement šŸ¤˜. These improvements are all the more impressive because the library has been extremely optimized. You can see the performance boost in either the large list example or the large panel example šŸ˜Ž.


In this blog, I’ll explore the performance challenges we faced and how we overcame them to achieve such important results. The solutions I’ll talk about are well suited to our problem domain. There are some principles and techniques that will emerge — but the specific issues may vary in the problem area.

Some of the techniques I describe in this blog post are fairly advanced, and most of them are best used within the boundaries of the React library rather than directly within the React application.

TLDR;

We’re all busy! Here’s a very high level overview of the blog:

Avoid render calls whenever possible. In addition to the previously explored technologies (round 1, Round 2), I have some new understandings here:

  • Avoid using props to pass messages
  • callrenderNot the only way to change styles
  • Avoid working offline
  • Batch relevant Redux status updates if you can

State management

React-beautiful-dnd uses Redux for most of its state management. This is an implementation detail, and users of the library can use any state management tool they like. Much of the content in this blog is specific to Redux applications — however, some of the techniques are generic. For those unfamiliar with Redux, here are some terms:

  • Store: A global state container – usually placed in context, so connected components can be registered for updates.
  • Connected component: A component registered directly with the store. Their responsibility is to respond to status updates in the Store and pass props to unconnected components. These are often referred to as smart or container components
  • Disconnected component: A component that is not connected to Redux. They are typically wrapped in components connected to the Store that receive props from state. These are often referred to as clunky or presentation components

If you’re interested, here’s some more detailed information about these concepts from Dan Abramov.

The first principle

As a general rule, you should avoid calling the render() function of a component whenever possible. Render calls are costly for the following reasons:

  • renderThe process of the function call is expensive
  • Reconciliation

Reconciliation is the process by which React builds a new tree, reconciles it with the current view (the virtual DOM), and performs the actual DOM updates as needed. The Reconciliation process is triggered after calling a render.

The processing and reconciliation of the Render functions are costly in terms of scale. If you have 100 or 10,000 components, you probably don’t want each component to coordinate the shared state in a store with each update. Ideally, only the component that needs to be updated should call its render function. This is especially true for our 60 updates per second (60 FPS) drag-and-drop.

I explored techniques for avoiding unnecessary render calls in the last two blogs (round 1, Round 2), and the React documentation on this subject also covers this topic. Just like everything has a balance, if you try too hard to avoid rendering, you can introduce a lot of potential redundant memory checks. This topic has been discussed elsewhere, so I won’t go into detail here.

In addition to the rendering cost, the more connected components you have when using Redux, the more state queries (mapStateToProps) and memory checks you need to run with each update. I discussed redux-related status queries, selectors, and memos in detail in the Round 2 blog.

Problem 1: Long pause before dragging begins

Note the time difference between when the circle appears under the mouse and when the selected card turns green.

When clicking on a card in a large list, it takes quite a long time to start dragging, which is 2.6 s šŸ˜¢! This is a bad experience for users who expect drag-and-drop interactions to be real-time. Let’s take a look at what happened and some of the techniques we used to solve the problem.

Issue 1: Publication of native dimensions

To perform the drag, we put a snapshot of the dimensions (coordinates, size, margins, etc.) of all relevant components into our state and at the beginning of the drag. We then use this information during the drag to calculate what needs to be moved. Let’s see how we do this initial snapshot:

  1. When we start dragging, we’re going tostateMaking a requestrequest.
  2. associatedThe dimension publishing component reads thisrequestAnd see if they need to post anything.
  3. If they need to post, they willNot connectedSet one on the publisher of the dimensionshouldPublishProperties.
  4. Not connectedDimension publishers collect dimensions from the DOM and use thempublishCall back to publish the dimension

Ok, so here are some pain points:

  1. When we start dragging, we’re instateOn launched onerequest.
  2. The associated dimension publishing component reads this request and sees if they need to publish anything

At this point, each associated dimension publisher needs to perform a check against the Store to see if they need to request the dimension. Not ideal, but not terrible. Let’s move on

  1. If they need to publish, they set one up on the unconnected dimension publishershouldPublishattribute

We used to use the shouldPublish attribute to pass a message to the component to perform an action. Unfortunately, this has the side effect of causing the component to render, which causes reconciliation of the component itself and its children. This is expensive when you do it on many components.

  1. Not connectedDimension publishers collect dimensions from the DOM and use thempublishCall back to publish the dimension

Things are going to get worse. First, we read many dimensions from the DOM at once, which may take some time. From there each dimension publisher will publish a dimension individually. These dimensions are stored in the state. This change in state triggers the store subscription, which causes the associated component state query and memory check in Step 2 to be performed. It also causes other connected components in the application to similarly run redundancy checks. Therefore, every time an unconnected dimension publisher publishes a dimension, it causes redundant work for all other connected components. This is an O(n ^ 2) algorithm. – Worse! Ah.

The dimension marshal

To address these issues, we created a new role to manage the dimension collection process: Dimension Marshal. Here’s how new dimension publishing works:

Drag work before:

  1. Let’s create adimension marshalAnd put it incontextIn the.
  2. When the dimension publisher is loaded into the DOM, it is loaded fromcontextReads thedimension marshalAnd to thedimension marshalRegister yourself. Dimension publishers no longer listen directly to stores. Therefore, there are no more unconnected dimension publishers.

Drag work begins:

  1. When we start dragging, we’re going tostatearequest 怂
  2. dimension marshalreceiverequestAnd request the key dimension (drag card and its container) directly from the desired dimension publisher to begin the drag. These are published to the Store and you can start dragging.
  3. And then,dimension marshalDimensions for all other Dimension Publishers will be requested asynchronously in the next frame. Doing so splits the cost of collecting the dimension from the DOM and publishing the dimension (next step) into a separate frame.
  4. In the other frame,dimension marshalPerform batching for all collection dimensionspublish. At this point, state is completely mixed; it only takes three frames.

Other performance advantages of this approach:

  • Fewer status updates result in less work for all connected components
  • There are no more connected dimension publishers, which means that the processing done in these components no longer needs to happen.

Because Dimension Marshal knows all the ids and indexes in the system, it can directly request any dimension O (1). This also enables it to determine how and when dimensions are collected and published. Previously, we had a separate shouldPublish message that responded immediately to everything. Dimension Marshal gives us a lot of flexibility in terms of tuning performance for this part of the life cycle. If necessary, we can even implement different collection algorithms based on device performance.

conclusion

We improved the performance of dimension collection in the following ways:

  • Pass messages without obvious updates without using props.
  • Break the work down into multiple frames.
  • Batch update status across multiple components.

Issue 2: Style updates

When a drag starts, we need to apply some style to each Draggable (e.g. Pointer-events: none;). . To do this we apply an inline style. To apply inline styles we need to render each Draggable. This could result in potentially calling Render on 100 draggable cards when the user tries to start dragging, which would cost 350 ms for 500 cards.

So, how do we update these styles without rendering?

Dynamic sharing styles šŸ’«

For all Draggable components, we now apply shared data attributes (such as data-react-beautiful-dnd-Draggable). The data property never changes. However, we dynamically change the styles applied to these data attributes through the shared style elements we created in the page head.

Here’s a simple example:

// Create a new style element const el = document.createElement('style');
el.type = 'text/css'; // Add it to the page header const head = document.querySelector('head'); head.appendChild(el); // At some point in the future, we can completely redefine the entire content of the style element constsetStyle = (newStyles) => { el.innerHTML = newStyles; }; // We can apply styles at some point in the lifecyclesetStyle(` [data-react-beautiful-dnd-drag-handle] { cursor: grab; } `); // Another time can change these stylessetStyle(` body { cursor: grabbing; } [data-react-beautiful-dnd-drag-handle] { point-events: none; } [data-react-beautiful-dnd-draggable] {transition: transform 0.2s ease; } `);Copy the code

If you’re interested, you can see how we implement it.

At various points in the drag life cycle, we redefined the content of the style rules themselves. You usually change the style of an element by switching classes. However, by using defined dynamic styles, we can avoid applying a new class to render any components that need to be rendered.

We use the data attribute instead of class to make the library easier for developers to use, who don’t need to merge the classes we provide with their own.

Using this technique, we can also optimize other phases of the drag-and-drop lifecycle. We can now update card styles without rendering them.

Note: You can create a preset style rule set and then change itbodyOn theclassTo activate different rule sets to implement similar techniques. However, by using our dynamic approach, we can avoid inbodyTo addclassEs. And allows us to use rule sets with different values over time, rather than just being fixed.

Don’t be afraid, the data property’s selector performance is very good, quite different from the Render performance.

Issue 3: Block unwanted drags

When a drag starts, we also call Render on the Draggable to update the canLift Prop to false. This is used to prevent a new drag from starting at a specific time in the drag life cycle. We need this prop because there is some combination of keyboard and mouse input that allows the user to start dragging something while already dragging something else. We still really need this canLift check — but how do we do it without calling render on all the Draggables?

The context function combined with State

Instead of using render to update the props for each Draggable to prevent dragging, we added the canLift function to the context. This function can get the current state from the Store and perform the required checks. In this way, we were able to perform the same checks without updating the Draggable props.

This code is greatly simplified, but it illustrates the approach:

import React from 'react';
import PropTypes from 'prop-types';
import createStore from './create-store'; Class Wrapper extends React.Component {// Put canLiftFn on the context static childContextTypes = {canLiftFn: PropTypes.func.isRequired, } getChildContext(): Context {return {
    canLiftFn: this.canLift,
   };
 }

 componentWillMount() { this.store = createStore(); } canLift = () => {// At this point we can enter store // so we can perform the required checksreturnthis.store.getState().canDrag; } / /... } class DraggableHandle extends React.Component { static contextTypes = { canLiftFn: PropTypes. Func. IsRequired,} / / we can use it to check whether we were allowed to begin to drag and dropcanStartDrag() {
    return this.context.canLiftFn();
  }

  // ...
}
Copy the code

Obviously, you just want to do this very discreetly. However, we found it to be a very useful way to provide store information to components without updating props. Since this check is done against user input and has no rendering impact, we can avoid it.

There is no longer a long pause before the drag begins

Drag in a list of 500 cards and immediately drag

By using the techniques described above, we can reduce the drag time on a single 500 dragable cards from 2.6s to 15 ms (in one frame), which is a 99% reduction šŸ˜! .

Problem 2: Slow displacement

Frame rate drops when moving a large number of cards.

When moving from one large list to another, the frame rate drops significantly. When you have 500 draggable cards, moving to the new list will take about 350 ms.

Issue 1: Too much exercise

One of the core design features of react-beautiful-DND is the way cards naturally move out of other cards when being dragged. However, when you enter a new list, you can usually replace a large number of cards at once. If you move to the top of the list, you need to move everything in the entire list to make room. Offline CSS changes themselves are inexpensive. However, communicating with Draggables via render to tell them how to move out is expensive to process a large number of cards at once.

The virtual displacement

We now only move things that are partially visible to the user, not cards that are invisible to the user. So completely invisible cards don’t move. This greatly reduces the amount of work we need to do to get into the large list, since we only need the draggable cards that Render can see.

When detecting visible content, we need to consider the current browser viewport as well as the scroll container (the element with its own scroll bar). Once the user scrolls, we update the displacement based on what is now visible. Making sure this displacement looks correct as the user scrolls is a bit complicated. They shouldn’t know we didn’t move the invisible cards. Here are some rules we came up with to create an experience that looks right to the user.

  • If the card needs to move and be visible: Move the card and animate its movement
  • If a card needs to be moved but it is not visible: do not move it
  • If a card needs to be moved and visible, but the card before it needs to be moved but not visible: move it, but do not animate it.

So we’re only moving visible cards, so no matter how big the current list is, there’s no problem moving from a performance standpoint because we’re only moving cards that are visible to the user.

Why not use virtual lists?

A Virtualized list of 10,000 cards from React – Virtualized.

Avoiding off-screen work can be a daunting task, and the techniques you use will vary depending on your application. We want to avoid moving and animating mounted elements that are not visible during drag-and-drop interactions. This is quite different from avoiding rendering off-screen components completely using a virtualization solution like React – Virtualized. Virtualization is amazing, but adds complexity to the code base. It also breaks some native browser features like print and find (Command/Control + F). Our decision was to provide superior performance for React applications, even if they don’t use virtualized lists. This makes it very easy to add beautiful, high-performance drag-and-drop operations that can be dragged and dropped into existing applications with very little overhead. That said, we also plan to support supporting Virtualised Lists – so developers can choose whether they want to use virtualized lists to reduce render time for large lists. This is useful if you have a list of 1000 cards.

Issue 2: Renountable updates

We let the user know when they drag the Droppable list by updating the isDraggingOver property. However, doing so causes Droppable to render – which in turn causes all its children to render – possibly 100 Draggable cards!

We do not control the child elements of the component

To avoid this, we created a performance-optimized recommendation document for users of react-beautiful-dnd to avoid rendering children of Droppable that don’t need to be rendered. The library itself does not control rendering of Droppable’s child elements, so the best we can do is provide a suggested optimization. This suggestion allows the user to set the Droppable while dragging and dropping, while avoiding the render call on all of its children.

import React, { Component } from 'react';

class Student extends Component<{ student: Person }> {
  render() {// Render a draggable element}} class InnerList extends Component<{students: Person[]}> {// shouldComponentUpdate(nextProps: Props) {if(this.props.students === nextProps.students) {
      return false;
    }
    return true; } // You also can't do your own shouldComponentUpdate check, // can only inherit from react. PureComponentrender() {
    return this.props.students.map((student: Person) => (
      <Student student={student} />
    ))
  }
}

class Students extends Component {
  render() {
    return (
      <Droppable droppableId="list">
        {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
          <div
            ref={provided.innerRef}
            style={{ backgroundColor: provided.isDragging ? 'green' : 'lightblue' }}
          >
            <InnerList students={this.props.students} />
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    )
  }
}
Copy the code

The instant displacement

Smooth movement between large lists.

By implementing these optimizations, we were able to reduce the time to move between lists of 500 cards, which shifted from 380 ms to 8 ms per frame! That’s another 99 percent reduction.

Other: Searches for a table

This optimization is not specific to React – but is useful when dealing with ordered lists

In react-beautiful-DND we often use arrays to store ordered data. However, we also want to quickly look up this data to retrieve the item, or to see if the item exists. Usually you need to do an array.prototype.find or something similar to get items from the list. If this happens too often, it can be a disaster for large arrays.

There are many techniques and tools to solve this problem (including Normalizr). One common approach is to store the data in an Object map with an array of ids to maintain order. This is a great optimization if you need to review the values in the list on a regular basis, and it speeds things up.

We did a few different things. We created lazy Object maps with Memoize-One (a memory function that remembers only the latest parameters) for on-demand look-ups in real time. The idea is that you create a function that takes an Array argument and returns an Object map. If the same array is passed to the function multiple times, the previously calculated Object map is returned. If the array changes, the map is recalculated. This allows you to have an immediate lookup table without periodic recalculation or the need to store it explicitly in State.

const getIdMap = memoizeOne((array) => {
  return array.reduce((previous, current) => {
   previous[current.id] = array[current];
   return previous;
  }, {});
});

const foo = { id: 'foo' };
const bar = { id: 'bar'}; // Const ordered = [foo, bar]; Const map1 = getMap(ordered); map1['foo'] === foo; // true
map1['bar'] === bar; // true
map1['baz'] === undefined; // trueconst map2 = getMap(ordered); // Return the same mapping as before - no need to recalculate const map1 === map2;Copy the code

Using lookup tables greatly speeds up the drag action, and we check for the presence of a card in each connected Draggable component at each update (O(nĀ²) in the system). Using this approach, we can compute an Object map based on state changes and have the connected Draggable component do O(1) lookups using the shared map.

Final words ā¤ļø

I hope you find this blog useful and consider some optimizations you can apply to your own libraries and applications. Take a look at react-beautiful-DND, or try playing with our example.

Jared Crowe and Sean Curtis for optimization help, Daniel Kerris, Jared Crowe, Marcin Szczepanski, Jed Watson, Cameron Fletcher, James Kyle, Ali Chamas and other Atlassians put blogs together.

record

I gave a talk at React Sydney about the main points of this blog.

YouTube video link: here

Optimize React performance on React Sydney.

Thank you for Marcin Szczepanski.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.