TL; DR

This article includes:

  1. Problem with using ExpirationTime for priority
  2. Why was the bit-operated Lane model chosen to solve these problems
  3. Demo and analysis before and after using Lane model

Context (ExpirationTime problem)

ExpirationTime principle

Before using the Lane model, React internally used ExpirationTime for task priorities.

The pseudocode for calculating ExpirationTime for a task is as follows:

// Ignore the complicated details inside React.
const task;
task.expirationTime = MAX_INT_31 - (currentTime + delayTimeByTaskPriority);
Copy the code

MAX_INT_31 is the largest integer in the 31-bit binary representation. CurrentTime represents the currentTime in milliseconds. DelayTimeByTaskPriority is the delay corresponding to the current task’s priority. For example, the high priority task taskA and the low priority task taskB have a delay of 0 and 500, respectively. If their currentTime is the same, So taskA. ExpirationTime is 500 more than taskB. ExpirationTime.

The scheduler selects the largest ExpirationTime ExpirationTime as the currentExecTaskTime of a task and assigns it to React to enter the reconciliation phase. The scheduler selects the currentExecTaskTime of a task as the largest ExpirationTime of a task and assigns it to React to enter the reconciliation phase.

ExpirationTime and bulk updates

You can easily understand how React implements batch updates as long as the task meets task.expirationTime >= currentExecTaskTime.

You implement bulk updates in an event handler or lifecycle function by setting the task to the same ExpirationTime. Task.expirationtime >= currentExecTaskTime expirationTime = currentExecTaskTime

ExpirationTime and concurrent mode

Concurrent Mode can be implemented using ExpirationTime. For example, when the low-priority task taskA is in the mediation phase, the user’s input in will result in the high-priority task taskB, which updates the input to show what it just entered. TaskB. ExpirationTime is larger than taskA. ExpirationTime is higher than taskA. In this scenario, React implements the concurrent mode by making taskA and taskB execute simultaneously.

The presence of Suspense

Demo

I’ll show you a Demo to get a sense of the problems you would like to have when you use ExpirationTime. The Demo is available online, please click here.

In Demo, count and App: are incremented by 1 per second. The key code is as follows:

