Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 93 additions & 18 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -2370,6 +2370,78 @@ function pushStyleContents(
return;
}

function getImagePreloadKey(
href: string,
imageSrcSet: ?string,
imageSizes: ?string,
) {
let uniquePart = '';
if (typeof imageSrcSet === 'string' && imageSrcSet !== '') {
uniquePart += '[' + imageSrcSet + ']';
if (typeof imageSizes === 'string') {
uniquePart += '[' + imageSizes + ']';
}
} else {
uniquePart += '[][]' + href;
}
return getResourceKey('image', uniquePart);
}

function pushImg(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
resources: Resources,
): null {
if (
props.loading !== 'lazy' &&
typeof props.src === 'string' &&
props.fetchPriority !== 'low'
) {
// We have a suspensey image and ought to preload it to optimize the loading of display blocking
// resources.
const {src, imageSrcSet, imageSizes} = props;
const key = getImagePreloadKey(src, imageSrcSet, imageSizes);
let resource = resources.preloadsMap.get(key);
if (!resource) {
resource = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's extract this stuff too like getPreloadResource(...).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to but I think it needs to go in another PR. there are challenges with this b/c the current implementation is highly optimized to avoid creating props object unless absolutely required and so this can't be tidily implemented in a single function. Also I should do the same thing for stylesheet and other resource types

type: 'preload',
chunks: [],
state: NoState,
props: {
rel: 'preload',
as: 'image',
// There is a bug in Safari where imageSrcSet is not respected on preload links
// so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
// This harms older browers that do not support imageSrcSet by making their preloads not work
// but this population is shrinking fast and is already small so we accept this tradeoff.
href: imageSrcSet ? undefined : src,
imageSrcSet,
imageSizes,
crossOrigin: props.crossOrigin,
integrity: props.integrity,
type: props.type,
fetchPriority: props.fetchPriority,
referrerPolicy: props.referrerPolicy,
},
};
resources.preloadsMap.set(key, resource);
if (__DEV__) {
markAsRenderedResourceDEV(resource, props);
}
pushLinkImpl(resource.chunks, resource.props);
}
if (
props.fetchPriority === 'high' ||
resources.highImagePreloads.size < 10
) {
resources.highImagePreloads.add(resource);
} else {
resources.bulkPreloads.add(resource);
}
}
return pushSelfClosing(target, props, 'img');
}

