• One AND, one negation: how React picks what to render next

    A scheduler’s whole job boils down to a single question, that it has to answer over and over: Of all the waiting tasks, which is the most urgent? One would expect that question to cost something: a loop, maybe scanning the candidates and keeping the best one so far or maybe a priority queue and its heap bookkeeping?

    React though, answers it on one expression. No loop, no comparison and no branch.

    export function getHighestPriorityLane(lanes: Lanes): Lane {  return lanes & -lanes;}

    That is it. The entire, tiny function with the responsibility to perform a bitwise AND of a number with its own negation. I know what you’re thinking.. it looks like the kind of a thing that should either be a no-op or some nonsense, however it is neither. It is the single most urgent piece of pending working in the application.

    The question that used to have a different shape

    Before any of this, React did roughly what your intuition expects. the data it worked with was shaped to make the intuitive answer correct. React 17 introduced a mechanism called Lanes to track the priority of the pending tasks. Before its introduction, React 16 (16.13.1) handled priorities with a single number called as the ExpirationTime and it handled priorities through magnitude-based ordering. Meaning, bigger meant more urgent, with synchronous work pinned all the way at the top as Sync = MAX_SIGNED_31_BIT_INT (#ref)

    Each root (React’s internal FiberRoot object) carried a firstPendingTime field that simply held the highest-priority expiration time waiting on it. That field was kept current with a plain comparison every time new work arrived:

    const firstPendingTime = root.firstPendingTime;if (expirationTime > firstPendingTime) {  root.firstPendingTime = expirationTime;}

    Finding the most urgent pending work was nothing more than a field read and required no computation at all, because the maximum had already been maintained on the way in. Notice how the maintenance works: every single time a component scheduled an update, that update’s own expirationTime was compared against the root’s current firstPendingTime. If it was more urgent (a larger number), then it would overwrite the field. The running maximum was recomputed incrementally at write time, one comparison per update, so when the question finally came to pick the highest priority task, the field already held the answer and nothing had to be searched.

    When the work loop wanted to know what to pick next, getNextRootExpirationTimeToWorkOn() mostly just returned root.firstPendingTime. This implementation worked, but it had a silent limitation baked into it. A single number can only rank work along one line. To see why that would be a problem, imagine a search input that filters a huge list as you type. A single keystroke sets off two different updates: One is the letter showing up in the input, which has to happen this instant, and the other is the huge list re-filtering itself, which can lag a moment behind, and must never be allowed to freeze your typing. The point is that neither of the two updates are more or less urgent than the other one on the shared scale, but at the same time they are both urgent in different ways. One says “do this right now”, and the other says “do this soon, but never at the cost of typing”. A single number cannot hold that distinction. It drops both on one line and demands that you say which is the larger one and such a comparison has no honest answer. The real difference between the two is simply lost.

    Lanes were introduced to break this collapse, and breaking it is exactly what reshapes the question.

    Priority as a set instead of a magnitude

    Starting React 16.14.0 (and the React 17 line), the priority of the pending work moved from a number on a scale, to a set of independent bits. A Lane is a single bit, Lanes is a bitmask of them, and they are laid out in a deliberate order. The most urgent lanes are represented by the lowest bit. The constants below are some of the Lane definitions in React 19 and above.

    export const SyncHydrationLane: Lane     = 0b0000000000000000000000000000001;export const SyncLane: Lane              = 0b0000000000000000000000000000010;export const InputContinuousLane: Lane   = 0b0000000000000000000000000001000;export const DefaultLane: Lane           = 0b0000000000000000000000000100000;// ...hydration, gesture, transition and other lanes in between...export const IdleLane: Lane              = 0b0010000000000000000000000000000;export const OffscreenLane: Lane         = 0b0100000000000000000000000000000;export const DeferredLane: Lane          = 0b1000000000000000000000000000000;

    Lower bit positions have higher priority by design. So, a root’s pendingLanes is no longer a single magnitude but a set of independent “urgencies”, where any subset can be lit at once. You can have synchronous work and an idle update, and three transitions all pending simultaneously, each represented by its own bit.

    But this move comes at a cost, and the cost is the question we started with. Once “what is pending” is a set rather than a maintained maximum, “what is most urgent” is no longer a field you can read. Recall that we learnt that in React < 16.14.0, the only effort required was reading the field, but with Lanes, React has to actually compute the minimum set bit of the mask, on demand, and it has to do it constantly, because the scheduler asks on every pass. So the move from a scalar to a set is what creates the need for a fast way to pull the most important member out of that set.

    Let’s understand what -lanes is

    Here is the part worth slowing down on, because the cleverness is invisible until you explore the bits. In JavaScript, the bitwise operators convert their operands to 32-bit signed integers, and negative integers are stored in two’s complement, which is: to negate a number you flip every bit and add one. That “add one” is the entire trick.

    A quick refresher on bitwise math

    Readers with a background in Computer Science may already be familiar with this. You can skip this part if you’ already familiar. If not then read this first because the rest of the article depends on it.

    A bitwise operator looks at each bit on its own, one column at a time, instead of using the number as a whole. The operator we need here is bitwise AND written as &. It compares two numbers bit by bit and puts a 1 in the resulting cell, only when both numbers have a 1 in that position:

      0b110   (6)& 0b101   (5)= 0b100   (4)

    Bits are commonly used like this when you want to store many true/false answers inside one number. A common example is file permissions on UNIX-based operating systems. The 7 in chmod 755 is 0b111: three separate switches (read, write, and run) stored in one number. To check if a file has write permissions, the system does one AND with the write bit.

    React lanes work the same way. One number holds a set of independent on/off facts, and you read them with &.

    Two’s complement is the normal way computers store negative whole numbers. The rule is simple and always the same: to get -n, take n, flip every bit (every 0 becomes 1, every 1 becomes 0), and then add 1. Computers use this method because it lets them subtract by adding, so the same hardware can do both jobs. This “flip and add 1” step is the whole reason lanes & -lanes works, and the next paragraph shows it step by step.

    Returning to Lanes, let’s say a lane bitmask is set as:

      0b0000000000000000000000000011010 <--- original (lanes)  0b1111111111111111111111111100101 <--- 1's complement+                                 1 <--- adding 1  ---------------------------------  0b1111111111111111111111111100110 <--- 2's complement (-lanes)

    To find the most urgent task, it does lanes & -lanes, so:

      0b0000000000000000000000000011010& 0b1111111111111111111111111100110  ---------------------------------  0b00000000000000000000000000000010 <--- lane with bit 1 is the most urgent.

    That’s the single, winning bit right there!

    The companion trick, and why it is a different trick

    Isolating the urgent bit is half the story. Next, lanes also need to be turned into array indices. React does a lot of per-lane bookkeeping, such as recording expiration timestamps (not the same one used by React < 16.14.0), entanglements, the maps of which updates and transitions belong to which lane, and it stores all of it in plain arrays, which is called LaneMaps.

    export function createLaneMap<T>(initial: T): LaneMap<T> {  // Intentionally pushing one by one.  // https://v8.dev/blog/elements-kinds#avoid-creating-holes  const laneMap = [];  for (let i = 0; i < TotalLanes; i++) {    laneMap.push(initial);  }  return laneMap;}

    The comment in the code above is a small performance hint. The array is filled one push at a time so that V8 keeps it as a fast packed array instead of a holey one. As per V8’s vocabulary, a “holey” array is the one with gaps in it and V8 doesn’t like holey arrays, as it slows down the performance.

    To index into these arrays, a single-bit lane has to become its ordinal position (its place in line counting from the lowest bit: 0 for the bottom bit, 1 for the next one and so on..).

    function pickArbitraryLaneIndex(lanes: Lanes) {  return 31 - clz32(lanes);}

    clz32 counts the leading zeros in the 32-bit representation. A lone bit at position i has value 2^i and therefore 31 - i zeros above it, so clz32 returns 31 - i and 31 - clz32 hands you back i, the index.

    There is quiet a asymmetry between the two helpers that demands a second look. getHighestPriorityLane returns the lowest set bit with lanes & -lanes, while pickArbitraryLaneIndex returns the highest set bit with 31 - clz32.  The names tell you why this difference is allowed: the first one has to be the most urgent lane, so which bit it picks matters a lot, whereas the index helper is used where the order genuinely does not matter.

    while (lanes > 0) {  const index = pickArbitraryLaneIndex(lanes);  const lane = 1 << index;  // ...read or mutate laneMap[index]...  lanes &= ~lane;}

    What actually changed

    The two versions arrive at the most urgent work differently. In version 16.13.1 and lower, the record of the most urgent task was maintained as the task came in, each update kept the firstPendingTime current, and selection simply read that field. From 16.14.0 onwards, the same is derived on the spot from the pendingLanes with lanes & -lanes. Where they genuinely differ is in what each representation can express.