It's useful to be able to distinguish between different invocations of
common helper libraries (like fetch) without having to click through
each one.
This adds a heuristic to extract a useful description of I/O from the
Promise value. We try to find things like getUser(id) -> User where
User.id is the id or fetch(url) -> Response where Response.url is the
url.
For urls we use the filename (or hostname if there is none) as the short
name if it can fit. The full url is in the tooltip.
<img width="845" alt="Screenshot 2025-06-27 at 7 58 20 PM"
src="https://github.com/user-attachments/assets/95f10c08-13a8-449e-97e8-52f0083a65dc"
/>
Stacked on #33658.
Unfortunately `console.timeStamp` has the same bug that
`performance.measure` used to have where equal start/end times stack in
call order instead of reverse call-order. We rely on that in general so
we should really switch back all.
But there is one case in particular where we always add the same
start/time and that's for the "triggers" -
Mount/Unmount/Reconnect/Disconnect. Switching to `console.timeStamp`
broke this because they now showed below the thing that mounted.
After:
<img width="726" alt="Screenshot 2025-06-27 at 3 31 16 PM"
src="https://github.com/user-attachments/assets/422341c8-bef6-4909-9403-933d76b71508"
/>
Also fixed a bug where clamped update times could end up logging zero
width entries that stacked up on top of each other causing a two row
scheduler lane which should always be one row.
<img width="634" alt="Screenshot 2025-06-27 at 1 13 20 PM"
src="https://github.com/user-attachments/assets/dc8c488b-4a23-453f-918f-36b245364934"
/>
We have to be careful with performance in DEV. It can slow down DX since
these are ran whether you're currently running a performance trace or
not. It can also show up as misleading since these add time to the
"Remaining Effects" entry.
I'm not adding all props to the entries. Instead, I'm only adding the
changed props after diffing and none for initial mount. I'm trying to as
much as possible pick a fast path when possible. I'm also only logging
this for the "render" entries and not the effects. If we did something
for effects, it would be more like checking with dep changed.
This could still have a negative effect on dev performance since we're
now also using the slower `performance.measure` API when there's a diff.
View Transitions has this annoying quirk where it adds `width` and
`height` to keyframes automatically when generating keyframes even when
it's not needed. This causes them to deopt from running on the
compositor thread in both Chrome and Safari. @bramus has a [good article
on
it](https://www.bram.us/2025/02/07/view-transitions-applied-more-performant-view-transition-group-animations/).
In React we can automatically rewrite the keyframes when we're starting
a View Transition to drop the `width` and `height` from the keyframes
when they have the same value and the same value as the pseudo element.
To compare it against the pseudo element we first apply the new
keyframes without the width/height and then read it back to see if it
has changed. For gestures, we have already cancelled the previous
animation so we can just read out from that.
The React API is just that we now accept this protocol as an alternative
to a native `AnimationTimeline` to be passed to
`startGestureTransition`. This is specifically the DOM version.
```js
interface CustomTimeline {
currentTime: number;
animate(animation: Animation): void | (() => void);
}
```
Instead, of passing this to the `Animation` that we start to control the
View Transition keyframes, we instead inverse the control and pass the
`Animation` to this one. It lets any custom implementation drive the
updates. It can do so by updating the time every frame or letting it run
a time based animation (such as momentum scroll).
In this case I added a basic polyfill for `ScrollTimeline` in the
example but we'll need a better one.
`react-stack-bottom-frame` -> `react_stack_bottom_frame`.
This survives `@babel/plugin-transform-function-name`, but now frames
will be displayed as `at Object.react_stack_bottom_frame (...)` in V8.
Checks that were relying on exact function name match were updated to
use either `.indexOf()` or `.includes()`
For backwards compatibility, both React DevTools and Flight Client will
look for both options. I am not so sure about the latter and if React
version is locked.
We generally treat these types of fields as optional on ReactDebugInfo
and should on ReactElement too.
That way we can consume prod payloads from third parties.
Stacked on #33666.
If we ever get a future reference to a cycle and that reference gets
eagerly parsed before the target has loaded then we can end up with a
cycle that never gets resolved. That's because our cycle resolution only
works if the cyclic future reference is created synchronously within the
parsing path of the child.
I haven't been able to construct a normal scenario where this would
break. So this doesn't fail any tests. However, I can construct it with
debug info since those are eagerly evaluated. It's also a prerequisite
if the debug data can come out of order, like if it's on a different
stream.
The fix here is to make all the internal dependencies in the "listener"
list into introspectable objects instead of closures. That way we can
traverse the list of dependencies of a blocked reference to see if it
ends up in a cycle and therefore skip the reference.
It would be nice to address this once and for all to be more resilient
to server changes, but I'm not sure if it's worth this complexity and
the extra CPU cost of tracing the dependencies. Especially if it's just
for debug data.
closes#32316fixesvercel/next.js#72104
---------
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
## Summary
This floods Timings track in dev mode and also hurts performance in dev.
Making sure we are buffering Performance entries (all of them are marks)
only when profiling in RDT. This should be removed once we roll out Perf
tracks.
This writes all debug info to a separate priority queue. In the future
I'll put this on a different channel.
Ideally I think we'd put it in the bottom of the stream but because it
actually blocks the elements from resolving anyway it ends up being
better to put them ahead. At least for now.
When we have two separate channels it's not possible to rely on the
order for consistency Even then we might write to that queue first for
this reason. We can't rely on it though. Which will show up like things
turning into Lazy instead of Element similar to how outlining can.
There's a special case where if we create a new task, e.g. to serialize
a promise like `<div>{promise}</div>` then that row doesn't have any
start time emitted but it has a `task.time` inherited. We mostly don't
need this because every other operation emits its own start time. E.g.
when we started rendering a Server Component or the real start time of a
real `await`.
For these implied awaits we don't have a start time. Ideally it would
probably be when we started the serialization, like when we called
`.then()` but we can't just emit that eagerly and we can't just advance
the `task.time` because that time represents the last render or previous
await and we use that to cut off awaits. However for this case we don't
want to cut off any inner awaits inside the node we're serializing if
they happened before the `.then()`.
Therefore, I just use the time of the previous operation - which is
likely either the resolution of a previous promise that blocked the
`<div>` like the promise of the Server Component that rendered it, or
just the start of the Server Component if it was sync.
This PR adds tests for the Node.js and Edge builds to verify that
component stacks and owner stacks of halted components appear as
expected, now that recent enhancements for those have been implemented
(the latest one being #33634).
---------
Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
<img width="926" alt="Screenshot 2025-06-25 at 1 02 14 PM"
src="https://github.com/user-attachments/assets/1877d13d-5259-4cc4-8f48-12981e3073fe"
/>
The I/O entry doesn't show as aborted in the Server Request track
because technically it wasn't. The end time is just made up. It's still
going. It's not aborted until the abort signal propagates and if we do
get that signal wired up before it emits, it instead would show up as
rejected.
---------
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
If an aborted task is not rendering, then this is an async abort.
Conceptually it's as if the abort happened inside the async gap. The
abort reason's stack frame won't have that on the stack so instead we
use the owner stack and debug task of any halted async debug info.
One thing that's a bit awkward is that if you do have a sync abort and
you use that error as the "reason" then that thing still has a sync
stack in a different component. In another approach I was exploring
having different error objects for each component but I don't think
that's worth it.
Now that we have `cacheSignal()` we can just use that instead of the
`abortListeners` concept which was really just the same thing for
cancelling the streams (ReadableStream, Blob, AsyncIterable).
## Summary
ReactNativeAttributePayloadFabric was synced to react-native in
https://github.com/facebook/react-native/commit/0e42d33cbcfadcf5d787108da785d56a83d07a9f.
We should now consume these methods from the
ReactNativePrivateInterface.
Moving these methods to the React Native repo gives us more flexibility
to experiment with new techniques for bridging and diffing props
payloads.
I did have to leave some stub implementations for existing unit tests,
but moved all detailed tests to the React Native repo.
## How did you test this change?
* `yarn prettier`
* `yarn test ReactFabric-test`
When we abort a render we don't really have much information about the
task that was aborted. Because before a Promise resolves there's no
indication about would have resolved it. In particular we don't know
which I/O would've ultimately called resolve().
However, we can at least emit any information we do have at the point
where we emit it. At the least the stack of the top most Promise.
Currently we synchronously flush at the end of an `abort()` but we
should ideally schedule the flush in a macrotask and emit this debug
information right before that. That way we would give an opportunity for
any `cacheSignal()` abort to trigger rejections all the way up and those
rejections informs the awaited stack.
---------
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
This is using the same trick as #30798 but for runtime code too. It's
essential zero cost.
This lets us include a source location for parent stacks of Server
Components when it has an owned child's location. Either from JSX or
I/O.
Ironically, a Component that throws an error will likely itself not get
the stack because it won't have any JSX rendered yet.
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>
This frees some memory that will be even more important in a follow up.
Currently, all `ReactPromise` instances hold onto their original
`Response`. The `Response` holds onto all objects that were in that
response since they're needed in case the parsed content ends up
referring to an existing object. If everything you retain are plain
objects then that's fine and the `Response` gets GC:ed, but if you're
retaining a `Promise` itself then it holds onto the whole `Response`.
The only thing that needs this reference at all is a
`ResolvedModelChunk` since it will lazily initialize e.g. by calling
`.then` on itself and so we need to know where to find any sibling
chunks it may refer to. However, we can just store the `Response` on the
`reason` field for this particular state.
That way when all lazy values are touched and initialized the `Response`
is freed. We also free up some memory by getting rid of the extra field.
It wasn't immediately obvious to me, that all the exports here are
related to legacy context, so renaming for clarity.
Modern context lives in `ReactFiberNewContext` which we could probably
also raname in a separate step to just Context.
Stacked on #33588, #33589 and #33590.
This lets us automatically show the resolved value in the UI.
<img width="863" alt="Screenshot 2025-06-22 at 12 54 41 AM"
src="https://github.com/user-attachments/assets/a66d1d5e-0513-4767-910c-5c7169fc2df4"
/>
We can also show rejected I/O that may or may not have been handled with
the error message.
<img width="838" alt="Screenshot 2025-06-22 at 12 55 06 AM"
src="https://github.com/user-attachments/assets/e0a8b6ae-08ba-46d8-8cc5-efb60956a1d1"
/>
To get this working we need to keep the Promise around for longer so
that we can access it once we want to emit an async sequence. I do this
by storing the WeakRefs but to ensure that the Promise doesn't get
garbage collected, I keep a WeakMap of Promise to the Promise that it
depended on. This lets the VM still clean up any Promise chains that
have leaves that are cleaned up. So this makes Promises live until the
last Promise downstream is done. At that point we can go back up the
chain to read the values out of them.
Additionally, to get the best possible value we don't want to get a
Promise that's used by internals of a third-party function. We want the
value that the first party gets to observe. To do this I had to change
the logic for which "await" to use, to be the one that is the first
await that happened in user space. It's not enough that the await has
any first party at all on the stack - it has to be the very first frame.
This is a little sketchy because it relies on the `.then()` call or
`await` call not having any third party wrappers. But it gives the best
object since it hides all the internals. For example when you call
`fetch()` we now log that actual `Response` object.
This adds better support for serializing class instances as Debug
values.
It adds a new marker on the object `{ "": "$P...", ... }` which
indicates which constructor's prototype to use for this object's
prototype. It doesn't encode arbitrary prototypes and it doesn't encode
any of the properties on the prototype. It might get some of the
properties from the prototype by virtue of `toString` on a `class`
constructor will include the whole class's body.
This will ensure that the instance gets the right name in logs.
Additionally, this now also invokes getters if they're enumerable on the
prototype. This lets us reify values that can only be read from native
classes.
---------
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
We already support serializing the values of instrumented Promises as
debug values such as in console logs. However, we don't support plain
native promises.
This waits a microtask to see if we can read the value within a
microtask and if so emit it. This is so that we can still close the
connection.
Otherwise, we emit a "halted" row into its row id which replaces the old
"Infinite Promise" reference.
We could potentially wait until the end of the render before cancelling
so that if it resolves before we exit we can still include its value but
that would require a bit more work. Ideally we'd have a way to get these
lazily later anyway.
It turns out this was being compiled to a `_defineProperty` helper by
Babel or Closure. We're supposed to have it error the build when we use
features like this that might get compiled.
We should stick to simple ES5 features.
There's a memory leak in DebugNode where the `Error` objects that we
instantiate retains their callstacks which can have Promises on them. In
fact, it's very likely since the current callsite has the "resource" on
it which is the Promise itself. If those Promises are retained then
their `destroy` async hook is never fired which doesn't clean up our map
which can contains the `Error` object. Creating a cycle that can't be
cleaned up.
This fix is just eagerly reifying and parsing the stacks.
I totally expect this to be crazy slow since there's so many Promises
that we end up not needing to visit otherwise. We'll need to optimize it
somehow. Perhaps by being smarter about which ones we might need stacks
for. However, at least it doesn't leak indefinitely.
Stacked on #33539.
Stores dedupes of `renderConsoleValue` in a separate set. This allows us
to dedupe objects safely since we can't write objects using this
algorithm if they might also be referenced by the "real" serialization.
Also renamed it to `renderDebugModel` since it's not just for console
anymore.
On pages that have a high number of server components (e.g. common when
doing syntax highlighting), the debug outlining can produce extremely
large RSC payloads. For example a documentation page I was working on
had a 13.8 MB payload. I noticed that a majority of this was the source
code for the same function components repeated over and over again (over
4000 times) within `$E()` eval commands.
This PR deduplicates the same functions by serializing by reference,
similar to what is already done for objects. Doing this reduced the
payload size of my page from 13.8 MB to 4.6 MB, and resulted in only 31
evals instead of over 4000. As a result it reduced development page load
and hydration time from 4 seconds to 1.5 seconds. It also means the
deserialized functions will have reference equality just as they did on
the server.
## Summary
Make this flag dynamic, so it can be controlled internally.
## How did you test this change?
Build, observe that `console.timeStamp` is only present in FB artifacts
and `enableComponentPerformanceTrack` is referenced.
This was really meant to be there from the beginning. A `cache()`:ed
entry has a life time. On the server this ends when the render finishes.
On the client this ends when the cache of that scope gets refreshed.
When a cache is no longer needed, it should be possible to abort any
outstanding network requests or other resources. That's what
`cacheSignal()` gives you. It returns an `AbortSignal` which aborts when
the cache lifetime is done based on the same execution scope as a
`cache()`ed function - i.e. `AsyncLocalStorage` on the server or the
render scope on the client.
```js
import {cacheSignal} from 'react';
async function Component() {
await fetch(url, { signal: cacheSignal() });
}
```
For `fetch` in particular, a patch should really just do this
automatically for you. But it's useful for other resources like database
connections.
Another reason it's useful to have a `cacheSignal()` is to ignore any
errors that might have triggered from the act of being aborted. This is
just a general useful JavaScript pattern if you have access to a signal:
```js
async function getData(id, signal) {
try {
await queryDatabase(id, { signal });
} catch (x) {
if (!signal.aborted) {
logError(x); // only log if it's a real error and not due to cancellation
}
return null;
}
}
```
This just gets you a convenient way to get to it without drilling
through so a more idiomatic code in React might look something like.
```js
import {cacheSignal} from "react";
async function getData(id) {
try {
await queryDatabase(id);
} catch (x) {
if (!cacheSignal()?.aborted) {
logError(x);
}
return null;
}
}
```
If it's called outside of a React render, we normally treat any cached
functions as uncached. They're not an error call. They can still load
data. It's just not cached. This is not like an aborted signal because
then you couldn't issue any requests. It's also not like an infinite
abort signal because it's not actually cached forever. Therefore,
`cacheSignal()` returns `null` when called outside of a React render
scope.
Notably the `signal` option passed to `renderToReadableStream` in both
SSR (Fizz) and RSC (Flight Server) is not the same instance that comes
out of `cacheSignal()`. If you abort the `signal` passed in, then the
`cacheSignal()` is also aborted with the same reason. However, the
`cacheSignal()` can also get aborted if the render completes
successfully or fatally errors during render - allowing any outstanding
work that wasn't used to clean up. In the future we might also expand on
this to give different
[`TaskSignal`](https://developer.mozilla.org/en-US/docs/Web/API/TaskSignal)
to different scopes to pass different render or network priorities.
On the client version of `"react"` this exposes a noop (both for
Fiber/Fizz) due to `disableClientCache` flag but it's exposed so that
you can write shared code.
## Summary
The devtools Components tab's component tree view currently has a
behavior where the indentation of each level of the tree scales based on
the available width of the view. If the view is narrow or component
names are long, all indentation showing the hierarchy of the tree scales
down with the view width until there is no indentation at all. This
makes it impossible to see the nesting of the tree, making the tree view
much less useful. With long component names and deep hierarchies this
issue is particularly egregious. For comparison, the Chrome Dev Tools
Elements panel uses a fixed indentation size, so it doesn't suffer from
this issue.
This PR adds a minimum pixel value for the indentation width, so that
even when the window is narrow some indentation will still be visible,
maintaining the visual representation of the component tree hierarchy.
Alternatively, we could match the behavior of the Chrome Dev Tools and
just use a constant indentation width.
## How did you test this change?
- tests (yarn test-build-devtools)
- tested in browser:
- added an alternate left/right split pane layout to
react-devtools-shell to test with
(https://github.com/facebook/react/pull/33516)
- tested resizing the tree view in different layout modes
### before this change:
https://github.com/user-attachments/assets/470991f1-dc05-473f-a2cb-4f7333f6bae4
with a long component name:
https://github.com/user-attachments/assets/1568fc64-c7d7-4659-bfb1-9bfc9592fb9d
### after this change:
https://github.com/user-attachments/assets/f60bd7fc-97f6-4680-9656-f0db3d155411
with a long component name:
https://github.com/user-attachments/assets/6ac3f58c-42ea-4c5a-9a52-c3b397f37b45
## Summary
This PR adds a 'Layout' selector to the devtools shell main example, as
well as a resizable split pane, allowing more realistic testing of how
the devtools behaves when used in a vertical or horizontal layout and at
different sizes (e.g. when resizing the Chrome Dev Tools pane).
## How did you test this change?
https://github.com/user-attachments/assets/81179413-7b46-47a9-bc52-4f7ec414e8be