function pushSelfClosing(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
Expand Down Expand Up @@ -3172,14 +3244,16 @@ export function pushStartInstance(
case 'pre': {
return pushStartPreformattedElement(target, props, type);
}
case 'img': {
return pushImg(target, props, resources);
}
// Omitted close tags
case 'base':
case 'area':
case 'br':
case 'col':
case 'embed':
case 'hr':
case 'img':
case 'keygen':
case 'param':
case 'source':
Expand Down Expand Up @@ -4242,6 +4316,9 @@ export function writePreamble(
resources.fontPreloads.forEach(flushResourceInPreamble, destination);
resources.fontPreloads.clear();

resources.highImagePreloads.forEach(flushResourceInPreamble, destination);
resources.highImagePreloads.clear();

// Flush unblocked stylesheets by precedence
resources.precedences.forEach(flushAllStylesInPreamble, destination);

Expand All @@ -4250,8 +4327,8 @@ export function writePreamble(
resources.scripts.forEach(flushResourceInPreamble, destination);
resources.scripts.clear();

resources.explicitPreloads.forEach(flushResourceInPreamble, destination);
resources.explicitPreloads.clear();
resources.bulkPreloads.forEach(flushResourceInPreamble, destination);
resources.bulkPreloads.clear();

// Write embedding preloadChunks
const preloadChunks = responseState.preloadChunks;
Expand Down Expand Up @@ -4308,6 +4385,9 @@ export function writeHoistables(
resources.fontPreloads.forEach(flushResourceLate, destination);
resources.fontPreloads.clear();

resources.highImagePreloads.forEach(flushResourceInPreamble, destination);
resources.highImagePreloads.clear();

// Preload any stylesheets. these will emit in a render instruction that follows this
// but we want to kick off preloading as soon as possible
resources.precedences.forEach(preloadLateStyles, destination);
Expand All @@ -4318,8 +4398,8 @@ export function writeHoistables(
resources.scripts.forEach(flushResourceLate, destination);
resources.scripts.clear();

resources.explicitPreloads.forEach(flushResourceLate, destination);
resources.explicitPreloads.clear();
resources.bulkPreloads.forEach(flushResourceLate, destination);
resources.bulkPreloads.clear();

// Write embedding preloadChunks
const preloadChunks = responseState.preloadChunks;
Expand Down Expand Up @@ -4859,12 +4939,13 @@ export type Resources = {
// Flushing queues for Resource dependencies
preconnects: Set<PreconnectResource>,
fontPreloads: Set<PreloadResource>,
highImagePreloads: Set<PreloadResource>,
// usedImagePreloads: Set<PreloadResource>,
precedences: Map<string, Set<StyleResource>>,
stylePrecedences: Map<string, StyleTagResource>,
bootstrapScripts: Set<PreloadResource>,
scripts: Set<ScriptResource>,
explicitPreloads: Set<PreloadResource>,
bulkPreloads: Set<PreloadResource>,

// Module-global-like reference for current boundary resources
boundaryResources: ?BoundaryResources,
Expand All @@ -4883,12 +4964,13 @@ export function createResources(): Resources {
// cleared on flush
preconnects: new Set(),
fontPreloads: new Set(),
highImagePreloads: new Set(),
// usedImagePreloads: new Set(),
precedences: new Map(),
stylePrecedences: new Map(),
bootstrapScripts: new Set(),
scripts: new Set(),
explicitPreloads: new Set(),
bulkPreloads: new Set(),

// like a module global for currently rendering boundary
boundaryResources: null,
Expand Down Expand Up @@ -5086,16 +5168,7 @@ export function preload(href: string, options: PreloadOptions) {
// both. This is to prevent identical calls with the same srcSet and sizes to be duplicated
// by varying the href. this is an edge case but it is the most correct behavior.
const {imageSrcSet, imageSizes} = options;
let uniquePart = '';
if (typeof imageSrcSet === 'string' && imageSrcSet !== '') {
uniquePart += '[' + imageSrcSet + ']';
if (typeof imageSizes === 'string') {
uniquePart += '[' + imageSizes + ']';
}
} else {
uniquePart += '[][]' + href;
}
key = getResourceKey(as, uniquePart);
key = getImagePreloadKey(href, imageSrcSet, imageSizes);
} else {
key = getResourceKey(as, href);
}
Expand Down Expand Up @@ -5177,8 +5250,10 @@ export function preload(href: string, options: PreloadOptions) {
}
if (as === 'font') {
resources.fontPreloads.add(resource);
} else if (as === 'image' && options.fetchPriority === 'high') {
resources.highImagePreloads.add(resource);
} else {
resources.explicitPreloads.add(resource);
resources.bulkPreloads.add(resource);
}
flushResources(request);
}
Expand Down
157 changes: 157 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFloat-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3771,6 +3771,163 @@ body {
);
});

it('can emit preloads for non-lazy images that are rendered', async () => {
function App() {
ReactDOM.preload('script', {as: 'script'});
ReactDOM.preload('a', {as: 'image'});
ReactDOM.preload('b', {as: 'image'});
return (
<html>
<body>
<img src="a" />
<img src="b" loading="lazy" />
<img src="b2" loading="lazy" />
<img src="c" imageSrcSet="srcsetc" />
<img src="d" imageSrcSet="srcsetd" imageSizes="sizesd" />
<img src="d" imageSrcSet="srcsetd" imageSizes="sizesd2" />
</body>
</html>
);
}

await act(() => {
renderToPipeableStream(<App />).pipe(writable);
});

// non-lazy images are first, then arbitrary preloads like for the script and lazy images
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" href="a" as="image" />
<link rel="preload" as="image" imagesrcset="srcsetc" />
<link
rel="preload"
as="image"
imagesrcset="srcsetd"
imagesizes="sizesd"
/>
<link
rel="preload"
as="image"
imagesrcset="srcsetd"
imagesizes="sizesd2"
/>
<link rel="preload" href="script" as="script" />
<link rel="preload" href="b" as="image" />
</head>
<body>
<img src="a" />
<img src="b" loading="lazy" />
<img src="b2" loading="lazy" />
<img src="c" imagesrcset="srcsetc" />
<img src="d" imagesrcset="srcsetd" imagesizes="sizesd" />
<img src="d" imagesrcset="srcsetd" imagesizes="sizesd2" />
</body>
</html>,
);
});

it('Does not preload lazy images', async () => {
function App() {
ReactDOM.preload('a', {as: 'image'});
return (
<html>
<body>
<img src="a" fetchPriority="low" />
<img src="b" fetchPriority="low" />
</body>
</html>
);
}
await act(() => {
renderToPipeableStream(<App />).pipe(writable);
});

expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="image" href="a" />
</head>
<body>
<img src="a" fetchpriority="low" />
<img src="b" fetchpriority="low" />
</body>
</html>,
);
});

it('preloads up to 10 suspensey images as high priority when fetchPriority is not specified', async () => {
function App() {
ReactDOM.preload('1', {as: 'image', fetchPriority: 'high'});
ReactDOM.preload('auto', {as: 'image'});
ReactDOM.preload('low', {as: 'image', fetchPriority: 'low'});
ReactDOM.preload('9', {as: 'image', fetchPriority: 'high'});
ReactDOM.preload('10', {as: 'image', fetchPriority: 'high'});
return (
<html>
<body>
{/* skipping 1 */}
<img src="2" />
<img src="3" fetchPriority="auto" />
<img src="4" fetchPriority="high" />
<img src="5" />
<img src="5low" fetchPriority="low" />
<img src="6" />
<img src="7" />
<img src="8" />
<img src="9" />
{/* skipping 10 */}
<img src="11" />
<img src="12" fetchPriority="high" />
</body>
</html>
);
}
await act(() => {
renderToPipeableStream(<App />).pipe(writable);
});

expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
{/* First we see the preloads calls that made it to the high priority image queue */}
<link rel="preload" as="image" href="1" fetchpriority="high" />
<link rel="preload" as="image" href="9" fetchpriority="high" />
<link rel="preload" as="image" href="10" fetchpriority="high" />
{/* Next we see up to 7 more images qualify for high priority image queue */}
<link rel="preload" as="image" href="2" />
<link rel="preload" as="image" href="3" fetchpriority="auto" />
<link rel="preload" as="image" href="4" fetchpriority="high" />
<link rel="preload" as="image" href="5" />
<link rel="preload" as="image" href="6" />
<link rel="preload" as="image" href="7" />
<link rel="preload" as="image" href="8" />
{/* Next we see images that are explicitly high priority and thus make it to the high priority image queue */}
<link rel="preload" as="image" href="12" fetchpriority="high" />
{/* Next we see the remaining preloads that did not make it to the high priority image queue */}
<link rel="preload" as="image" href="auto" />
<link rel="preload" as="image" href="low" fetchpriority="low" />
<link rel="preload" as="image" href="11" />
</head>
<body>
{/* skipping 1 */}
<img src="2" />
<img src="3" fetchpriority="auto" />
<img src="4" fetchpriority="high" />
<img src="5" />
<img src="5low" fetchpriority="low" />
<img src="6" />
<img src="7" />
<img src="8" />
<img src="9" />
{/* skipping 10 */}
<img src="11" />
<img src="12" fetchpriority="high" />
</body>
</html>,
);
});

describe('ReactDOM.prefetchDNS(href)', () => {
it('creates a dns-prefetch resource when called', async () => {
function App({url}) {
Expand Down