Skip to content

Commit

Permalink
Fix double preload (facebook#26729)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gnoff authored and AndyPengc12 committed Apr 15, 2024
1 parent 87f7558 commit f15b6e1
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 11 deletions.
23 changes: 20 additions & 3 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
22 changes: 14 additions & 8 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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);
Expand Down
160 changes: 160 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFloat-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}) {
Expand Down

0 comments on commit f15b6e1

Please sign in to comment.