## Summary
`FragmentInstance.addEventListener` and `removeEventListener` fail to
cross-match listeners when the `capture` option is passed as a
**boolean** in one call and an **options object** in the other. This
violates the [DOM Living
Standard](https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener),
which states that `addEventListener(type, fn, true)` and
`addEventListener(type, fn, {capture: true})` are identical.
### Root Cause
In `ReactFiberConfigDOM.js`, the `normalizeListenerOptions` function
generates a listener key string for deduplication. The boolean branch
generates a **different format** than the object branch:
```js
// Boolean branch (old) — produces "c=1"
return `c=${opts ? '1' : '0'}`;
// Object branch — produces "c=1&o=0&p=0"
return `c=${opts.capture ? '1' : '0'}&o=${opts.once ? '1' : '0'}&p=${opts.passive ? '1' : '0'}`;
```
Because the keys differ, `indexOfEventListener` cannot match them — so
`removeEventListener('click', fn, {capture: true})` silently fails to
remove a listener registered with `addEventListener('click', fn, true)`,
and vice versa. This causes a **memory leak and event listener
accumulation** on all Fragment child DOM nodes.
### Fix
Normalize the boolean branch to produce the same full key format:
```js
// Boolean branch (fixed) — now produces "c=1&o=0&p=0" (matches object branch)
return `c=${opts ? '1' : '0'}&o=0&p=0`;
```
This makes both forms produce an identical key, matching the DOM spec
behavior.
### When Was This Introduced
This bug has been present since `FragmentInstance` event listener
tracking was first added. It became reachable in production as of
[#36026](https://github.com/facebook/react/pull/36026) which enabled
`enableFragmentRefs` + `enableFragmentRefsInstanceHandles` across all
builds (merged 3 days ago).
### Tests
Added two new regression tests to `ReactDOMFragmentRefs-test.js`:
1. `removes a capture listener registered with boolean when removed with
options object`
2. `removes a capture listener registered with options object when
removed with boolean`
Both tests were failing before this fix and pass after.
## How did you test this change?
Added two new automated tests covering both cross-form removal
directions. Existing tests continue to pass.
## Changelog
### React DOM
- **Fixed** `FragmentInstance.removeEventListener()` not removing
capture-phase listeners when the `capture` option form (boolean vs
options object) differs between `add` and `remove` calls.
## Summary
Follow-up to #36148 (which added credentialless as a recognized boolean
attribute for iframes). Adds credentialless to possibleStandardNames so
React's dev warning can suggest the correct casing when users write it
as Credentialless (or another incorrect case). Includes an SSR test
asserting the "Did you mean credentialless?" warning fires.
## Test plan
- yarn test ReactDOMComponent passes, including the new should warn
about incorrect casing on the credentialless property (ssr) case
## Summary
The `credentialless` attribute is a boolean HTML attribute for
`<iframe>` elements that loads the iframe in a new, ephemeral context
without access to the parent's credentials (cookies, client
certificates, etc.). This change adds it to all boolean attribute
switch/case lists in React DOM so it is properly handled as a boolean
(set when true, removed when false) rather than being treated as an
unknown string attribute.
Per the [Anonymous iframe spec
(WICG)](https://wicg.github.io/anonymous-iframe/):
> The credentialless attribute enables loading documents hosted by the
iframe with a new and ephemeral storage partition. It is a boolean
value. The default is false.
```
partial interface HTMLIFrameElement {
attribute boolean credentialless;
};
```
Changes:
- ReactDOMComponent.js: Added to both `setProp` and
`diffHydratedGenericElement`
- ReactFizzConfigDOM.js: Added to `pushAttribute` for server-side
rendering
- ReactDOMUnknownPropertyHook.js: Added to both validation switch/case
lists
## Test plan
- Added unit test in DOMPropertyOperations-test.js verifying
`credentialless={true}` sets the attribute to `''` and
`credentialless={false}` removes it
- All tests pass in source and www channels (590 tests each)
- Flow type checking passes (dom-node renderer)
- Prettier and lint pass
<!--
Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.
Before submitting a pull request, please make sure the following is
done:
1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
10. If you haven't already, complete the CLA.
Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->
## Summary
This PR fixes a few small spelling errors in comments across the
codebase (`teh`→`the`, `occuring`→`occurring`, `occured`→`occurred`). No
behavior changes.
<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->
## How did you test this change?
<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
If you leave this empty, your PR will very likely be closed.
-->
This is a comments-only change. I verified the diff is limited to
comment text and does not affect logic or runtime behavior.
<!--
Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.
Before submitting a pull request, please make sure the following is
done:
1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
10. If you haven't already, complete the CLA.
Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->
## Summary
<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->
I just fixed typos as followings.
- `succesful` → `successful`
- `becuase` → `because`
- `enought` → `enough`
- `defualt` → `default`
## How did you test this change?
<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
If you leave this empty, your PR will very likely be closed.
-->
This PR only includes test case description, dummy strings for test, and
comments updates, so it has no impact on runtime behavior.
Therefore, I manually reviewed changed texts to ensure correctness.
I found two focus bugs when working on documentation for Fragment Refs.
1) If an element delegates focus handling, it will return false from
setFocusIfFocusable even though a focus event has occured on a different
element. The fix for this is a document level event listener rather than
only listening on the current element.
For example, if you have a form with multiple nested label>inputs.
Calling focus on the label will focus its input but not fire an event on
the label. setFocusIfFocusable returns false and you end up continuing
to attempt focus down the form tree.
2) If an element is already focused, setFocusIfFocusable will return
false. The fix for this is checking the document's activeElement with an
early return.
In the same form example, if the first input is already focused and you
call fragmentInstance.focus() at the form level, the second input would
end up getting focused since the focus event on the first is not
triggered.
https://github.com/facebook/react/pull/34935 Introduced
`unstable_reactFragments` handle on DOM nodes to enable caching of
Observers.
This has been tested in production and is stable so it can be rolled out
with the Fragment Refs feature.
When flushing the shell, stylesheets with precedence are emitted in the
`<head>` which blocks paint regardless. Outlining a boundary solely
because it has suspensey CSS provides no benefit during the shell flush
and causes a higher-level fallback to be shown unnecessarily (e.g.
"Middle Fallback" instead of "Inner Fallback").
This change passes a flushingInShell flag to hasSuspenseyContent so the
host config can skip stylesheet-only suspensey content when flushing the
shell. Suspensey images (used for ViewTransition animation reveals)
still trigger outlining during the shell since their motivation is
different.
When flushing streamed completions the behavior is unchanged — suspensey
CSS still causes outlining so the parent content can display sooner
while the stylesheet loads.
Follow up to https://github.com/facebook/react/pull/35630
We don't currently have any operations that depend on the updating of
text nodes added or removed after Fragment mount. But for the sake of
completeness and extending the ability to any other host configs, this
change calls `commitNewChildToFragmentInstance` and
`deleteChildFromFragmentInstance` on HostText fibers.
Both DOM and Fabric configs early return because we cannot attach event
listeners or observers to text. In the future, there could be some
stateful Fragment feature that uses text that could extend this.
This PR adds text node support to FragmentInstance operations, allowing
fragment refs to properly handle fragments that contain text nodes
(either mixed with elements or text-only).
Not currently adding/removing new text nodes as we don't need to track
them for events or observers in DOM. Will follow up on this and with
Fabric support.
## Support through parent element
- `dispatchEvent`
- `compareDocumentPosition`
- `getRootNode`
## Support through Range API
- `getClientRects`: Uses Range to calculate bounding rects for text
nodes
- `scrollIntoView`: Uses Range to scroll to text node positions directly
## No support
- `focus`/`focusLast`/`blur`: Noop for text-only fragments
- `observeUsing`: Warns for text-only fragments in DEV
- `addEventListener`/`removeEventListener`: Ignores text nodes, but
still works on Fragment level through `dispatchEvent`
Follow up to #35559.
The clean up function of the custom timeline doesn't necessarily clean
up the animation. Just the timeline's internal state.
This affects Firefox which doesn't support ScrollTimeline so uses the
polyfill's custom timeline.
Currently we always clone the root when a gesture transition happens.
The was to add an optimization where if a Transition could be isolated
to an absolutely positioned subtree then we could just clone that
subtree or just do a plain insertion if it was simple an Enter. That way
when switching between two absolutely positioned pages the shell
wouldn't need to be cloned. In that case `detectMutationOrInsertClones`
would return false. However, currently it always return true because we
don't yet have that optimization.
The idea was to warn when the root required cloning to ensure that you
optimize it intentionally since it's easy to accidentally update more
than necessary. However, since this is not yet actionable I'm removing
this warning for now.
Instead, I add a warning for particularly bad cases where you really
shouldn't clone like iframe and video. They may not be very actionable
without the optimization since you can't scope it down to a subtree
without the optimization. So if they're above the gesture then they're
always cloned atm. However, it might also be that it's unnecessary to
keep them mounted if they could be removed or hidden with Activity.
Stacked on #35556 and #35559.
Given that we don't automatically clean up all view transition
animations since #35337 and browsers are buggy, it's important that you
clean up any `Animation` started manually from the events. However,
there was no clean up function for when the View Transition is forced to
stop. This also makes it harder to clean up custom timers etc too.
This lets you return a clean up function from all the events on
`<ViewTransition>`.
Follow up to #35337.
During a gesture, we always cancel the original animation and create a
new one that we control. That's the one we need to add to the set that
needs to be cancelled. Otherwise future gestures hang.
An unfortunate consequence is that any custom ones that you start e.g.
with #35556 or through other means aren't automatically cleaned up (in
fact there's not even a clean up callback yet). This can lead these to
freeze the whole UI afterwards. It would be really good to get this
fixed in browsers instead so we can revert #35337.
When the Fizz runtime runs a view-transition we apply
`view-transition-name` and `view-transition-class` to the `style`. These
can be observed by Fiber when hydrating which incorrectly leads to
hydration errors.
More over, even after we remove them, the `style` attribute has now been
normalized which we are unable to diff because we diff against the SSR
generated `style` attribute string and not the normalized form. So if
there are other inline styles defined, we have to skip diffing them in
this scenario.
Continue attaching `internalInstanceKey` to DOM nodes in DEV. This
prevents breaking some internal dev tooling while we experiment with the
broader change. Note that this does not reference the DOM handle within
the flag, just attaches it and deletes it. Internals tracking is still
done through the private map.
Speculative fix to https://github.com/facebook/react/issues/35336
written by Claude.
I have verified that applying a similar patch locally to the repro from
#35336 does fix the crash.
I'm not familiar enough with the underlying APIs to tell whether the fix
is correct or sufficient.
We already append `randomKey` to each handle name to prevent external
libraries from accessing and relying on these internals. But more
libraries recently have been getting around this by simply iterating
over the element properties and using a `startsWith` check.
This flag allows us to experiment with moving these handles to an
internal map.
This PR starts with the two most common internals, the props object and
the fiber. We can consider moving additional properties such as the
container root and others depending on perf results.
This is an alternative to #35059.
If the name needs escaping, then instead of escaping it, we just use a
base64 name. This wouldn't allow you to match on an escaped name in your
own CSS like you should be able to if browsers worked properly. But at
least it would provide matching name in current browsers which is
probably sufficient if you're using auto-generated names.
This also covers some cases where `CSS.escape()` isn't sufficient anyway
like when the name ends in a dot.
This PR adds a `unstable_reactFragments?: Set<FragmentInstance>`
property to DOM nodes that belong to a Fragment with a ref (top level
host components). This allows you to access a FragmentInstance from a
DOM node.
This is flagged behind `enableFragmentRefsInstanceHandles`.
The primary use case to unblock is reusing IntersectionObserver
instances. A fairly common practice is to cache and reuse
IntersectionObservers that share the same config, with a map of
node->callbacks to run for each entry in the IO callback. Currently this
is not possible with Fragment Ref `observeUsing` because the key in the
cache would have to be the `FragmentInstance` and you can't find it
without a handle from the node. This works now by accessing
`entry.target.fragments`.
This also opens up possibilities to use `FragmentInstance` operations in
other places, such as events. We can do
`event.target.unstable_reactFragments`, then access
`fragmentInstance.getClientRects` for example. In a future PR, we can
assign an event's `currentTarget` as the Fragment Ref for a more direct
handle when the event has been dispatched by the Fragment itself.
The first commit here implemented a handle only on observed elements.
This is awkward because there isn't a good way to document or expose
this temporary property. `element.fragments` is closer to what we would
expect from a DOM API if a standard was implemented here. And by
assigning it to all top-level nodes of a Fragment, it can be used beyond
the cached IntersectionObserver callback.
One tradeoff here is adding extra work during the creation of
FragmentInstances as well as keeping track of adding/removing nodes.
Previously we only track the Fiber on creation but here we add a
traversal which could apply to a large set of top-level host children.
The `element.unstable_reactFragments` Set can also be randomly ordered.
## Overview
This PR adds the `ref` prop to `<Fragment>` in `react@canary`.
This means this API is ready for final feedback and prepared for a
semver stable release.
## What this means
Shipping Fragment refs to canary means they have gone through extensive
testing in production, we are confident in the stability of the APIs,
and we are preparing to release it in a future semver stable version.
Libraries and frameworks following the [Canary
Workflow](https://react.dev/blog/2023/05/03/react-canaries) should begin
implementing and testing these features.
## Why we follow the Canary Workflow
To prepare for semver stable, libraries should test canary features like
Fragment refs with `react@canary` to confirm compatibility and prepare
for the next semver release in a myriad of environments and
configurations used throughout the React ecosystem. This provides
libraries with ample time to catch any issues we missed before slamming
them with problems in the wider semver release.
Since these features have already gone through extensive production
testing, and we are confident they are stable, frameworks following the
[Canary Workflow](https://react.dev/blog/2023/05/03/react-canaries) can
also begin adopting canary features like Fragment refs.
This adoption is similar to how different Browsers implement new
proposed browser features before they are added to the standard. If a
frameworks adopts a canary feature, they are committing to stability for
their users by ensuring any API changes before a semver stable release
are opaque and non-breaking to their users.
Apps not using a framework are also free to adopt canary features like
Fragment refs as long as they follow the [Canary
Workflow](https://react.dev/blog/2023/05/03/react-canaries), but we
generally recommend waiting for a semver stable release unless you have
the capacity to commit to following along with the canary changes and
debugging library compatibility issues.
Waiting for semver stable means you're able to benefit from libraries
testing and confirming support, and use semver as signal for which
version of a library you can use with support of the feature.
## Docs
Check out the ["React Labs: View Transitions, Activity, and
more"](https://react.dev/blog/2025/04/23/react-labs-view-transitions-activity-and-more#fragment-refs)
blog post, and [the new docs for Fragment
refs`](https://react.dev/reference/react/Fragment#fragmentinstance) for
more info.
The View Transition docs were unclear about this but apparently the
`finished` promise never settles if the animation never started. So if
there's an error that rejects the `ready` promise, we'll never run the
clean up which can cause it to stall.
Fixes#34662.
However, ultimately that is caused by Chrome stalling our default
`onDefaultTransitionIndicator` but it should be unblocked after 10
seconds, not a minute.
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>
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.
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.
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"
/>
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.
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"
/>
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"
/>
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).
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.
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.
Currently suspensey images doesn't account for how long we've already
been waiting. This means that you can for example wait for 300ms for the
throttle + 500ms for the images. If a Transition takes a while to
complete you can also wait that time + an additional 500ms for the
images.
This tracks the start time of a Transition so that we can count the
timeout starting from when the user interacted or when the last fallback
committed (which is where the 300ms throttle is computed from). Creating
a single timeline.
This also moves the timeout to a central place which I'll use in a
follow up.
Fixes#34098.
There's an issue in Chrome where the `InvalidStateError` always has the
same error message. The spec doesn't specify the error message to use
but it's more useful to have a specific one for each case like Safari
does.
One reason it's better to have a specific error message is because the
browser console is not the main surface that people look for errors.
Chrome relies on a separate log also in the console. Frameworks has
built-in error dialogs that pop up first and that's where you see the
error and that dialog can't show something specific. Additionally, these
errors can't log something specific to servers in production logging. So
this is a bad strategy.
It's not good to have those error dialogs pop up for non-actionable
errors like when it doesn't start because the document was hidden. Since
we don't have more specific information we have no choice but to hide
all of them. This includes actionable things like duplicate names
(although we also have a React specific warning for that in the common
case).
This adds `experimental_scrollIntoView(alignToTop)`. It doesn't yet
support `scrollIntoView(options)`.
Cases:
- No host children: Without host children, we represent the virtual
space of the Fragment by attempting to scroll to the nearest edge by
using its siblings. If the preferred sibling is not found, we'll try the
other side, and then the parent.
- 1 or more host children: In order to handle the case of children
spread between multiple scroll containers, we scroll to each child in
reverse order based on the `alignToTop` flag.
Due to the complexity of multiple scroll containers and dealing with
portals, I've added this under a separate feature flag with an
experimental prefix. We may stabilize it along with the other APIs, but
this allows us to not block the whole feature on it.
This PR was previously implementing a much more complex approach to
handling multiple scroll containers and portals. We're going to start
with the simple loop and see if we can find any concrete use cases where
that doesn't suffice. 01f31d43013ba7f6f54fd8a36990bbafc3c3cc68 is the
diff between approaches here.