Skip to content

Commit 8d8bcde

Browse files
committed
extends image preloading to accept imagesrcset and imagesizes to handle cases where srcset and sizes are used over src. Normally we key off an href however in the case of responsive images it is possible that no src attribute is desired. Right now this happens most prominently with Safari which does understand srcset for the img tag but not for the link preload tag. Because of this it can be detrimental to preload with a fallback src since Safari will end up downloading the wrong image 100% of the time.
The solution to this is to allow the user to omit the href if they provide imagesrcset (and optionally) imagesizes. Effectively the key for image preloads will become the union of src (href), imagesrcset, and imagesizes.
1 parent 8ed2797 commit 8d8bcde

File tree

5 files changed

+311
-85
lines changed

5 files changed

+311
-85
lines changed

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ import {
105105
} from 'react-reconciler/src/ReactWorkTags';
106106
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
107107
import {
108-
validatePreloadArguments,
109108
validatePreinitArguments,
110109
validateLinkPropsForStyleResource,
111110
getValueDescriptorExpectingObjectForWarning,
@@ -2167,21 +2166,102 @@ function preload(href: string, options: PreloadOptions) {
21672166
return;
21682167
}
21692168
if (__DEV__) {
2170-
validatePreloadArguments(href, options);
2169+
// TODO move this to ReactDOMFloat and expose a stricter function interface or possibly
2170+
// typed functions (preloadImage, preloadStyle, ...)if (__DEV__) {
2171+
switch (undefined) {
2172+
default: {
2173+
let encountered = '';
2174+
if (typeof href !== 'string') {
2175+
encountered += `The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2176+
href,
2177+
)}.`;
2178+
}
2179+
if (options == null || typeof options !== 'object') {
2180+
encountered += `The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
2181+
options,
2182+
)}.`;
2183+
if (typeof href === 'string') {
2184+
encountered += ` Try fixing the options argument for the call with \`href\` "${href}".`;
2185+
}
2186+
} else if (typeof options.as !== 'string' || !options.as) {
2187+
encountered += `The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning(
2188+
options.as,
2189+
)}.`;
2190+
}
2191+
if (encountered) {
2192+
console.error(
2193+
'ReactDOM.preload(): Expected two arguments, an `href` string and an `options` object with an `as` property valid for a `<link rel="preload" as="..." />` tag. %s',
2194+
encountered,
2195+
);
2196+
break;
2197+
}
2198+
2199+
const as = options.as;
2200+
if (as === 'image') {
2201+
if (
2202+
!href &&
2203+
(!options.imageSrcSet || typeof options.imageSrcSet !== 'string')
2204+
) {
2205+
console.error(
2206+
'ReactDOM.preload(): When preloading as an "image" expected either the `href` argument to be a non-empty string or the `imageSrcSet` option to be a non-empty string or both. The `href` encountered was %s and the `imageSrcSet` encountered was %s.',
2207+
getValueDescriptorExpectingObjectForWarning(href),
2208+
getValueDescriptorExpectingObjectForWarning(options.imageSrcSet),
2209+
);
2210+
}
2211+
} else {
2212+
if (!href) {
2213+
console.error(
2214+
'ReactDOM.preload(): When preloading as %s expected the `href` argument to be a non-empty string. The `href` encountered was %s.',
2215+
getValueDescriptorExpectingObjectForWarning(options.as),
2216+
getValueDescriptorExpectingObjectForWarning(href),
2217+
);
2218+
}
2219+
}
2220+
}
2221+
}
21712222
}
21722223
const ownerDocument = getDocumentForImperativeFloatMethods();
21732224
if (
21742225
typeof href === 'string' &&
2175-
href &&
21762226
typeof options === 'object' &&
21772227
options !== null &&
21782228
ownerDocument
21792229
) {
21802230
const as = options.as;
2181-
const limitedEscapedHref =
2182-
escapeSelectorAttributeValueInsideDoubleQuotes(href);
2183-
const preloadSelector = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
2231+
let preloadSelector = `link[rel="preload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes(
2232+
as,
2233+
)}"]`;
2234+
if (as === 'image') {
2235+
const {imageSrcSet, imageSizes} = options;
2236+
if (typeof imageSrcSet === 'string' && imageSrcSet !== '') {
2237+
preloadSelector += `[imagesrcset="${escapeSelectorAttributeValueInsideDoubleQuotes(
2238+
imageSrcSet,
2239+
)}"]`;
2240+
} else if (!href) {
2241+
// This call has neither a imageSrcSet nor an href so we can't preload anything.
2242+
// In dev we should have already warned above. We noop now.
2243+
return;
2244+
} else {
2245+
preloadSelector += `[href="${escapeSelectorAttributeValueInsideDoubleQuotes(
2246+
href,
2247+
)}"]`;
2248+
}
21842249
2250+
if (typeof imageSizes === 'string' && imageSizes !== '') {
2251+
preloadSelector += `[imagesizes="${escapeSelectorAttributeValueInsideDoubleQuotes(
2252+
imageSizes,
2253+
)}"]`;
2254+
}
2255+
} else {
2256+
if (!href) {
2257+
// This call has no href and its type does not specify an alternate preloadabl
2258+
// resources so we noop
2259+
return;
2260+
}
2261+
preloadSelector += `[href="${escapeSelectorAttributeValueInsideDoubleQuotes(
2262+
href,
2263+
)}"]`;
2264+
}
21852265
// Some preloads are keyed under their selector. This happens when the preload is for
21862266
// an arbitrary type. Other preloads are keyed under the resource key they represent a preload for.
21872267
// Here we figure out which key to use to determine if we have a preload already.

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

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4819,12 +4819,12 @@ type PreconnectResource = TResource<'preconnect', null>;
48194819
type PreloadAsProps = {
48204820
rel: 'preload',
48214821
as: string,
4822-
href: string,
4822+
href: ?string,
48234823
[string]: mixed,
48244824
};
48254825
type PreloadModuleProps = {
48264826
rel: 'modulepreload',
4827-
href: string,
4827+
href: ?string,
48284828
[string]: mixed,
48294829
};
48304830
type PreloadProps = PreloadAsProps | PreloadModuleProps;
@@ -5063,32 +5063,97 @@ export function preload(href: string, options: PreloadOptions) {
50635063
}
50645064
const resources = getResources(request);
50655065
if (__DEV__) {
5066-
if (typeof href !== 'string' || !href) {
5067-
console.error(
5068-
'ReactDOM.preload(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
5069-
getValueDescriptorExpectingObjectForWarning(href),
5070-
);
5071-
} else if (options == null || typeof options !== 'object') {
5072-
console.error(
5073-
'ReactDOM.preload(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preloaded but encountered %s instead.',
5074-
getValueDescriptorExpectingEnumForWarning(options),
5075-
);
5076-
} else if (typeof options.as !== 'string') {
5077-
console.error(
5078-
'ReactDOM.preload(): Expected the `as` property in the `options` argument (second) to contain a string value describing the type of resource to be preloaded but encountered %s instead. Values that are valid in for the `as` attribute of a `<link rel="preload" as="..." />` tag are valid here.',
5079-
getValueDescriptorExpectingEnumForWarning(options.as),
5080-
);
5066+
switch (undefined) {
5067+
default: {
5068+
let encountered = '';
5069+
if (typeof href !== 'string') {
5070+
encountered += `The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5071+
href,
5072+
)}.`;
5073+
}
5074+
if (options == null || typeof options !== 'object') {
5075+
encountered += `The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
5076+
options,
5077+
)}.`;
5078+
if (typeof href === 'string') {
5079+
encountered += ` Try fixing the options argument for the call with \`href\` "${href}".`;
5080+
}
5081+
} else if (typeof options.as !== 'string' || !options.as) {
5082+
encountered += `The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning(
5083+
options.as,
5084+
)}.`;
5085+
}
5086+
if (encountered) {
5087+
console.error(
5088+
'ReactDOM.preload(): Expected two arguments, an `href` string and an `options` object with an `as` property valid for a `<link rel="preload" as="..." />` tag. %s',
5089+
encountered,
5090+
);
5091+
break;
5092+
}
5093+
5094+
const as = options.as;
5095+
if (as === 'image') {
5096+
if (
5097+
!href &&
5098+
(!options.imageSrcSet || typeof options.imageSrcSet !== 'string')
5099+
) {
5100+
console.error(
5101+
'ReactDOM.preload(): When preloading as an "image" expected either the `href` argument to be a non-empty string or the `imageSrcSet` option to be a non-empty string or both. The `href` encountered was %s and the `imageSrcSet` encountered was %s.',
5102+
getValueDescriptorExpectingObjectForWarning(href),
5103+
getValueDescriptorExpectingObjectForWarning(options.imageSrcSet),
5104+
);
5105+
}
5106+
} else {
5107+
if (!href) {
5108+
console.error(
5109+
'ReactDOM.preload(): When preloading as %s expected the `href` argument to be a non-empty string. The `href` encountered was %s.',
5110+
getValueDescriptorExpectingObjectForWarning(options.as),
5111+
getValueDescriptorExpectingObjectForWarning(href),
5112+
);
5113+
}
5114+
}
5115+
}
50815116
}
50825117
}
50835118
if (
50845119
typeof href === 'string' &&
5085-
href &&
50865120
typeof options === 'object' &&
50875121
options !== null &&
5088-
typeof options.as === 'string'
5122+
typeof options.as === 'string' &&
5123+
options.as
50895124
) {
50905125
const as = options.as;
5091-
const key = getResourceKey(as, href);
5126+
let key: string;
5127+
if (as === 'image') {
5128+
const {imageSrcSet, imageSizes} = options;
5129+
let uniquePart = '';
5130+
if (typeof imageSrcSet === 'string' && imageSrcSet !== '') {
5131+
uniquePart += '[' + imageSrcSet + ']';
5132+
} else if (!href) {
5133+
// This call has neither a imageSrcSet nor an href so we can't preload anything.
5134+
// In dev we should have already warned above. We noop now.
5135+
return;
5136+
} else {
5137+
uniquePart += '[]';
5138+
}
5139+
5140+
if (typeof imageSizes === 'string' && imageSizes !== '') {
5141+
uniquePart += '[' + imageSizes + ']';
5142+
} else {
5143+
uniquePart += '[]';
5144+
}
5145+
5146+
uniquePart += href;
5147+
5148+
key = getResourceKey(as, uniquePart);
5149+
} else {
5150+
if (!href) {
5151+
// This call has no href and its type does not specify an alternate preloadabl
5152+
// resources so we noop
5153+
return;
5154+
}
5155+
key = getResourceKey(as, href);
5156+
}
50925157
let resource = resources.preloadsMap.get(key);
50935158
if (__DEV__) {
50945159
const devResource = getAsResourceDEV(resource);
@@ -5528,12 +5593,18 @@ function preloadPropsFromPreloadOptions(
55285593
return {
55295594
rel: 'preload',
55305595
as,
5531-
href,
5596+
// When preloading with imageSrcSet and imageSizes sometimes it
5597+
// can make sense to not have a default href. We validate that
5598+
// you are omitting it elsewhere but here we just mark it undefined
5599+
// if it is falsey so that when it renders it omits the href
5600+
href: href ? href : undefined,
55325601
crossOrigin: as === 'font' ? '' : options.crossOrigin,
55335602
integrity: options.integrity,
55345603
type: options.type,
55355604
nonce: options.nonce,
55365605
fetchPriority: options.fetchPriority,
5606+
imageSrcSet: options.imageSrcSet,
5607+
imageSizes: options.imageSizes,
55375608
};
55385609
}
55395610

packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -62,60 +62,6 @@ function propNamesListJoin(
6262
}
6363
}
6464

65-
export function validatePreloadArguments(href: mixed, options: mixed) {
66-
if (__DEV__) {
67-
if (!href || typeof href !== 'string') {
68-
const typeOfArg = getValueDescriptorExpectingObjectForWarning(href);
69-
console.error(
70-
'ReactDOM.preload() expected the first argument to be a string representing an href but found %s instead.',
71-
typeOfArg,
72-
);
73-
} else if (typeof options !== 'object' || options === null) {
74-
const typeOfArg = getValueDescriptorExpectingObjectForWarning(options);
75-
console.error(
76-
'ReactDOM.preload() expected the second argument to be an options argument containing at least an "as" property' +
77-
' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".',
78-
typeOfArg,
79-
href,
80-
);
81-
} else {
82-
const as = options.as;
83-
switch (as) {
84-
// Font specific validation of options
85-
case 'font': {
86-
if (options.crossOrigin === 'use-credentials') {
87-
console.error(
88-
'ReactDOM.preload() was called with an "as" type of "font" and with a "crossOrigin" option of "use-credentials".' +
89-
' Fonts preloading must use crossOrigin "anonymous" to be functional. Please update your font preload to omit' +
90-
' the crossOrigin option or change it to any other value than "use-credentials" (Browsers default all other values' +
91-
' to anonymous mode). The href for the preload call where this warning originated is "%s"',
92-
href,
93-
);
94-
}
95-
break;
96-
}
97-
case 'script':
98-
case 'style': {
99-
break;
100-
}
101-
102-
// We have an invalid as type and need to warn
103-
default: {
104-
const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
105-
console.error(
106-
'ReactDOM.preload() expected a valid "as" type in the options (second) argument but found %s instead.' +
107-
' Please use one of the following valid values instead: %s. The href for the preload call where this' +
108-
' warning originated is "%s".',
109-
typeOfAs,
110-
'"style", "font", or "script"',
111-
href,
112-
);
113-
}
114-
}
115-
}
116-
}
117-
}
118-
11965
export function validatePreinitArguments(href: mixed, options: mixed) {
12066
if (__DEV__) {
12167
if (!href || typeof href !== 'string') {

0 commit comments

Comments
 (0)