react — Engineering Performance
25 engineers all time · Jan 2025 – Sep 2026 · built 2026-09-30 · GitHub
Performance snapshot
Today's rolling 90-day reading for react, compared with the start of the series. Pick a window to move that comparison point.
Avg. perf / dev / mo
−35.0%
1.05 → 0.69 ETV
Active engineers
−26.7%
15.0 → 11.0
Features
−16.8pp
35.1% → 18.3%
vs. Meta
0.35x
1.5x → 0.35x · −65% below
react vs. Meta
Per-engineer ETV for react against Meta as a whole. Both lines are 90-day rolling averages scaled to a 30-day month, so they share one axis and can be read against each other at any point. Pick a window to zoom the chart to it.
Performance Composition
Each month's output split by type of work: Features (new value), Maintenance (sustaining systems), Tests, Docs, and Fixes (rework). The yellow line is output per engineer, so when it rises each engineer is delivering more, whatever the team size did. Unit: Engineering Throughput Value (ETV).
Engineering capacity
Effective engineers behind react, against its pre-AI baseline. Each subject has its own: react's is 1.05 ETV / dev / mo, its first reading in Q1 2025. Per-engineer ETV divided by that gives a capacity multiple, and that multiple applied to the engineers active in the trailing 90 days turns it into engineer-equivalents. The line is the real headcount, so the gap between line and area is what the leverage is worth. Because each baseline is its own, every subject opens at 1.0x on its first day: multiples measure improvement and are not comparable between subjects.
Knowledge concentration
How dependent is this repo on a small number of engineers? Higher top-1 share = higher key-person risk.
Sebastian Markbåge owns 28.3 % of commits.
Behind the numbers
Written summary of the work completed each month.
No monthly reports available yet.
Top engineers
Most impactful commits
Top 10 by ETV in the all-time window.
- 4.7ETVImplement Partial Hydration for Activity (#32863) Stacked on #32862 and #32842. This means that Activity boundaries now act as boundaries which can have their effects mounted independently. Just like Suspense boundaries, we hydrate the outer content first and then start hydrating the content in an Offscreen lane. Flowing props or interacting with the content increases the priority just like Suspense boundaries. This skips emitting even the comments for `<Activity mode="hidden">` so we don't hydrate those. Instead those are deferred to a later client render. The implementation are just forked copies of the SuspenseComponent branches and then carefully going through each line and tweaking it. The main interesting bit is that, unlike Suspense, Activity boundaries don't have fallbacks so all those branches where you might commit a suspended tree disappears. Instead, if something suspends while hydration, we can just leave the dehydrated content in place. However, if something does suspend during client rendering then it should bubble up to the parent. Therefore, we have to be careful to only pushSuspenseHandler when hydrating. That's really the main difference. This just uses the existing basic Activity tests but I've started work on port all of the applicable Suspense tests in SelectiveHydration-test and PartialHydration-test to Activity versions.Sebastian Markbåge · 3ef31d19 · 2025-04-23
- 2.9ETV[Flight] Add more DoS mitigations to Flight Reply, and harden Flight (#35632) This fixes security vulnerabilities in Server Functions. --------- Co-authored-by: Sebastian Markbåge <sebastian@calyptus.eu> Co-authored-by: Josh Story <josh.c.story@gmail.com> Co-authored-by: Janka Uryga <lolzatu2@gmail.com> Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>Hendrik Liebau · 10680271 · 2026-01-26
- 2.8ETV[react-devtools-facade] 2/ implement component tree tools (#36597) Adds the component-tree building blocks and the `createTools(facade)` assembler — the first tools layered on top of the `installFacade` hook from commit 1. ### `createTools(facade): Tools` Reads the facade's tracked state (fiber roots + per-renderer internals) and returns a plain `Tools` object — no globals; the integrator decides what to do with it. Tools return **typed, plain JavaScript values** (or `{error}`); serialization (to an integration package's wire format) is left to the caller. ### Tools - **`getComponentTree(depth?, rootUid?)`** — the component tree as a flat array of `{uid, type, name, key, firstChild, nextSibling}` nodes (an adjacency list referencing other nodes by label). - **`getComponentByUid(uid)`** — one component's `{type, name, key?, props?, hooks?}`. For function components, `hooks` is the inspected hooks tree (nested `subHooks`), obtained via `react-debug-tools'` `inspectHooksOfFiberWithoutDefaultDispatcher` with the renderer's injected dispatcher (normalized by `getDispatcherRef`) — so hooks introspection never falls back to, or bundles, React's shared internals. - **`findComponents(name, rootUid?, page?, pageSize?)`** — paginated, case-insensitive name search. - **`getComponentSource(uid)`** — the component's definition location `{name, fileName, line, column}` (or `null`). - **`getOwnersStack(uid)`** — the raw JSX owner-stack string (DEV only). - **`getOwnersBranch(uid)`** — the structured owner chain `[{uid, name, type}]`, ordered immediate owner → root (DEV only). ### UIDs Components are addressed by stable `rN` uids, assigned lazily and memoized per fiber (and its alternate), so a component keeps the same uid across re-renders and across every tool. These uids don't survive page reloads.Ruslan Lesiutin · fb2cfa0f · 2026-06-18
- 2.4ETV[Fizz] Support Suspense boundaries anywhere (#32069) Suspense is meant to be composable but there has been a lonstanding limitation with using Suspense above the `<body>` tag of an HTML document due to peculiarities of how HTML is parsed. For instance if you used Suspense to render an entire HTML document and had a fallback that might flush an alternate Document the comment nodes which describe this boundary scope won't be where they need to be in the DOM for client React to properly hydrate them. This is somewhat a problem of our own making in that we have a concept of a Preamble and we leave the closing body and html tags behind until streaming has completed which produces a valid HTML document that also matches the DOM structure that would be parsed from it. However Preambles as a concept are too important to features like Float to imagine moving away from this model and so we can either choose to just accept that you cannot use Suspense anywhere except inside the `<body>` or we can build special support for Suspense into react-dom that has a coherent semantic with how HTML documents are written and parsed. This change implements Suspense support for react-dom/server by correctly serializing boundaries during rendering, prerendering, and resumgin on the server. It does not yet support Suspense everywhere on the client but this will arrive in a subsequent change. In practice Suspense cannot be used above the `<body>` tag today so this is not a breaking change since no programs in the wild could be using this feature anyway. React's streaming rendering of HTML doesn't lend itself to replacing the contents of the documentElement, head, or body of a Document. These are already special cased in fiber as HostSingletons and similarly for Fizz the values we render for these tags must never be updated by the Fizz runtime once written. To accomplish these we redefine the Preamble as the tags that represent these three singletons plus the contents of the document.head. If you use Suspense above any part of the Preamble then nothing will be written to the destination until the boundary is no longer pending. If the boundary completes then the preamble from within that boudnary will be output. If the boundary postpones or errors then the preamble from the fallback will be used instead. Additionally, by default anything that is not part of the preamble is implicitly in body scope. This leads to the somewhat counterintuitive consequence that the comment nodes we use to mark the borders of a Suspense boundary in Fizz can appear INSIDE the preamble that was rendered within it. ```typescript render(( <Suspense> <html lang="en"> <body> <div>hello world</div> </body> </html> </Suspense> )) ``` will produce an HTML document like this ```html <!DOCTYPE html> <html lang="en"> <head></head> <body> <!--$--> <-- this is the comment Node representing the outermost Suspense <div>hello world</div> <$--/$--> </body> </html> ``` Later when I update Fiber to support Suspense anywhere hydration will similarly start implicitly in the document body when the root is part of the preamble (the document or one of it's singletons).Josh Story · b25bcd46 · 2025-01-17
- 2.1ETVPatch FlightReplyServer with fixes from ReactFlightClient (#35277) FlightReplyServer are for client->server and ReactFlightClient is for server->client. They're not 100% symmetrical. We did a number of refactors to ReactFlightClient in PRs like #29823 and #33664 to change the structure of the resolution. This PR brings those changes to synchronize the two approaches. Which addresses deep resolution of cycles and deferred error handling. This also fixes a critical security vulnerability.Sebastian Markbåge · 7dc903cd · 2025-12-03
- 2.0ETV[assert helpers] react-dom (pt3) (#31983) moar assert helpers this finishes all of react-dom except the server integration tests which are tricky to convertRicky · 03e4ec2d · 2025-01-05
- 2.0ETVAdd <ViewTransition> Component (#31975) This will provide the opt-in for using [View Transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) in React. View Transitions only trigger for async updates like `startTransition`, `useDeferredValue`, Actions or `<Suspense>` revealing from fallback to content. Synchronous updates provide an opt-out but also guarantee that they commit immediately which View Transitions can't. There's no need to opt-in to View Transitions at the "cause" side like event handlers or actions. They don't know what UI will change and whether that has an animated transition described. Conceptually the `<ViewTransition>` component is like a DOM fragment that transitions its children in its own isolate/snapshot. The API works by wrapping a DOM node or inner component: ```js import {ViewTransition} from 'react'; <ViewTransition><Component /></ViewTransition> ``` The default is `name="auto"` which will automatically assign a `view-transition-name` to the inner DOM node. That way you can add a View Transition to a Component without controlling its DOM nodes styling otherwise. A difference between this and the browser's built-in `view-transition-name: auto` is that switching the DOM nodes within the `<ViewTransition>` component preserves the same name so this example cross-fades between the DOM nodes instead of causing an exit and enter: ```js <ViewTransition>{condition ? <ComponentA /> : <ComponentB />}</ViewTransition> ``` This becomes especially useful with `<Suspense>` as this example cross-fades between Skeleton and Content: ```js <ViewTransition> <Suspense fallback={<Skeleton />}> <Content /> </Suspense> </ViewTransition> ``` Where as this example triggers an exit of the Skeleton and an enter of the Content: ```js <Suspense fallback={<ViewTransition><Skeleton /></ViewTransition>}> <ViewTransition><Content /></ViewTransition> </Suspense> ``` Managing instances and keys becomes extra important. You can also specify an explicit `name` property for example for animating the same conceptual item from one page onto another. However, best practices is to property namespace these since they can easily collide. It's also useful to add an `id` to it if available. ```js <ViewTransition name="my-shared-view"> ``` The model in general is the same as plain `view-transition-name` except React manages a set of heuristics for when to apply it. A problem with the naive View Transitions model is that it overly opts in every boundary that *might* transition into transitioning. This is leads to unfortunate effects like things floating around when unrelated updates happen. This leads the whole document to animate which means that nothing is clickable in the meantime. It makes it not useful for smaller and more local transitions. Best practice is to add `view-transition-name` only right before you're about to need to animate the thing. This is tricky to manage globally on complex apps and is not compositional. Instead we let React manage when a `<ViewTransition>` "activates" and add/remove the `view-transition-name`. This is also when React calls `startViewTransition` behind the scenes while it mutates the DOM. I've come up with a number of heuristics that I think will make a lot easier to coordinate this. The principle is that only if something that updates that particular boundary do we activate it. I hope that one day maybe browsers will have something like these built-in and we can remove our implementation. A `<ViewTransition>` only activates if: - If a mounted Component renders a `<ViewTransition>` within it outside the first DOM node, and it is within the viewport, then that ViewTransition activates as an "enter" animation. This avoids inner "enter" animations trigger when the parent mounts. - If an unmounted Component had a `<ViewTransition>` within it outside the first DOM node, and it was within the viewport, then that ViewTransition activates as an "exit" animation. This avoids inner "exit" animations triggering when the parent unmounts. - If an explicitly named `<ViewTransition name="...">` is deep within an unmounted tree and one with the same name appears in a mounted tree at the same time, then both are activated as a pair, but only if they're both in the viewport. This avoids these triggering "enter" or "exit" animations when going between parents that don't have a pair. - If an already mounted `<ViewTransition>` is visible and a DOM mutation, that might affect how it's painted, happens within its children but outside any nested `<ViewTransition>`. This allows it to "cross-fade" between its updates. - If an already mounted `<ViewTransition>` resizes or moves as the result of direct DOM nodes siblings changing or moving around. This allows insertion, deletion and reorders into a list to animate all children. It is only within one DOM node though, to avoid unrelated changes in the parent to trigger this. If an item is outside the viewport before and after, then it's skipped to avoid things flying across the screen. - If a `<ViewTransition>` boundary changes size, due to a DOM mutation within it, then the parent activates (or the root document if there are no more parents). This ensures that the container can cross-fade to avoid abrupt relayout. This can be avoided by using absolutely positioned children. When this can avoid bubbling to the root document, whatever is not animating is still responsive to clicks during the transition. Conceptually each DOM node has its own default that activates the parent `<ViewTransition>` or no transition if the parent is the root. That means that if you add a DOM node like `<div><ViewTransition><Component /></ViewTransition></div>` this won't trigger an "enter" animation since it was the div that was added, not the ViewTransition. Instead, it might cause a cross-fade of the parent ViewTransition or no transition if it had no parent. This ensures that only explicit boundaries perform coarse animations instead of every single node which is really the benefit of the View Transitions model. This ends up working out well for simple cases like switching between two pages immediately while transitioning one floating item that appears on both pages. Because only the floating item transitions by default. Note that it's possible to add manual `view-transition-name` with CSS or `style={{ viewTransitionName: 'auto' }}` that always transitions as long as something else has a `<ViewTransition>` that activates. For example a `<ViewTransition>` can wrap a whole page for a cross-fade but inside of it an explicit name can be added to something to ensure it animates as a move when something relates else changes its layout. Instead of just cross-fading it along with the Page which would be the default. There's more PRs coming with some optimizations, fixes and expanded APIs. This first PR explores the above core heuristic. --------- Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>Sebastian Markbåge · a4d122f2 · 2025-01-08
- 1.8ETV[compiler] Aggregate error reporting, separate eslint rules (#34176) NOTE: this is a merged version of @mofeiZ's original PR along with my edits per offline discussion. The description is updated to reflect the latest approach. The key problem we're trying to solve with this PR is to allow developers more control over the compiler's various validations. The idea is to have a number of rules targeting a specific category of issues, such as enforcing immutability of props/state/etc or disallowing access to refs during render. We don't want to have to run the compiler again for every single rule, though, so @mofeiZ added an LRU cache that caches the full compilation output of N most recent files. The first rule to run on a given file will cause it to get cached, and then subsequent rules can pull from the cache, with each rule filtering down to its specific category of errors. For the categories, I went through and assigned a category roughly 1:1 to existing validations, and then used my judgement on some places that felt distinct enough to warrant a separate error. Every error in the compiler now has to supply both a severity (for legacy reasons) and a category (for ESLint). Each category corresponds 1:1 to a ESLint rule definition, so that the set of rules is automatically populated based on the defined categories. Categories include a flag for whether they should be in the recommended set or not. Note that as with the original version of this PR, only eslint-plugin-react-compiler is changed. We still have to update the main lint rule. ## Test Plan * Created a sample project using ESLint v9 and verified that the plugin can be configured correctly and detects errors * Edited `fixtures/eslint-v9` and introduced errors, verified that the w latest config changes in that fixture it correctly detects the errors * In the sample project, confirmed that the LRU caching is correctly caching compiler output, ie compiling files just once. Co-authored-by: Mofei Zhang <feifei0@meta.com>Joseph Savona · 7d29ecbe · 2025-08-21
- 1.7ETV[eslint-plugin-react-hooks] updates for component syntax (#33089) Adds support for Flow's component and hook syntax. [docs](https://flow.org/en/docs/react/component-syntax/)Jan Kassens · 4c4a57c4 · 2025-05-02
- 1.6ETV[Flight] Add Debug Channel option for stateful connection to the backend in DEV (#33627) This adds plumbing for opening a stream from the Flight Client to the Flight Server so it can ask for more data on-demand. In this mode, the Flight Server keeps the connection open as long as the client is still alive and there's more objects to load. It retains any depth limited objects so that they can be asked for later. In this first PR it just releases the object when it's discovered on the server and doesn't actually lazy load it yet. That's coming in a follow up. This strategy is built on the model that each request has its own channel for this. Instead of some global registry. That ensures that referential identity is preserved within a Request and the Request can refer to previously written objects by reference. The fixture implements a WebSocket per request but it doesn't have to be done that way. It can be multiplexed through an existing WebSocket for example. The current protocol is just a Readable(Stream) on the server and WritableStream on the client. It could even be sent through a HTTP request body if browsers implemented full duplex (which they don't). This PR only implements the direction of messages from Client to Server. However, I also plan on adding Debug Channel in the other direction to allow debug info (optionally) be sent from Server to Client through this channel instead of through the main RSC request. So the `debugChannel` option will be able to take writable or readable or both. --------- Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>Sebastian Markbåge · bbc13fa1 · 2025-06-24