• Scheduling in React
  • Original article by Philipp Spiess
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Xuyuey
  • Proofreader: Portandbridge, Reaper622

In modern applications, user interfaces typically handle multiple tasks simultaneously. For example, a search component might need to auto-complete results in response to user input, and an interactive dashboard might need to update charts while loading data from the server and sending analysis data to the back end.

All of these parallel steps can lead to a slow or unresponsive interface, which can lead to low user satisfaction, so let’s learn how to solve this problem.

Scheduling in the user interface

Our users expect immediate feedback. Whether it’s clicking a button to open a modal box or adding text to an input box, they don’t want to wait until they see some kind of confirmation status. For example, clicking a button instantly opens the modal box, and the input box instantly displays the keyword you just entered.

To imagine what happens under parallel operation, let’s take a look at the application demonstrated by Dan Abramov in his talk on Beyond React 16 at JSConf Iceland 2018.

Here’s how the app works: The more you type in the input box, the more detail there will be in the chart below. Because the two updates (the input field and the chart) are happening at the same time, the browser has to do so much computation that it will discard some of the frames. This leads to significant latency and a poor user experience.

Video address

However, the version of the input box on the user interface that updates first when there is new typing seems to run faster for the user. Because users receive immediate feedback, even though they require the same amount of computation time.

Video address

Unfortunately, the current user interface architecture makes achieving this priority very important, and one way to address this is to update the charts through debouncing. The problem with this approach is that the graph is still rendered synchronously while the shake-off callback is executed, which again causes the user interface to be unresponsive for a period of time. We can do better!

Browser event loop

Before we learn how to properly implement update priorities, let’s dig deeper and understand why browsers have problems handling user interactions.

JavaScript code executes in a single thread, which means that only one line of JavaScript code can run at any given time. This thread is also responsible for handling the life cycle of other documents, such as layout and drawing. 1 means that every time JavaScript code runs, the browser stops doing anything else.

To ensure that the user interface is responsive, we only have a short window of time before we can receive the next input. At the Chrome Dev Summit 2018, Shubhie Panicker and Jason Miller presented presentations on the theme of A Quest to Guarantee Responsiveness. In the presentation, they gave a visual description of the browser event loop, and we can see that we only have 16ms (on a typical 60Hz screen) before drawing the next frame, and then the browser needs to process the next event:

Most JavaScript frameworks (including the current version of React) will be updated synchronously. We can think of this behavior as a function render(), which only returns after a DOM update. During this time, the main thread is blocked.

Problems with the current solution

Armed with this information, we can outline two issues that must be addressed in order to achieve a more responsive user interface:

  1. Long running tasks cause frame loss. We need to make sure that all tasks are small and can be completed in milliseconds so that they can be run in one frame.

  2. Different tasks have different priorities. In the sample application above, we saw that prioritizing user input leads to a better overall experience. To do this, we need a way to define the order of priorities and schedule tasks according to that order.

Concurrent React and scheduler

⚠️ Warning: The API below is unstable and subject to change. I will keep as updated as possible.

In order to implement a well-scheduled user interface with React, we must take a look at the following two upcoming React features:

  • Concurrent React, also known as Time Slicing. With the help of the new Fiber architecture rewritten by React 16, React now allows the rendering process to be completed in segments, returning 2 to the main thread to perform other tasks.

    We’ll hear more about concurrency React later. Now it’s important to understand that when this mode is enabled React will split the React component that is rendered synchronously into smaller pieces and run on multiple frames.

    ➡️ Using this pattern, in the future we will be able to break tasks that require long renderings into smaller chunks.

  • The scheduler. It is a generic collaborative main thread scheduler developed by the React Core team that registers callback functions with no priority in the browser.

    Currently, there are several priorities:

    • ImmediateTasks that need to be executed synchronously.
    • UserBlockingUser-blocking priority (expired after 250 ms), tasks that need to be run as a result of user interaction (for example, button clicks).
    • NormalNormal priority (expired after 5 s), does not have to make the user feel the update immediately.
    • LowLow priority (expired after 10 s), tasks that can be deferred but still need to be completed eventually (for example, analysis notifications).
    • IdleIdle priority (never expired), tasks that do not need to be run (for example, hide content outside the interface).

    Each priority has a corresponding expiration time, which is necessary to ensure that lower-priority tasks can run even when there are so many higher-priority tasks that they can run continuously. In scheduling algorithms, this problem is called starvation. Expiration time ensures that each scheduled task can be executed eventually. For example, even if we have animations running in our application, we don’t miss any analysis notifications.

    In the engine, the scheduler sorts all registered callback functions by expiration time (the time the callback function was registered plus the expiration time of the priority) and stores them in a list. The scheduler then registers itself in the callback function after the browser draws the next frame. 3 In this callback function, the browser executes as many registered callback functions as it can until the browsing begins to draw the next frame.

    ➡️ With this feature, we can schedule tasks with different priorities.

Scheduling in methods

Let’s take a look at how you can use these features to make your application more responsive. To do that, let’s start with ScheduleTron 3000, an application I built myself that allows users to highlight search terms in a list of names. Let’s take a look at its initial implementation:

// The application contains a search box and a list of names. The display of the list is controlled by the searchValue state variable. // This variable is updated by the search box.function App() {
  const [searchValue, setSearchValue] = React.useState();

  function handleChange(value) {
    setSearchValue(value);
  }

  return( <div> <SearchBox onChange={handleChange} /> <NameList searchValue={searchValue} /> </div> ); } // The search box renders a native HTML input element and controls it with the inputValue variable. // When a new key is pressed, it first updates the local inputValue variable, then it updates the App component's searchValue variable, and then simulates an analysis notification sent to the server.function SearchBox(props) {
  const [inputValue, setInputValue] = React.useState();

  function handleChange(event) {
    const value = event.target.value;

    setInputValue(value);
    props.onChange(value);
    sendAnalyticsNotification(value);
  };

  return (
    <input
      type="text"
      value={inputValue}
      onChange={handleChange}
    />
  );
}

