Skip to content

Commit 5a1f45d

Browse files
committed
Models image resources as their own type to encode the "promotion" behavior when an img tag is rendered and we promote the priority
1 parent d2cb6fe commit 5a1f45d

File tree

2 files changed

+178
-58
lines changed

2 files changed

+178
-58
lines changed

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

Lines changed: 97 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ export type ResumableState = {
228228
anonymous: {[key: string]: Exists},
229229
credentials: {[key: string]: Exists},
230230
},
231+
imageResources: {
232+
[key: string]: Preloaded,
233+
},
231234
styleResources: {
232235
[key: string]: Exists | Preloaded | PreloadedWithCredentials,
233236
},
@@ -566,6 +569,7 @@ export function createResumableState(
566569
anonymous: {},
567570
credentials: {},
568571
},
572+
imageResources: {},
569573
styleResources: {},
570574
scriptResources: {},
571575
moduleUnknownResources: {},
@@ -2588,43 +2592,58 @@ function pushImg(
25882592
// resumableState.
25892593
const sizes = typeof props.sizes === 'string' ? props.sizes : undefined;
25902594
const key = getImageResourceKey(src, srcSet, sizes);
2591-
const resources: ResumableState['unknownResources']['asType'] =
2592-
resumableState.unknownResources.hasOwnProperty('image')
2593-
? resumableState.unknownResources.image
2594-
: (resumableState.unknownResources.image = {});
25952595

2596-
let resource: void | Resource;
2597-
if (!resources.hasOwnProperty(key)) {
2598-
const preloadProps: PreloadProps = {
2599-
rel: 'preload',
2600-
as: 'image',
2601-
// There is a bug in Safari where imageSrcSet is not respected on preload links
2602-
// so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2603-
// This harms older browers that do not support imageSrcSet by making their preloads not work
2604-
// but this population is shrinking fast and is already small so we accept this tradeoff.
2605-
href: srcSet ? undefined : src,
2606-
imageSrcSet: srcSet,
2607-
imageSizes: sizes,
2608-
crossOrigin: props.crossOrigin,
2609-
integrity: props.integrity,
2610-
type: props.type,
2611-
fetchPriority: props.fetchPriority,
2612-
referrerPolicy: props.referrerPolicy,
2613-
};
2614-
resource = [];
2615-
resources[key] = PRELOAD_SIGIL;
2616-
pushLinkImpl(resource, preloadProps);
2617-
} else {
2618-
resource = renderState.preloads.images.get(key);
2619-
}
2596+
const promotablePreloads = renderState.preloads.images;
2597+
2598+
let resource = promotablePreloads.get(key);
26202599
if (resource) {
2600+
// We consider whether this preload can be promoted to higher priority flushing queue.
2601+
// The only time a resource will exist here is if it was created during this render
2602+
// and was not already in the high priority queue.
2603+
if (
2604+
props.fetchPriority === 'high' ||
2605+
renderState.highImagePreloads.size < 10
2606+
) {
2607+
// Delete the resource from the map since we are promoting it and don't want to
2608+
// reenter this branch in a second pass for duplicate img hrefs.
2609+
promotablePreloads.delete(key);
2610+
2611+
// $FlowFixMe - Flow should understand that this is a Resource if the condition was true
2612+
renderState.highImagePreloads.add(resource);
2613+
}
2614+
} else if (!resumableState.imageResources.hasOwnProperty(key)) {
2615+
// We must construct a new preload resource
2616+
resumableState.imageResources[key] = PRELOAD_SIGIL;
2617+
resource = [];
2618+
pushLinkImpl(
2619+
resource,
2620+
({
2621+
rel: 'preload',
2622+
as: 'image',
2623+
// There is a bug in Safari where imageSrcSet is not respected on preload links
2624+
// so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2625+
// This harms older browers that do not support imageSrcSet by making their preloads not work
2626+
// but this population is shrinking fast and is already small so we accept this tradeoff.
2627+
href: srcSet ? undefined : src,
2628+
imageSrcSet: srcSet,
2629+
imageSizes: sizes,
2630+
crossOrigin: props.crossOrigin,
2631+
integrity: props.integrity,
2632+
type: props.type,
2633+
fetchPriority: props.fetchPriority,
2634+
referrerPolicy: props.referrerPolicy,
2635+
}: PreloadProps),
2636+
);
26212637
if (
26222638
props.fetchPriority === 'high' ||
26232639
renderState.highImagePreloads.size < 10
26242640
) {
26252641
renderState.highImagePreloads.add(resource);
26262642
} else {
26272643
renderState.bulkPreloads.add(resource);
2644+
// We can bump the priority up if the same img is rendered later
2645+
// with fetchPriority="high"
2646+
promotablePreloads.set(key, resource);
26282647
}
26292648
}
26302649
}
@@ -5057,8 +5076,8 @@ function getResourceKey(href: string): string {
50575076

50585077
function getImageResourceKey(
50595078
href: string,
5060-
imageSrcSet: ?string,
5061-
imageSizes: ?string,
5079+
imageSrcSet?: ?string,
5080+
imageSizes?: ?string,
50625081
): string {
50635082
if (imageSrcSet) {
50645083
return imageSrcSet + '\n' + (imageSizes || '');
@@ -5147,20 +5166,48 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
51475166
const resumableState = getResumableState(request);
51485167
const renderState = getRenderState(request);
51495168
if (as && href) {
5150-
options = options || {};
5151-
let key: string;
5152-
5153-
if (as === 'image') {
5154-
// For image preloads the key contains either the imageSrcSet + imageSizes or the href but not
5155-
// both. This is to prevent identical calls with the same srcSet and sizes to be duplicated
5156-
// by varying the href. this is an edge case but it is the most correct behavior.
5157-
key = getImageResourceKey(href, options.imageSrcSet, options.imageSizes);
5158-
} else {
5159-
key = getResourceKey(href);
5160-
}
5161-
51625169
switch (as) {
5170+
case 'image': {
5171+
let imageSrcSet, imageSizes, fetchPriority;
5172+
if (options) {
5173+
imageSrcSet = options.imageSrcSet;
5174+
imageSizes = options.imageSizes;
5175+
fetchPriority = options.fetchPriority;
5176+
}
5177+
const key = getImageResourceKey(href, imageSrcSet, imageSizes);
5178+
if (resumableState.imageResources.hasOwnProperty(key)) {
5179+
// we can return if we already have this resource
5180+
return;
5181+
}
5182+
resumableState.imageResources[key] = PRELOAD_SIGIL;
5183+
const resource = ([]: Resource);
5184+
pushLinkImpl(
5185+
resource,
5186+
Object.assign(
5187+
({
5188+
rel: 'preload',
5189+
// There is a bug in Safari where imageSrcSet is not respected on preload links
5190+
// so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
5191+
// This harms older browers that do not support imageSrcSet by making their preloads not work
5192+
// but this population is shrinking fast and is already small so we accept this tradeoff.
5193+
href: imageSrcSet ? undefined : href,
5194+
as,
5195+
}: PreloadAsProps),
5196+
options,
5197+
),
5198+
);
5199+
if (fetchPriority === 'high') {
5200+
renderState.highImagePreloads.add(resource);
5201+
} else {
5202+
renderState.bulkPreloads.add(resource);
5203+
// Stash the resource in case we need to promote it to higher priority
5204+
// when an img tag is rendered
5205+
renderState.preloads.images.set(key, resource);
5206+
}
5207+
break;
5208+
}
51635209
case 'style': {
5210+
const key = getResourceKey(href);
51645211
if (resumableState.styleResources.hasOwnProperty(key)) {
51655212
// we can return if we already have this resource
51665213
return;
@@ -5171,15 +5218,17 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
51715218
Object.assign(({rel: 'preload', href, as}: PreloadAsProps), options),
51725219
);
51735220
resumableState.styleResources[key] =
5174-
typeof options.crossOrigin === 'string' ||
5175-
typeof options.integrity === 'string'
5221+
options &&
5222+
(typeof options.crossOrigin === 'string' ||
5223+
typeof options.integrity === 'string')
51765224
? [options.crossOrigin, options.integrity]
51775225
: PRELOAD_SIGIL;
51785226
renderState.preloads.stylesheets.set(key, resource);
51795227
renderState.bulkPreloads.add(resource);
51805228
break;
51815229
}
51825230
case 'script': {
5231+
const key = getResourceKey(href);
51835232
if (resumableState.scriptResources.hasOwnProperty(key)) {
51845233
// we can return if we already have this resource
51855234
return;
@@ -5192,13 +5241,15 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
51925241
Object.assign(({rel: 'preload', href, as}: PreloadAsProps), options),
51935242
);
51945243
resumableState.scriptResources[key] =
5195-
typeof options.crossOrigin === 'string' ||
5196-
typeof options.integrity === 'string'
5244+
options &&
5245+
(typeof options.crossOrigin === 'string' ||
5246+
typeof options.integrity === 'string')
51975247
? [options.crossOrigin, options.integrity]
51985248
: PRELOAD_SIGIL;
51995249
break;
52005250
}
52015251
default: {
5252+
const key = getResourceKey(href);
52025253
const hasAsType = resumableState.unknownResources.hasOwnProperty(as);
52035254
let resources;
52045255
if (hasAsType) {
@@ -5224,18 +5275,6 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
52245275
case 'font':
52255276
renderState.fontPreloads.add(resource);
52265277
break;
5227-
case 'image':
5228-
renderState.preloads.images.set(key, resource);
5229-
if (options.imageSrcSet) {
5230-
// There is a bug in Safari where imageSrcSet is not respected on preload links
5231-
// so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
5232-
// This harms older browers that do not support imageSrcSet by making their preloads not work
5233-
// but this population is shrinking fast and is already small so we accept this tradeoff.
5234-
props.href = null;
5235-
}
5236-
if (options.fetchPriority === 'high') {
5237-
renderState.highImagePreloads.add(resource);
5238-
}
52395278
// intentional fall through
52405279
default:
52415280
renderState.bulkPreloads.add(resource);

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4014,6 +4014,87 @@ body {
40144014
);
40154015
});
40164016

4017+
it('can promote images to high priority when at least one instance specifies a high fetchPriority', async () => {
4018+
function App() {
4019+
// If a ends up in a higher priority queue than b it will flush first
4020+
ReactDOM.preload('a', {as: 'image'});
4021+
ReactDOM.preload('b', {as: 'image'});
4022+
return (
4023+
<html>
4024+
<body>
4025+
<link rel="stylesheet" href="foo" precedence="default" />
4026+
<img src="1" />
4027+
<img src="2" />
4028+
<img src="3" />
4029+
<img src="4" />
4030+
<img src="5" />
4031+
<img src="6" />
4032+
<img src="7" />
4033+
<img src="8" />
4034+
<img src="9" />
4035+
<img src="10" />
4036+
<img src="11" />
4037+
<img src="12" />
4038+
<img src="a" fetchPriority="low" />
4039+
<img src="a" />
4040+
<img src="a" fetchPriority="high" />
4041+
<img src="a" />
4042+
<img src="a" />
4043+
</body>
4044+
</html>
4045+
);
4046+
}
4047+
4048+
await act(() => {
4049+
renderToPipeableStream(<App />).pipe(writable);
4050+
});
4051+
expect(getMeaningfulChildren(document)).toEqual(
4052+
<html>
4053+
<head>
4054+
{/* The First 10 high priority images were just the first 10 rendered images */}
4055+
<link rel="preload" as="image" href="1" />
4056+
<link rel="preload" as="image" href="2" />
4057+
<link rel="preload" as="image" href="3" />
4058+
<link rel="preload" as="image" href="4" />
4059+
<link rel="preload" as="image" href="5" />
4060+
<link rel="preload" as="image" href="6" />
4061+
<link rel="preload" as="image" href="7" />
4062+
<link rel="preload" as="image" href="8" />
4063+
<link rel="preload" as="image" href="9" />
4064+
<link rel="preload" as="image" href="10" />
4065+
{/* The "a" image was rendered a few times but since at least one of those was with
4066+
fetchPriorty="high" it ends up in the high priority queue */}
4067+
<link rel="preload" as="image" href="a" />
4068+
{/* Stylesheets come in between high priority images and regular preloads */}
4069+
<link rel="stylesheet" href="foo" data-precedence="default" />
4070+
{/* The remainig images that preloaded at regular priority */}
4071+
<link rel="preload" as="image" href="b" />
4072+
<link rel="preload" as="image" href="11" />
4073+
<link rel="preload" as="image" href="12" />
4074+
</head>
4075+
<body>
4076+
<img src="1" />
4077+
<img src="2" />
4078+
<img src="3" />
4079+
<img src="4" />
4080+
<img src="5" />
4081+
<img src="6" />
4082+
<img src="7" />
4083+
<img src="8" />
4084+
<img src="9" />
4085+
<img src="10" />
4086+
<img src="11" />
4087+
<img src="12" />
4088+
<img src="a" fetchpriority="low" />
4089+
<img src="a" />
4090+
<img src="a" fetchpriority="high" />
4091+
<img src="a" />
4092+
<img src="a" />
4093+
</body>
4094+
</html>,
4095+
);
4096+
});
4097+
40174098
it('preloads from rendered images properly use srcSet and sizes', async () => {
40184099
function App() {
40194100
ReactDOM.preload('1', {as: 'image', imageSrcSet: 'ss1'});

0 commit comments

Comments
 (0)