Skip to content

Commit 046f17d

Browse files
Clarify image loading during View Transitions (#8671)
* Clarify img View Transition example * Avoid redundant image alt text * Tighten img example introduction * Move img example outcome below demo * Clarify Suspense image example * Simplify View Transition comparison examples * Use a Suspense load in the image example * Align Suspense resource loading examples * Reduce View Transition example changes * Remove artificial image loading delay * Keep Suspense explanation self-contained * Scope shared examples to each API * Use Suspense for View Transition image example * Make image loading comparison visible * Tighten View Transition image examples * Document View Transition resource timeout * Cache image promises in Suspense examples * Fix Suspense resource demos * Clarify View Transition image reveal * Fix Suspense font loading example * Align image loading explanations * Address image documentation review * Simplify View Transition image guidance
1 parent 8c68ae8 commit 046f17d

3 files changed

Lines changed: 155 additions & 132 deletions

File tree

‎src/content/reference/react-dom/components/img.md‎

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ To display an image, render the [built-in browser `<img>` component](https://dev
5151

5252
* Do not pass an empty string to `src`. It may cause the browser to request the current page again. React warns in development and omits the attribute. To render no image, omit the `<img>` or pass `null` to `src`.
5353
* `<img>` cannot have children or use `dangerouslySetInnerHTML`. React throws an error if you pass either.
54-
* `fetchPriority="low"` does not stop React from waiting for the image to load and decode during a client-rendered View Transition update. Use `loading="lazy"` or an `onLoad` handler to opt out of that behavior.
54+
* `fetchPriority="low"` does not stop React from waiting for the image to load during a client-rendered View Transition update. Use `loading="lazy"` or an `onLoad` handler to opt out of that behavior.
5555

5656
---
5757

@@ -125,25 +125,26 @@ To create an explicit preload hint, call [`preload`](/reference/react-dom/preloa
125125

126126
### Waiting for an image during a View Transition {/*waiting-for-an-image-during-a-view-transition*/}
127127

128-
During a client-rendered [`<ViewTransition>`](/reference/react/ViewTransition) update, React may wait for an image to load and decode before starting the animation. This applies when a new `<img>` with a non-empty `src` is rendered, or when an existing image's `src` or `srcSet` changes. The image must be inside the `<ViewTransition>` subtree and must not have `loading="lazy"` or an `onLoad` handler. React does not wait for images during synchronous updates.
128+
During a [`<ViewTransition>`](/reference/react/ViewTransition) update, React waits up to 500 ms for visible images to load before starting the animation. This includes newly rendered `<img>` elements and existing images whose `src` or `srcSet` changes. Setting `loading="lazy"` or adding an `onLoad` handler opts an image out.
129129

130-
When a Suspense boundary reveals streamed content inside a `<ViewTransition>`, React may also wait for visible images with a non-empty `src` that do not have `loading="lazy"`. React stops waiting after a timeout so that a slow image does not block the update indefinitely.
131-
132-
In this example, the Suspense boundary is wrapped in a `<ViewTransition>` and shows a profile skeleton until the portrait has loaded.
133-
134-
For comparison, the second button inserts the same card directly into the DOM. The card appears immediately, and the browser displays the image after it loads:
130+
Compare how the same image appears when a Suspense boundary reveals its content inside and outside a `<ViewTransition>`:
135131

136132
<Sandpack>
137133

138134
```js
139-
import { ViewTransition, Suspense, useState, startTransition } from 'react';
140-
import { freshImageUrl } from './image.js';
141-
import VanillaProfile from './VanillaProfile.js';
142-
143-
function Profile({ src }) {
135+
import {
136+
ViewTransition,
137+
Suspense,
138+
use,
139+
useState,
140+
} from 'react';
141+
import { fetchImageSrc } from './image.js';
142+
143+
function Profile({ cacheKey }) {
144+
const src = use(fetchImageSrc(cacheKey));
144145
return (
145146
<div className="card">
146-
<img src={src} alt="Jack Pope" width={80} height={80} />
147+
<img src={src} alt="" width={80} height={80} />
147148
<p>Jack Pope</p>
148149
</div>
149150
);
@@ -159,56 +160,51 @@ function ProfilePlaceholder() {
159160
}
160161

161162
export default function App() {
162-
const [src, setSrc] = useState(null);
163+
const [showWithTransition, setShowWithTransition] =
164+
useState(false);
165+
const [showWithoutTransition, setShowWithoutTransition] =
166+
useState(false);
163167
return (
164168
<>
165-
<button
166-
onClick={() => {
167-
startTransition(() => {
168-
setSrc(freshImageUrl());
169-
});
170-
}}>
171-
Show profile
169+
<button onClick={() => setShowWithTransition(true)}>
170+
Show profile with View Transition
172171
</button>
173-
{src && (
172+
{showWithTransition && (
174173
<ViewTransition>
175174
<Suspense fallback={<ProfilePlaceholder />}>
176-
<Profile src={src} />
175+
<Profile cacheKey="with-transition" />
177176
</Suspense>
178177
</ViewTransition>
179178
)}
180179
<hr />
181-
<VanillaProfile />
180+
<button onClick={() => setShowWithoutTransition(true)}>
181+
Show profile without View Transition
182+
</button>
183+
{showWithoutTransition && (
184+
<Suspense fallback={<ProfilePlaceholder />}>
185+
<Profile cacheKey="without-transition" />
186+
</Suspense>
187+
)}
182188
</>
183189
);
184190
}
185191
```
186192

187-
```js src/VanillaProfile.js
188-
import { useRef } from 'react';
189-
import { freshImageUrl } from './image.js';
193+
```js src/image.js hidden
194+
// Normally, the caching logic would be inside a framework.
195+
const cache = new Map();
190196

191-
export default function VanillaProfile() {
192-
const ref = useRef(null);
193-
function show() {
194-
ref.current.innerHTML = `<div class="card">
195-
<img src="${freshImageUrl()}" alt="Jack Pope" width="80" height="80" />
196-
<p>Jack Pope</p>
197-
</div>`;
197+
export function fetchImageSrc(cacheKey) {
198+
if (!cache.has(cacheKey)) {
199+
cache.set(cacheKey, loadImageSrc());
198200
}
199-
return (
200-
<>
201-
<button onClick={show}>Show profile (direct DOM update)</button>
202-
<div ref={ref} />
203-
</>
204-
);
201+
return cache.get(cacheKey);
205202
}
206-
```
207203

208-
```js src/image.js hidden
209-
// Add a unique parameter so the image isn't cached,
210-
// and every run shows the loading state.
211-
export function freshImageUrl() {
204+
async function loadImageSrc() {
205+
// Delay the response so the Suspense fallback is visible.
206+
await new Promise(resolve => setTimeout(resolve, 1000));
207+
// Add a unique parameter so the image isn't cached.
212208
return 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now();
213209
}
214210
```
@@ -255,3 +251,5 @@ hr {
255251
```
256252

257253
</Sandpack>
254+
255+
With `<ViewTransition>`, React keeps the skeleton visible for up to 500 ms while the image loads, so the card can be revealed with its image already in place. Without `<ViewTransition>`, Suspense stops showing the skeleton as soon as the Promise resolves. If the image is still loading, the card appears first and the image pops in afterward.

0 commit comments

Comments
 (0)