ReactDOM.render(<App />, container);
Copy the code

ℹ️ is used in this exampleReact Hooks. If you’re not familiar with this new feature, check it outCodeSandbox code. Also, you may be wondering why we used two different state variables in this example. Let’s find out why.

Give it a try! Enter a name in the search box below (for example, “Ada Stewart”) and see how it works:

Check it out in CodeSandbox

You may notice that the interface is not that responsive. To magnify this problem, I have deliberately lengthened the rendering time of the list. Because this list is large, it can have a significant impact on application performance.

Our users expect immediate feedback, but the application is unresponsive for a long time after the button is pressed. To see what’s going on, take a look at the Developer tools’ Performance TAB. Here’s a screenshot I took when I typed the name “Ada” into the input field:

We can see a lot of red triangles, which is usually not a good signal. For each type, we see a KeyPress event being fired. All events are triggered in one frame, 5 causing the duration of the frame to be extended to 733 ms. This is much higher than our average frame budget of 16 ms.

In this Keypress event, our React code is called, the inputValue and searchValue are updated, and an analysis notification is sent. In turn, the updated status values cause the application to re-render each name entry. The task is onerous but must be completed, and if you use the native method, it blocks the main process.

The first step to improving the current state is to use an unstable concurrency mode. Unstable_ConcurrentMode > wrap part of our React tree like this:

- ReactDOM.render(<App />, container);
+ ReactDOM.render(
+  <React.unstable_ConcurrentMode>
+    <App />
+  </React.unstable_ConcurrentMode>,
+  rootElement
+ );
Copy the code

However, in this case, just using concurrent mode doesn’t change our experience. React still receives two status updates at the same time, and there is no way to know which is more important.

We want to set the inputValue first, then update the searchValue and send analysis notifications, so we only need to update the input box at the beginning. To do this, we use the scheduler exposed API (which can be installed using the NPM I Scheduler) to sort low-priority callback functions:

import { unstable_next } from "scheduler";
function SearchBox(props) {
  const [inputValue, setInputValue] = React.useState();

  function handleChange(event) {
    const value = event.target.value;

    setInputValue(value);
    unstable_next(function() {      
      props.onChange(value);      
      sendAnalyticsNotification(value);    
    });  
  }
  
  return <input type="text" value={inputValue} onChange={handleChange} />;
}
Copy the code

In the API we used, unstable_next(), all React updates are set to Normal priority, which is lower than the default priority inside the onChange listener.

In fact, with this change, our input fields are already much more responsive, and no more frames are discarded when we type. Let’s look at the Performance TAB again:

We can see that long running tasks are now broken down into smaller tasks that can be completed in a single frame. The red triangle that tells us we have frames dropped has also disappeared.

However, the analysis notification (highlighted in the screenshot above) is still not ideal, and it is still executed at the same time as rendering. Because our users won’t see this task, we can give it a lower priority.

import {
  unstable_LowPriority,
  unstable_runWithPriority,
  unstable_scheduleCallback
} from "scheduler";

function sendDeferredAnalyticsNotification(value) {
  unstable_runWithPriority(unstable_LowPriority, function() {
    unstable_scheduleCallback(function() {
      sendAnalyticsNotification(value);
    });
  });
}
Copy the code

If we now used in the search box component sendDeferredAnalyticsNotification (), and then look again at the Performance TAB, and drag to the end, we can see that the work was done, the render analysis notification to be sent, All tasks in the program are perfectly scheduled:

Give it a try:

Check it out in CodeSandbox

The limitations of the scheduler

Using the scheduler, we can control the order in which callback functions are executed. It is built into the latest React implementation and can be used in conjunction with concurrent mode without separate Settings.

That said, the scheduler has two limitations:

  1. Resource robbery. The scheduler tries to use all available resources. This can cause problems if multiple instances of the scheduler are running on the same thread and competing for resources. We need to ensure that all parts of the application are using the same scheduler instance.
  2. Balance user-defined tasks with browser work. Because the scheduler runs in the browser, it can only access the API exposed by the browser. Document life cycles, such as rendering or garbage collection, can interfere with work in uncontrollable ways.

To address these limitations, the Google Chrome team is working with React, Polymer, Ember, Google Maps, and the Web Standards Community to create the Scheduling API in the browser. Isn’t that exciting?

conclusion

The concurrent React and scheduler allow us to implement task scheduling in our applications, which will allow us to create responsive user interfaces.

React officials are likely to release these features in the second quarter of 2019. Until then, you can use these flaky apis, but keep an eye out for changes.

If you want to be the first to know when these apis change or to document new features, please subscribe to This Week in React ⚛️.


1. MDN Web Docs has a great article on this issue. ↩

2. This is an awesome word that returns a method that supports paused execution. You can see similar concepts on Generator functions. ↩

3. In the current implementation of the scheduler, it is implemented using postMessage() in a requestAnimationFrame() callback. It is called immediately after the frame finishes rendering. ↩

4. This is another way to implement concurrent mode, using the new createRoot() API. ↩

5. When processing the first KeyPress event, the browser looks up the event to be processed in its queue and decides which event listener to run before rendering the frame. ↩

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


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.