const Sub = ({ count }) = > {
  const [resource, setResource] = useState(undefined)
  Suspense fallback logic is triggered 3 seconds after clicking a button
  const [startTransition, isPending] = useTransition({ timeoutMs: 3000 })

  return (
    <div>
      <button
        onClick={()= >{// createResource(6000) returns a Promise that takes 6 seconds to Fulfill // Simulate a request that takes 6 seconds startTransition(() => { setResource(createResource(6000)) }) }} > CLICK ME</button>
      <pre>{JSON.stringify({ count, isPending }, null, 2)}</pre>
      {resource === undefined ? "Initial state" : resource.read()}
    </div>)}const App = props= > {
  const [s, setS] = useState(0)
  useEffect(() = > {
    const t = setInterval(() = > {
      setS(x= > x + 1)},1000)
    return () = > {
      clearInterval(t)
    }
  }, [])

  return (
    <>
      <Suspense fallback={<Loading />} ><Sub count={s} />
      </Suspense>
      <div>App: {s}</div>
    </>)}Copy the code

In Demo, CLICK the “CLICK ME” button when count is 2.

  1. IsPending changes from False to true immediately
  2. The count value andApp:It’s fixed at 2. It doesn’t change
  3. Suspense mount in 3 secondsfallbackAt this time,App:For 5
  4. Suspense mount in 3 secondschildrenIn this case, the value of count andApp:For eight

In step 2, although the timer updates the count value every second, the page does not display the new count value. This is a problem with ExpirationTime.

why

The root cause is that a high-priority I/O task blocks a low-priority CPU task.

IO tasks in React are tasks related to the Suspense mechanism; all other tasks are CPU tasks. A task is an IO task if it causes a Thenable object to be thrown by a Suspense subcomponent.

In this case, setResource(createResource(6000)) creates an IO task taskA, which, if executed, throws a ThEnable object when the Sub function component is called. Set (x => x + 1) ¶ setS(x => x + 1) of a TIMER will create a CPU task taskB with an ExpirationTime of less than taskA. ExpirationTime.

Since taskB has a lower priority, make sure taskA is executed before taskB. Under this rule, there are only two scheduling methods for taskA and taskB.

  1. TaskB is executed after taskA. TaskB is not executed because taskA cannot complete, and the result is: the page is stuck.
  2. TaskA and taskB execute together.

If taskA is executed with taskB, count in the Sub component will still be 2 because the Sub component will throw the Thenable object. But the value of App: will increase. This will result in a BUG on the page where App: is 3 but count is 2.

React executes with taskA and taskB, but until taskA completes, React does not enter the commit phase, so the count value in the Demo will not be updated. After all, performance doesn’t matter when it comes to bugs.

The DEFINITION I/O task is not complete

If timeout is set when an I/O task is triggered, the I/O task is defined as incomplete if the following two conditions are met.

  1. Within the timeout period
  2. Suspense subcomponents throw thenable objects

In Demo debugging, we found that after triggering taskB, React first tries to execute both taskA and taskB, i.e. taskA and taskB during the reconcile phase. Because the Sub component throws the Thenable object during execution, it determines whether the current time is within the timeout period before the commit phase. If it is within the timeout period, the execution result of this reconciliation phase will be ignored and the submission phase will not be entered. Otherwise, the submission phase will be carried out. Finally, the submission phase will determine whether to display children or fallback.

Expect the Demo

Let’s look at the desired effect in this scenario. The Demo uses the React Lane model and can be accessed online, please poke here.

In the figure above, CLICK the “CLICK ME” button when the count is 2.

  1. IsPending changes from False to true immediately
  2. The count value andApp:It increases by 1 per second
  3. Suspense mount in 3 secondsfallbackAt this time,App:For 5
  4. Suspense mount in 3 secondschildrenIn this case, the value of count andApp:For eight

To solve the problem

Try to fix it on ExpirationTime

As mentioned earlier, a “high-priority I/O task” would block a “low-priority CPU task”. In this scenario, the ExpirationTime mechanism cannot skip high-priority I/O tasks. As a result, low-priority CPU tasks (ExpirationTime increases the count value) are not executed. As a result, the PAGE is blocked.

The deeper reason the ExpirationTime mechanism causes this problem is that it combines task priority with bulk updates. If the priority (currentExecTaskTime) is decided, all task-. expirationTime >= currentExecTaskTime tasks will be executed.

You need to support low-priority tasks first and high-priority tasks second. ExpirationTime is a ExpirationTime context with two methods:

  1. Use Set to maintain ExpirationTime’s collection if the tasktask.expirationTimeIn that collection, the task should be performed. However, this method is not feasible because it consumes time and memory.
  2. Use intervals to represent tasks that need to be executed if the task satisfieslowExpirationTime <= task.expirationTime <= highExpirationTime, should perform the task. This method cannot express “Only ExpirationTime tasks are 1 and 3 and not ExpirationTime tasks are 2”. Therefore, this method cannot be used.

Using the Lane model

React finally chooses to express task priority using Lane, which is represented by binary bits. SyncLane has the highest priority of 1, followed by 2, 4, 8, and so on. All Lane definitions can be found in the source code.

Batch update through Lanes. Lanes is an integer. If Lanes is an integer, all tasks with priorities corresponding to the binary bit 1 are executed. For example, if Lanes are 17, SyncLane (1) and DefaultLane (16) tasks are updated in batches.

Demo in Lane mode

As mentioned earlier, the Demo ran as expected in Lane mode. The Demo is available online, please click here.

CLICK ME and run setResource(createResource(6000)) to generate a task with a priority of 8192, which corresponds to TransitionShortLanes. Setting ((x) => x + 1) generates a task with a priority of 512, which corresponds to DefaultLanes.

Each time the timer is triggered, the task with the priority of 512 takes precedence, causing the count value and App: to increase by 1. The task with priority 8192 is then executed, but because it is an unfinished I/O task, it does not enter the commit phase.

In fact, with the Lane model, the PRIORITY of THE IO task is always lower than the priority of the CPU task (increasing the count value), so the mechanism does not solve the problem by executing the low-priority task first, then the high-priority task.

You can subtract the I/O ExpirationTime of an I/O context task every time a new CPU task is generated. Context However, this method is not feasible because of its high time complexity and easy to lead to numerical overflow.

Disadvantages of using Lane

1. Hunger tasks

Using Lane instead of ExpirationTime can cause starvation. React currently implements a timeout mechanism for tasks in the scheduler.

Refer to the “Stuff I Moderation omitted from this Initial PR” section in the Initial Lanes Implementation.

Poor readability

It is very difficult to debug from Lanes to the corresponding priority 😭.

Reference documentation

  1. Initial Lanes implementation
  2. Some questions about lanes

, recruiting

The writer is in Chengdu – Bytedance – private cloud direction, the main technology stack is React + Node.js. Team expansion speed is fast, the technical atmosphere within the group is active. Public cloud private cloud is just starting, there are many technical challenges, the future is foreseeable.

Interested can resume through this link: job.toutiao.com/s/e69g1rQ

You can also add my wechat moonball_cxy, chat together, make a friend.

Original is not easy, don’t forget to encourage oh ❤️