mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Fix double preload (#26729)
I found a couple scenarios where preloads were issued too aggressively 1. During SSR, if you render a new stylesheet after the preamble flushed it will flush a preload even if the resource was already preloaded 2. During Client render, if you call `ReactDOM.preload()` it will only check if a preload exists in the Document before inserting a new one. It should check for an underlying resource such as a stylesheet link or script if the preload is for a recognized asset type
This commit is contained in:
@@ -2138,8 +2138,12 @@ function preload(href: string, options: PreloadOptions) {
|
||||
const as = options.as;
|
||||
const limitedEscapedHref =
|
||||
escapeSelectorAttributeValueInsideDoubleQuotes(href);
|
||||
const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
|
||||
let key = preloadKey;
|
||||
const preloadSelector = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
|
||||
|
||||
// Some preloads are keyed under their selector. This happens when the preload is for
|
||||
// an arbitrary type. Other preloads are keyed under the resource key they represent a preload for.
|
||||
// Here we figure out which key to use to determine if we have a preload already.
|
||||
let key = preloadSelector;
|
||||
switch (as) {
|
||||
case 'style':
|
||||
key = getStyleKey(href);
|
||||
@@ -2152,7 +2156,20 @@ function preload(href: string, options: PreloadOptions) {
|
||||
const preloadProps = preloadPropsFromPreloadOptions(href, as, options);
|
||||
preloadPropsMap.set(key, preloadProps);
|
||||
|
||||
if (null === ownerDocument.querySelector(preloadKey)) {
|
||||
if (null === ownerDocument.querySelector(preloadSelector)) {
|
||||
if (
|
||||
as === 'style' &&
|
||||
ownerDocument.querySelector(getStylesheetSelectorFromKey(key))
|
||||
) {
|
||||
// We already have a stylesheet for this key. We don't need to preload it.
|
||||
return;
|
||||
} else if (
|
||||
as === 'script' &&
|
||||
ownerDocument.querySelector(getScriptSelectorFromKey(key))
|
||||
) {
|
||||
// We already have a stylesheet for this key. We don't need to preload it.
|
||||
return;
|
||||
}
|
||||
const instance = ownerDocument.createElement('link');
|
||||
setInitialProperties(instance, 'link', preloadProps);
|
||||
markNodeAsHoistable(instance);
|
||||
|
||||
@@ -1900,6 +1900,7 @@ function pushLink(
|
||||
if (!resource) {
|
||||
const resourceProps = stylesheetPropsFromRawProps(props);
|
||||
const preloadResource = resources.preloadsMap.get(key);
|
||||
let state = NoState;
|
||||
if (preloadResource) {
|
||||
// If we already had a preload we don't want that resource to flush directly.
|
||||
// We let the newly created resource govern flushing.
|
||||
@@ -1908,11 +1909,14 @@ function pushLink(
|
||||
resourceProps,
|
||||
preloadResource.props,
|
||||
);
|
||||
if (preloadResource.state & Flushed) {
|
||||
state = PreloadFlushed;
|
||||
}
|
||||
}
|
||||
resource = {
|
||||
type: 'stylesheet',
|
||||
chunks: ([]: Array<Chunk | PrecomputedChunk>),
|
||||
state: NoState,
|
||||
state,
|
||||
props: resourceProps,
|
||||
};
|
||||
resources.stylesMap.set(key, resource);
|
||||
@@ -4004,12 +4008,9 @@ function flushAllStylesInPreamble(
|
||||
}
|
||||
|
||||
function preloadLateStyle(this: Destination, resource: StyleResource) {
|
||||
if (__DEV__) {
|
||||
if (resource.state & PreloadFlushed) {
|
||||
console.error(
|
||||
'React encountered a Stylesheet Resource that already flushed a Preload when it was not expected to. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
if (resource.state & PreloadFlushed) {
|
||||
// This resource has already had a preload flushed
|
||||
return;
|
||||
}
|
||||
|
||||
if (resource.type === 'style') {
|
||||
@@ -5209,10 +5210,15 @@ function preinit(href: string, options: PreinitOptions): void {
|
||||
}
|
||||
}
|
||||
if (!resource) {
|
||||
let state = NoState;
|
||||
const preloadResource = resources.preloadsMap.get(key);
|
||||
if (preloadResource && preloadResource.state & Flushed) {
|
||||
state = PreloadFlushed;
|
||||
}
|
||||
resource = {
|
||||
type: 'stylesheet',
|
||||
chunks: ([]: Array<Chunk | PrecomputedChunk>),
|
||||
state: NoState,
|
||||
state,
|
||||
props: stylesheetPropsFromPreinitOptions(href, precedence, options),
|
||||
};
|
||||
resources.stylesMap.set(key, resource);
|
||||
|
||||
@@ -3391,6 +3391,166 @@ body {
|
||||
);
|
||||
});
|
||||
|
||||
it('will not flush a preload for a new rendered Stylesheet Resource if one was already flushed', async () => {
|
||||
function Component() {
|
||||
ReactDOM.preload('foo', {as: 'style'});
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="loading...">
|
||||
<BlockedOn value="blocked">
|
||||
<link rel="stylesheet" href="foo" precedence="default" />
|
||||
hello
|
||||
</BlockedOn>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
await act(() => {
|
||||
renderToPipeableStream(
|
||||
<html>
|
||||
<body>
|
||||
<Component />
|
||||
</body>
|
||||
</html>,
|
||||
).pipe(writable);
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="preload" as="style" href="foo" />
|
||||
</head>
|
||||
<body>
|
||||
<div>loading...</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
await act(() => {
|
||||
resolveText('blocked');
|
||||
});
|
||||
await act(loadStylesheets);
|
||||
assertLog(['load stylesheet: foo']);
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="foo" data-precedence="default" />
|
||||
<link rel="preload" as="style" href="foo" />
|
||||
</head>
|
||||
<body>
|
||||
<div>hello</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
it('will not flush a preload for a new preinitialized Stylesheet Resource if one was already flushed', async () => {
|
||||
function Component() {
|
||||
ReactDOM.preload('foo', {as: 'style'});
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback="loading...">
|
||||
<BlockedOn value="blocked">
|
||||
<Preinit />
|
||||
hello
|
||||
</BlockedOn>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Preinit() {
|
||||
ReactDOM.preinit('foo', {as: 'style'});
|
||||
}
|
||||
await act(() => {
|
||||
renderToPipeableStream(
|
||||
<html>
|
||||
<body>
|
||||
<Component />
|
||||
</body>
|
||||
</html>,
|
||||
).pipe(writable);
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="preload" as="style" href="foo" />
|
||||
</head>
|
||||
<body>
|
||||
<div>loading...</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
await act(() => {
|
||||
resolveText('blocked');
|
||||
});
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="preload" as="style" href="foo" />
|
||||
</head>
|
||||
<body>
|
||||
<div>hello</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
it('will not insert a preload if the underlying resource already exists in the Document', async () => {
|
||||
await act(() => {
|
||||
renderToPipeableStream(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="foo" precedence="default" />
|
||||
<script async={true} src="bar" />
|
||||
<link rel="preload" href="baz" as="font" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="container" />
|
||||
</body>
|
||||
</html>,
|
||||
).pipe(writable);
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="foo" data-precedence="default" />
|
||||
<script async="" src="bar" />
|
||||
<link rel="preload" href="baz" as="font" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="container" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
container = document.getElementById('container');
|
||||
|
||||
function ClientApp() {
|
||||
ReactDOM.preload('foo', {as: 'style'});
|
||||
ReactDOM.preload('bar', {as: 'script'});
|
||||
ReactDOM.preload('baz', {as: 'font'});
|
||||
return 'foo';
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
await clientAct(() => root.render(<ClientApp />));
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="foo" data-precedence="default" />
|
||||
<script async="" src="bar" />
|
||||
<link rel="preload" href="baz" as="font" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">foo</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
describe('ReactDOM.prefetchDNS(href)', () => {
|
||||
it('creates a dns-prefetch resource when called', async () => {
|
||||
function App({url}) {
|
||||
|
||||
Reference in New Issue
Block a user