Skip to content

Commit 86aff4c

Browse files
committed
Images that can be suspended on should load as early as possible during SSR. We now track whether a rendered img is "suspensey" and emit a corresponding preload behind fonts in priority.
1 parent c8add69 commit 86aff4c

File tree

2 files changed

+129
-11
lines changed

2 files changed

+129
-11
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2367,6 +2367,67 @@ function pushStyleContents(
23672367
return;
23682368
}
23692369

2370+
function getImagePreloadKey(
2371+
href: string,
2372+
imageSrcSet: ?string,
2373+
imageSizes: ?string,
2374+
) {
2375+
let uniquePart = '';
2376+
if (typeof imageSrcSet === 'string' && imageSrcSet !== '') {
2377+
uniquePart += '[' + imageSrcSet + ']';
2378+
if (typeof imageSizes === 'string') {
2379+
uniquePart += '[' + imageSizes + ']';
2380+
}
2381+
} else {
2382+
uniquePart += '[][]' + href;
2383+
}
2384+
return getResourceKey('image', uniquePart);
2385+
}
2386+
2387+
function pushImg(
2388+
target: Array<Chunk | PrecomputedChunk>,
2389+
props: Object,
2390+
resources: Resources,
2391+
): null {
2392+
if (props.loading !== 'lazy' && typeof props.src === 'string') {
2393+
// We have a suspensey image and ought to preload it to optimize the loading of display blocking
2394+
// resources.
2395+
const {src, imageSrcSet, imageSizes} = props;
2396+
const key = getImagePreloadKey(src, imageSrcSet, imageSizes);
2397+
let resource = resources.preloadsMap.get(key);
2398+
if (!resource) {
2399+
resource = {
2400+
type: 'preload',
2401+
chunks: [],
2402+
state: NoState,
2403+
props: {
2404+
rel: 'preload',
2405+
as: 'image',
2406+
// There is a bug in Safari where imageSrcSet is not respected on preload links
2407+
// so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2408+
// This harms older browers that do not support imageSrcSet by making their preloads not work
2409+
// but this population is shrinking fast and is already small so we accept this tradeoff.
2410+
href: imageSrcSet ? undefined : src,
2411+
imageSrcSet,
2412+
imageSizes,
2413+
crossOrigin: props.crossOrigin,
2414+
integrity: props.integrity,
2415+
type: props.type,
2416+
fetchPriority: props.fetchPriority,
2417+
referrerPolicy: props.referrerPolicy,
2418+
},
2419+
};
2420+
resources.preloadsMap.set(key, resource);
2421+
if (__DEV__) {
2422+
markAsRenderedResourceDEV(resource, props);
2423+
}
2424+
pushLinkImpl(resource.chunks, resource.props);
2425+
}
2426+
resources.suspenseyImages.add(resource);
2427+
}
2428+
return pushSelfClosing(target, props, 'img');
2429+
}
2430+
23702431
function pushSelfClosing(
23712432
target: Array<Chunk | PrecomputedChunk>,
23722433
props: Object,
@@ -3169,14 +3230,16 @@ export function pushStartInstance(
31693230
case 'pre': {
31703231
return pushStartPreformattedElement(target, props, type);
31713232
}
3233+
case 'img': {
3234+
return pushImg(target, props, resources);
3235+
}
31723236
// Omitted close tags
31733237
case 'base':
31743238
case 'area':
31753239
case 'br':
31763240
case 'col':
31773241
case 'embed':
31783242
case 'hr':
3179-
case 'img':
31803243
case 'keygen':
31813244
case 'param':
31823245
case 'source':
@@ -4239,6 +4302,9 @@ export function writePreamble(
42394302
resources.fontPreloads.forEach(flushResourceInPreamble, destination);
42404303
resources.fontPreloads.clear();
42414304

4305+
resources.suspenseyImages.forEach(flushResourceInPreamble, destination);
4306+
resources.suspenseyImages.clear();
4307+
42424308
// Flush unblocked stylesheets by precedence
42434309
resources.precedences.forEach(flushAllStylesInPreamble, destination);
42444310

@@ -4305,6 +4371,9 @@ export function writeHoistables(
43054371
resources.fontPreloads.forEach(flushResourceLate, destination);
43064372
resources.fontPreloads.clear();
43074373

4374+
resources.suspenseyImages.forEach(flushResourceInPreamble, destination);
4375+
resources.suspenseyImages.clear();
4376+
43084377
// Preload any stylesheets. these will emit in a render instruction that follows this
43094378
// but we want to kick off preloading as soon as possible
43104379
resources.precedences.forEach(preloadLateStyles, destination);
@@ -4856,6 +4925,7 @@ export type Resources = {
48564925
// Flushing queues for Resource dependencies
48574926
preconnects: Set<PreconnectResource>,
48584927
fontPreloads: Set<PreloadResource>,
4928+
suspenseyImages: Set<PreloadResource>,
48594929
// usedImagePreloads: Set<PreloadResource>,
48604930
precedences: Map<string, Set<StyleResource>>,
48614931
stylePrecedences: Map<string, StyleTagResource>,
@@ -4880,6 +4950,7 @@ export function createResources(): Resources {
48804950
// cleared on flush
48814951
preconnects: new Set(),
48824952
fontPreloads: new Set(),
4953+
suspenseyImages: new Set(),
48834954
// usedImagePreloads: new Set(),
48844955
precedences: new Map(),
48854956
stylePrecedences: new Map(),
@@ -5083,16 +5154,7 @@ export function preload(href: string, options: PreloadOptions) {
50835154
// both. This is to prevent identical calls with the same srcSet and sizes to be duplicated
50845155
// by varying the href. this is an edge case but it is the most correct behavior.
50855156
const {imageSrcSet, imageSizes} = options;
5086-
let uniquePart = '';
5087-
if (typeof imageSrcSet === 'string' && imageSrcSet !== '') {
5088-
uniquePart += '[' + imageSrcSet + ']';
5089-
if (typeof imageSizes === 'string') {
5090-
uniquePart += '[' + imageSizes + ']';
5091-
}
5092-
} else {
5093-
uniquePart += '[][]' + href;
5094-
}
5095-
key = getResourceKey(as, uniquePart);
5157+
key = getImagePreloadKey(href, imageSrcSet, imageSizes);
50965158
} else {
50975159
key = getResourceKey(as, href);
50985160
}

packages/react-dom/src/__tests__/ReactDOMFloat-test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3771,6 +3771,62 @@ body {
37713771
);
37723772
});
37733773

3774+
it('can emit preloads for non-lazy images that are rendered', async () => {
3775+
function App() {
3776+
ReactDOM.preload('script', {as: 'script'});
3777+
ReactDOM.preload('a', {as: 'image'});
3778+
ReactDOM.preload('b', {as: 'image'});
3779+
return (
3780+
<html>
3781+
<body>
3782+
<img src="a" />
3783+
<img src="b" loading="lazy" />
3784+
<img src="b2" loading="lazy" />
3785+
<img src="c" imageSrcSet="srcsetc" />
3786+
<img src="d" imageSrcSet="srcsetd" imageSizes="sizesd" />
3787+
<img src="d" imageSrcSet="srcsetd" imageSizes="sizesd2" />
3788+
</body>
3789+
</html>
3790+
);
3791+
}
3792+
3793+
await act(() => {
3794+
renderToPipeableStream(<App />).pipe(writable);
3795+
});
3796+
3797+
// non-lazy images are first, then arbitrary preloads like for the script and lazy images
3798+
expect(getMeaningfulChildren(document)).toEqual(
3799+
<html>
3800+
<head>
3801+
<link rel="preload" href="a" as="image" />
3802+
<link rel="preload" as="image" imagesrcset="srcsetc" />
3803+
<link
3804+
rel="preload"
3805+
as="image"
3806+
imagesrcset="srcsetd"
3807+
imagesizes="sizesd"
3808+
/>
3809+
<link
3810+
rel="preload"
3811+
as="image"
3812+
imagesrcset="srcsetd"
3813+
imagesizes="sizesd2"
3814+
/>
3815+
<link rel="preload" href="script" as="script" />
3816+
<link rel="preload" href="b" as="image" />
3817+
</head>
3818+
<body>
3819+
<img src="a" />
3820+
<img src="b" loading="lazy" />
3821+
<img src="b2" loading="lazy" />
3822+
<img src="c" imagesrcset="srcsetc" />
3823+
<img src="d" imagesrcset="srcsetd" imagesizes="sizesd" />
3824+
<img src="d" imagesrcset="srcsetd" imagesizes="sizesd2" />
3825+
</body>
3826+
</html>,
3827+
);
3828+
});
3829+
37743830
describe('ReactDOM.prefetchDNS(href)', () => {
37753831
it('creates a dns-prefetch resource when called', async () => {
37763832
function App({url}) {

0 commit comments

Comments
 (0)