6810 Commits
Author SHA1 Message Date
Jordan Brown ac4ac1f5a6 [lint] Enable custom hooks configuration for useEffectEvent calling rules
We need to be able to specify additional effect hooks for the RulesOfHooks lint rule
in order to allow useEffectEvent to be called by custom effects. ExhaustiveDeps
does this with a regex suppplied to the rule, but that regex is not accessible from
other rules.

This diff introduces a `react-eslint` entry you can put in the eslint settings that
allows you to specify custom effect hooks and share them across all rules.

This works like:
```
{
  settings: {
    'react-eslint': {
      additionalEffectHooks: string,
    },
  },
}
```

The next diff allows useEffect to read from the same configuration.


----
2025-09-30 07:13:44 -04:00
Sebastian Markbåge 09d3cd8fb5 [DevTools] Larger panel buttons and center (#34619)
The panel icons are quite small. Especially compared to the equivalent
buttons elsewhere in Chrome DevTools that otherwise use the same icons.
This makes them a little bigger to make them similar size to our other
button icons.

They were also a bit off center. This centers them as well.

Before:

<img width="409" height="426" alt="Screenshot 2025-09-26 at 4 23 15 PM"
src="https://github.com/user-attachments/assets/4a5de032-e316-44ed-9424-8bccce00f0cd"
/>

After:

<img width="519" height="388" alt="Screenshot 2025-09-26 at 4 22 57 PM"
src="https://github.com/user-attachments/assets/1763e522-5683-4fac-a913-27910a30a039"
/>
2025-09-28 12:09:08 -04:00
Sebastian Markbåge f78b2343cc [DevTools] Recursively compute the bounding rect of the roots (#34629)
It's possible for the children to overflow the bounding rect of the root
in general when they overflow in the DOM. However even when it doesn't
overflow in the DOM, the bounding rect of the root can shrink while the
content is suspended. In fact, it's very likely.

Originally I thought we didn't need to consider this recursively because
document scrolling takes absolute positioned content into account but
because we're using nested overflow scrolling, we have to manually
compute this.
2025-09-28 10:15:31 -04:00
Sebastian "Sebbie" Silbermann e08f53b182 Match react-dom/static test entrypoints and published entrypoints (#34599) 2025-09-28 13:26:31 +02:00
Sebastian Markbåge 2622487a74 [DevTools] Move Timeline to footer instead of header (#34617)
One thing that always bothered me is that the collapse buttons on either
side of the toolbar looks like left/right buttons which would conflict
with some steps buttons I plan to add. Another issue is that we'll need
to add more tool buttons to the top and probably eventually a Search
field. Ideally this whole section should line up vertically with the
height of the title row.

I also realized that all UIs that have some kind of timeline control
(and play/pause/skip) do that in the bottom below the content. E.g.
music players and video players all do that. We're better off playing
into that structure since that's the UI analogy we're going for here.
Makes it clearer what the weird timeline is for.

By moving it to the bottom it also frees up the top for the collapse
buttons and more controls.

__Horizontal__

<img width="794" height="809" alt="Screenshot 2025-09-26 at 3 40 35 PM"
src="https://github.com/user-attachments/assets/dacad9c4-d52f-4b66-9585-5cc74f230e6f"
/>

__Vertical__

<img width="570" height="812" alt="Screenshot 2025-09-26 at 3 40 53 PM"
src="https://github.com/user-attachments/assets/db225413-849e-46f1-b764-8fbd08b395c4"
/>
2025-09-26 16:27:49 -04:00
Sebastian "Sebbie" Silbermann 8a24ef3e75 [DevTools] Show Transition indicator when "suspended by" rows are expanded (#34565) 2025-09-26 22:27:22 +02:00
Ruslan Lesiutin c552618a82 flags: make enableAsyncDebugInfo dynamic for www (#34430)
As titled. This adds dev-only debugging information to Fizz / Flight
that could be used for tracking Promise's stack traces in "suspended by"
section of DevTools.
2025-09-26 11:43:03 -07:00
Sebastian "Sebbie" Silbermann df38ac9a3b Ensure useEffectEvent implementation is available in Canary (#34614) 2025-09-26 18:53:12 +02:00
Jack Pope 8bb7241f4c Bump useEffectEvent to Canary (#34610)
Bumps `useEffectEvent` from `@experimental` to `@canary`. Removes the
`experimental_` prefix from the export.

## TODO
- [ ] Update useEffectEvent reference page and Canary badging in docs:
https://github.com/reactjs/react.dev/pull/8025
2025-09-26 11:51:30 -04:00
Sebastian "Sebbie" Silbermann 8d557a638e [DevTools] Only show Suspense rects matching "unique-suspenders-only" filter (#34607) 2025-09-26 17:29:15 +02:00
Sebastian MarkbågeandSebastian Sebbie Silbermann 6a51a9fea6 [DevTools] Track Server Environment Names of Each SuspenseNode (#34605)
Tracks the environment names of the I/O in each SuspenseNode and sent it
to the front end when the suspenders change.

In the front end, every child boundary should really be treated as it
has all environment names of the parents too since they're blocked by
the parent too. We could do this tracking on backend but if there's ever
one added on the root would need to be send for every child.

This lets us highlight which subtrees are blocked by content on the
server.

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
2025-09-26 09:43:38 -04:00
Sebastian Markbåge 1fd291d3c5 [DevTools] Disable the tree list for now (#34606)
When there are no named Activities we should hide the tree side panel
(and the button to show it). Since it's not implemented yet there are
never any ones so it's always hidden.
2025-09-26 09:43:00 -04:00
Sebastian MarkbågeandJosh Story 047715c4ba [Flight] Preload <img> and <link> using hints before they're rendered (#34604)
In Fizz and Fiber we emit hints for suspensey images and CSS as soon as
we discover them during render. At the beginning of the stream. This
adds a similar capability when a Host Component is known to be a Host
Component during the Flight render.

The client doesn't know that these resources are in the payload until it
parses that particular component which is lazy. So they need to be
hoisted with hints. We detect when these are rendered during Flight and
add them as hints. That allows you to consume a Flight payload to
preload prefetched content without having to render it.

`<link rel="preload">` can be hoisted more or less as is.

`<link rel="stylesheet">` we preload but we don't actually insert them
anywhere until they're rendered. We do these even for non-suspensey
stylesheets since we know that when they're rendered they're going to
start loading even if they're not immediately used. They're never lazy.

`<img src>` we only preload if they follow the suspensey image pattern
since otherwise they may be more lazy e.g. by if they're in the
viewport. We also skip if they're known to be inside `<picture>`. Same
as Fizz. Ideally this would preload the other `<source>` but it's
tricky.

The downside of this is that you might conditionally render something in
only one branch given a client component. However, in that case you're
already eagerly fetching the server component's data in that branch so
it's not too much of a stretch that you want to eagerly fetch the
corresponding resources as well. If you wanted it to be lazy, you
should've done a lazy fetch of the RSC.

We don't collect hints when any of these are wrapped in a Client
Component. In those cases you might want to add your own preload to a
wrapper Shared Component.

Everything is skipped if it's known to be inside `<noscript>`.

Note that the format context is approximate (see #34601) so it's
possible for these hints to overfetch or underfetch if you try to trick
it. E.g. by rendering Server Components inside a Client Component that
renders `<noscript>`.

---------

Co-authored-by: Josh Story <josh.c.story@gmail.com>
2025-09-25 23:44:14 -04:00
Sebastian Markbåge b0c1dc01ec [Flight] Add approximate parent context for FormatContext (#34601)
Flight doesn't have any semantically sound notion of a parent context.
That's why we removed Server Context. Each root can really start
anywhere in the tree when you refetch subtrees. Additionally when you
dedupe elements they can end up in multiple different parent contexts.

However, we do have a DEV only version of this with debugTask being
tracked for the nearest parent element to track the context of
properties inside of it.

To apply certain DOM specific hints and optimizations when you render
host components we need some information of the context. This is usually
very local so doesn't suffer from the likelihood that you refetch in the
middle. We'll also only use this information for optimistic hints and
not hard semantics so getting it wrong isn't terrible.

```
<picture>
  <img />
</picture>
<noscript>
  <p>
    <img />
  </p>
</noscript>
```

For example, in these cases we should exclude preloading the image but
we have to know if that's the scope we're in.

We can easily get this wrong if they're split or even if they're wrapped
in client components that we don't know about like:

```
<NoScript>
  <p>
    <img />
  </p>
</NoScript>
```

However, getting it wrong in either direction is not the end of the
world. It's about covering the common cases well.
2025-09-25 12:05:47 -04:00
Sebastian Markbåge 6eb5d67e9c [Fizz] Outline a Suspense Boundary if it has Suspensey CSS or Images (#34552)
We should favor outlining a boundary if it contains Suspensey CSS or
Suspensey Images since then we can load that content separately and not
block the main content. This also allows us to animate the reveal.

For example this should be able to animate the reveal even though the
actual HTML content isn't large in this case it's worth outlining so
that the JS runtime can choose to animate this reveal.

```js
<ViewTransition>
  <Suspense>
    <img src="..." />
  </Suspense>
</ViewTransition>
```

For Suspensey Images, in Fizz, we currently only implement the suspensey
semantics when a View Transition is running. Therefore the outlining
only applies if it appears inside a Suspense boundary which might
animate. Otherwise there's no point in outlining. It is also only if the
Suspense boundary itself might animate its appear and not just any
ViewTransition. So the effect is very conservative.

For CSS it applies even without ViewTransition though, since it can help
unblock the main content faster.
2025-09-25 09:38:41 -04:00
Hendrik LiebauandSebastian Markbage ac2c1a5a58 [Flight] Ensure blocked debug info is handled properly (#34524)
This PR ensures that server components are reliably included in the
DevTools component tree, even if debug info is received delayed, e.g.
when using a debug channel. The fix consists of three parts:

- We must not unset the debug chunk before all debug info entries are
resolved.
- We must ensure that the "RSC Stream" IO debug info entry is pushed
last, after all other entries were resolved.
- We need to transfer the debug info from blocked element chunks onto
the lazy node and the element.

Ideally, we wouldn't even create a lazy node for blocked elements that
are at the root of the JSON payload, because that would basically wrap a
lazy in a lazy. This optimization that ensures that everything around
the blocked element can proceed is only needed for nested elements.
However, we also need it for resolving deduped references in blocked
root elements, unless we adapt that logic, which would be a bigger lift.

When reloading the Flight fixture, the component tree is now displayed
deterministically. Previously, it would sometimes omit synchronous
server components.

<img width="306" height="565" alt="complete"
src="https://github.com/user-attachments/assets/db61aa10-1816-43e6-9903-0e585190cdf1"
/>

---------

Co-authored-by: Sebastian Markbage <sebastian@calyptus.eu>
2025-09-25 15:13:15 +02:00
Sebastian "Sebbie" Silbermann c44fbf43b1 [DevTools] Fix instrumentation error when reconciling promise-as-a-child (#34587) 2025-09-24 22:50:12 +02:00
Sebastian "Sebbie" Silbermann 58d17912e8 Fix failing React DevTools regression tests (#34585) 2025-09-24 19:08:13 +02:00
Sebastian Markbåge e233218359 Track "Animating" Entry for Gestures while the Gesture is Still On-going (#34548)
Stacked on #34546.

Same as #34538 but for gestures.

Includes various fixes.

This shows how it ends with a Transition when you release in the
committed state. Note how the Animation of the Gesture continues until
the Transition is done so that the handoff is seamless.

<img width="853" height="134" alt="Screenshot 2025-09-20 at 7 37 29 PM"
src="https://github.com/user-attachments/assets/6192a033-4bec-43b9-884b-77e3a6f00da6"
/>
2025-09-24 11:26:03 -04:00
Sebastian Markbåge 05b61f812a Add Gesture Track in Performance Tab (#34546) 2025-09-24 17:20:14 +02:00
Sebastian Markbåge e0c421ab71 Include SyncLane in includesBlockingLane helper (#34543)
This helper weirdly doesn't include the sync lane.

Everywhere we use it we have to check the sync lane separately. We can
simplify things by simply including the sync lane.

This fixes a lack of optimization because we should not check the store
consistency for a `flushSync` render.


https://github.com/facebook/react/blob/d91d28c8ba6fe7c96e651f82fc47c9d5481bf5f9/packages/react-reconciler/src/ReactFiberHooks.js#L1691-L1693
2025-09-24 09:34:35 -04:00
Ruslan Lesiutin 2ee6147510 [DevTools] Switch sourcemap-codec dependency (#34569)
[sourcemap-codec](https://www.npmjs.com/package/sourcemap-codec)
(deprecated) ->
[@jridgewell/sourcemap-codec](https://www.npmjs.com/package/@jridgewell/sourcemap-codec)

Validated that symbolication still works.
2025-09-24 06:11:53 -07:00
Jordan Brown e02c173fa5 [lint] Allow useEffectEvent in useLayoutEffect and useInsertionEffect (#34492)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34492).
* #34497
* __->__ #34492
2025-09-23 16:56:09 -04:00
Ruslan Lesiutin 24a2ba03fb [DevTools] fix: dedupe file fetch requests and define a timeout (#34566)
If there is a large owner stack, we could potentially spam multiple
fetch requests for the same source map. This adds a simple deduplication
logic, based on URL.

Also, this adds a timeout of 60 seconds to all fetch requests initiated
by fileFetcher content script.
2025-09-23 11:38:07 -07:00
Sebastian "Sebbie" Silbermann 012b371cde [DevTools] Handle LegacyHidden Fibers like Offscreen Fibers. (#34564) 2025-09-23 20:14:53 +02:00
Jack Pope 83c88ad470 Handle fabric root level fragment with compareDocumentPosition (#34533)
The root instance doesn't have a canonical property so we were not
returning a public instance that we can call compareDocumentPosition on
when a Fragment had no other host parent in Fabric. In this case we need
to get the ReactNativeElement from the ReactNativeDocument.

I've also added test coverage for this case in DOM for consistency,
though it was already working there because we use DOM elements as root.
This same test will be copied to RN using Fantom.
2025-09-23 10:56:43 -04:00
Sebastian "Sebbie" Silbermann 720bb13069 [compiler] Export PluginOptions as a type that can be used in input positions (#34550) 2025-09-22 18:28:19 +02:00
Sebastian Markbåge d91d28c8ba Use the JSX of the ViewTransition as the Stack Trace of "Animating" Traces (#34539)
Stacked on #34538.

Track the Task of the first ViewTransition that we detected as
animating. Use this as the Task as "Starting Animation", "Animating"
etc. That way you can see which ViewTransition spawned the Animation.
Although it's likely to be multiple.

<img width="757" height="393" alt="Screenshot 2025-09-19 at 10 19 18 PM"
src="https://github.com/user-attachments/assets/a6cdcb89-bd02-40ec-b3c3-11121c29e892"
/>
2025-09-20 11:11:27 -04:00
Sebastian Markbåge b4fe1e6c7e Log the time until the Animation finishes as "Animating" (#34538)
Stacked on #34522.

<img width="1025" height="200" alt="Screenshot 2025-09-19 at 6 37 28 PM"
src="https://github.com/user-attachments/assets/f25900f6-6503-48b1-876d-bd6697a29c6f"
/>

We already cover the time between "Starting Animation" and "Remaining
Effects" as "Animating". However, if the effects are forced then we can
still be animating after that. This fills in that gap.

This also fills in the gap if another render starts before the animation
finishes on the same track. It'll mark the blank space between the
previous render finishing and the next render starting as "Animating".

This should correspond roughly to the native "Animations" track.
2025-09-20 11:10:42 -04:00
Sebastian Markbåge b204edda3a Log Custom Reason for the Suspended Commit Track (#34522)
Stacked on #34511.

We currently log all Suspended Commit as "Suspended on Images or CSS"
but it can really be other reasons too now. Like waiting on the previous
View Transition. This allows the host config configure this reason.

Now when one animation starts before another one finishes we log that as
"Waiting for the previous Animation".

<img width="592" height="257" alt="Screenshot 2025-09-17 at 11 53 45 PM"
src="https://github.com/user-attachments/assets/817af8b5-37ae-46d8-bfd1-cd3fc637f3f3"
/>
2025-09-20 11:01:52 -04:00
Sebastian Markbåge 565eb7888e Unwrap a reference to a Lazy value (#34535)
If we are referencing a lazy value that isn't explicitly lazy ($L...)
it's because we added it around an element that was blocked to be able
to defer things inside.

However, once that is unblocked we can start unwrap it and just use the
inner element instead for any future reference. The race condition is
still there since it's a race condition whether we added the wrapper in
the first place.

This just makes it consistent with unwrapping of the rest of the path.
2025-09-19 18:23:18 -04:00
Hendrik Liebau d415fd3ed7 [Flight] Handle Lazy in renderDebugModel (#34536)
If we don't handle Lazy types specifically in `renderDebugModel`, all of
their properties will be emitted using `renderDebugModel` as well. This
also includes its `_debugInfo` property, if the Lazy comes from the
Flight client. That array might contain objects that are deduped, and
resolving those references in the client can cause runtime errors, e.g.:

```
TypeError: Cannot read properties of undefined (reading '$$typeof')
```

This happened specifically when an "RSC stream" debug info entry, coming
from the Flight client through IO tracking, was emitted and its
`debugTask` property was deduped, which couldn't be resolved in the
client.

To avoid actually initializing a lazy causing a side-effect, we make
some assumptions about the structure of its payload, and only emit
resolved or rejected values, otherwise we emit a halted chunk.
2025-09-19 23:38:11 +02:00
Janka Uryga 01cad9eaca [Flight] Support Async Modules in Turbopack Server References (#34531)
Seems like this was missed in
https://github.com/facebook/react/pull/31313
2025-09-19 12:12:37 -07:00
Hendrik Liebau c03a51d836 Move getDebugInfo test util function to internal-test-utils (#34523)
In an upstack PR, I need `getDebugInfo` in another test file, so I'm
moving it to `internal-test-utils` so it can be shared.
2025-09-18 21:32:36 +02:00
Sebastian Markbåge ad578aa01f Log Suspended startViewTransition Phase (#34511)
Stacked on #34510.

The "Commit" phase for a View Transition starts before the snapshot
phase (before mutation) and then stretches into the async gap of
`startViewTransition`, encompasses the mutation phase inside of its
update callback and finally the layout phase.

However, between the mutation phase and the layout phase we may suspend
the start of the view transition on fonts and/or images. In that case we
now split the Commit phase into first one before we suspend and then we
log "Waiting for Images and/or Fonts" and then another Commit phase
around the layout effects.

<img width="897" height="119" alt="Screenshot 2025-09-16 at 11 37 26 PM"
src="https://github.com/user-attachments/assets/0fe21388-bb48-4456-a594-62227d12d9b7"
/>
2025-09-18 15:25:41 -04:00
Sebastian "Sebbie" Silbermann 03a96c75db [DevTools] Record Suspense node for roots in legacy renderers (#34516) 2025-09-18 18:50:23 +02:00
Sebastian "Sebbie" Silbermann 755cebad6b [DevTools] Elevate Suspense rects to visualize hierarchy (#34455) 2025-09-18 18:37:00 +02:00
Sebastian Markbåge 84af9085c1 Log Performance Track Entries for View Transitions (#34510)
Stacked on #34509.

View Transitions introduces a bunch of new types of gaps in the commit
phase which needs to be logged differently in the performance track.

One thing that can happen is that a `flushSync` update forces the View
Transition to abort before it has started if it happens in the gap
before the transition is ready. In that case we log "Interrupted View
Transition".

Otherwise, when we're done in `startViewTransition` there's some work to
finalize the animations before the `ready` calllback. This is logged as
"Starting Animation".

Then there's a gap before the passive effects fire which we log as
"Animating". This can be long unless they're forced to flush early e.g.
due to another lane updating.

The "Animating" track should then pick up which doesn't do yet. This one
is tricky because this is after the actual commit phase and needs to be
interrupted by new renders which themselves can be suspended on the
animation finshing.

This PR is just a subset of all the cases. Will need a lot more work.

<img width="679" height="161" alt="Screenshot 2025-09-16 at 10 19 06 PM"
src="https://github.com/user-attachments/assets/0407372d-aaed-41f5-a262-059b2686ae87"
/>
2025-09-17 13:06:30 -04:00
Sebastian "Sebbie" Silbermann 128abcfa01 [DevTools] Don't inline workers for extensions (#34508) 2025-09-17 17:59:55 +02:00
Sebastian Markbåge e3c9656d20 Ensure Performance Track are Clamped and Don't overlap (#34509)
This simplifies the logic for clamping the start times of various
phases. Instead of checking in multiple places I ensure we compute a
value for each phase that is then clamped to the next phase so they
don't overlap. If they're zero they're not printed.

I also added a name for all the anonymous labels. Those are mainly
fillers for sync work that should be quick but it helps debugging if we
can name them.

Finally the real fix is to update the clamp time which previously could
lead to overlapping entries for consecutive updates when a previous
update never finalized before the next update.
2025-09-17 10:52:02 -04:00
Sebastian "Sebbie" Silbermann 27b4076ab0 [DevTools] Use a single Webpack config for the extensions (#34513) 2025-09-17 15:45:25 +02:00
Sebastian "Sebbie" Silbermann 81d66927af [DevTools] Stop polyfilling Buffer (#34512) 2025-09-17 15:36:21 +02:00
Sebastian "Sebbie" Silbermann 6a4c8f51fa [DevTools] Store Webpack stats when building extensions (#34514) 2025-09-17 15:03:12 +02:00
Sebastian "Sebbie" Silbermann 16df13b84c [DevTools] Minify backend (#34507) 2025-09-17 14:52:32 +02:00
Sebastian "Sebbie" Silbermann a51f925217 [DevTools] Only check if we previously removed IO if its removal failed (#34506) 2025-09-16 19:55:03 +02:00
Sebastian "Sebbie" Silbermann 941cd803a7 [DevTools] Don't keep stale root instances we never mounted around (#34504) 2025-09-16 19:17:28 +02:00
Sebastian "Sebbie" Silbermann 851bad0c88 [DevTools] Ignore repeated removals of the same IO (#34495) 2025-09-16 18:54:52 +02:00
Sebastian Markbåge 348a4e2d44 [Fiber] Wait for suspensey image in the viewport before starting an animation (#34500)
Stacked on #34486.

If we gave up on loading suspensey images for blocking the commit (e.g.
due to #34481), we can still block the view transition from committing
to allow an animation to include the image from the start.

At this point we have more information about the layout so we can
include only the images that are within viewport in the calculation
which may end up with a different answer.

This only applies when we attempt to run an animation (e.g. something
mutated inside a `<ViewTransition>` in a Transition). We could attempt a
`startViewTransition` if we gave up on the suspensey images just so that
we could block it even if no animation would be running.

However, this point the screen is frozen and you can no longer have sync
updates interrupt so ideally we would have already blocked the commit
from happening in the first place.

The reason to have two points where we block is that ideally we leave
the UI responsive while blocking, which blocking the commit does. In the
simple case of all images or a single image being within the viewport,
that's favorable. By combining the techniques we only end up freezing
the screen in the special case that we had a lot of images added outside
the viewport and started an animation with some image inside the
viewport (which presumably is about to finish anyway).
2025-09-15 18:11:04 -04:00
Sebastian Markbåge 5d49b2b7f4 [Fiber] Track SuspendedState on stack instead of global (#34486)
Stacked on #34481.

We currently track the suspended state temporarily with a global which
is safe as long as we always read it during a sync pass. However, we
sometimes read it in closures and then we have to be carefully to pass
the right one since it's possible another commit on a different root has
started at that point. This avoids this footgun.

Another reason to do this is that I want to read it in
`startViewTransition` which is in an async gap after which point it's no
longer safe. So I have to pass that through the `commitRoot` bound
function.
2025-09-15 16:10:47 -04:00
Sebastian Markbåge ae22247dce [Fiber] Don't wait on Suspensey Images if we guess that we don't load them all in time anyway (#34481)
Stacked on #34478.

In general we don't like to deal with timeouts in suspense world. We've
had that in the past but in general it doesn't work well because if you
have a timeout and then give up you made everything wait longer for no
benefit at the end. That's why the recommendation is to remove a
Suspense boundary if you expect it to be fast and add one if you expect
it to be slow. You have to estimate as the developer.

Suspensey images suffer from this same problem. We want to apply
suspensey images to as much as possible so that it's the default to
avoid flashing because if just a few images flash it's still almost as
bad as all of them. However, we do know that it's also very common to
use images and on a slow connection or many images, it's not worth it so
we have the timeout to eventually give up.

However, this means that in cases that are always slow or connections
that are always slow, you're always punished for no reason.

Suspensey images is mainly a polish feature to make high end experiences
on high end connections better but we don't want to unnecessarily punish
all slow connections in the process or things like lots of images below
the viewport.

This PR adds an estimate for whether or not we'll likely be able to load
all the images within the timeout on a high end enough connection. If
not, we'll still do a short suspend (unless we've already exceeded the
wait time adjusted for #34478) to allow loading from cache if available.

This estimate is based on two heuristics:

1) We compute an estimated bandwidth available on the current device in
mbps. This is computed from performance entries that have loaded static
resources already on the site. E.g. this can be other images, css, or
scripts. We see how long they took. If we don't have any entries (or if
they're all cross-origin in Safari) we fallback to
`navigator.connection.downlink` in Chrome or a 5mbps default in
Firefox/Safari.
2) To estimate how many bytes we'll have to download we use the
width/height props of the img tag if available (or a 100 pixel default)
times the device pixel ratio. We assume that a good img implementation
downloads proper resolution image for the device and defines a
width/height up front to avoid layout trash. Then we estimate that it
takes about 0.25 bytes per pixel which is somewhat conservative
estimate.

This is somewhat conservative given that the image could've been
preloaded and be better compressed.

So it really only kicks in for high end connections that are known to
load fast.

In a follow up, we can add an additional wait for View Transitions that
does the same estimate but only for the images that turn out to be in
viewport.
2025-09-15 16:08:59 -04:00