Skip to content

Commit

Permalink
[Float][Fizz][Fiber] implement preconnect and prefetchDNS float metho…
Browse files Browse the repository at this point in the history
…ds (#26237)

Adds two new ReactDOM methods

### `ReactDOM.prefetchDNS(href: string)`
In SSR this method will cause a `<link rel="dns-prefetch" href="..." />`
to flush before most other content both on intial flush (Shell) and late
flushes. It will only emit one link per href.

On the client, this method will case the same kind of link to be
inserted into the document immediately (when called during render, not
during commit) if there is not already a matching element in the
document.

### `ReactDOM.preconnect(href: string, options?: { crossOrigin?: string
})`
In SSR this method will cause a `<link rel="dns-prefetch" href="..."
[corssorigin="..."] />` to flush before most other content both on
intial flush (Shell) and late flushes. It will only emit one link per
href + crossorigin combo.

On the client, this method will case the same kind of link to be
inserted into the document immediately (when called during render, not
during commit) if there is not already a matching element in the
document.
  • Loading branch information
gnoff committed Feb 25, 2023
1 parent e7d7d4c commit 1173a17
Show file tree
Hide file tree
Showing 10 changed files with 432 additions and 14 deletions.
107 changes: 104 additions & 3 deletions packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
import {
validatePreloadArguments,
validatePreinitArguments,
getValueDescriptorExpectingObjectForWarning,
getValueDescriptorExpectingEnumForWarning,
} from '../shared/ReactDOMResourceValidation';
import {createElement, setInitialProperties} from './ReactDOMComponent';
import {
Expand Down Expand Up @@ -103,12 +105,18 @@ export function cleanupAfterRenderResources() {
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
// from Internals -> ReactDOM -> FloatClient -> Internals so this doesn't introduce a new one.
export const ReactDOMClientDispatcher = {preload, preinit};
export const ReactDOMClientDispatcher = {
prefetchDNS,
preconnect,
preload,
preinit,
};

export type HoistableRoot = Document | ShadowRoot;

// global maps of Resources
// global collections of Resources
const preloadPropsMap: Map<string, PreloadProps> = new Map();
const preconnectsSet: Set<string> = new Set();

// getRootNode is missing from IE and old jsdom versions
export function getHoistableRoot(container: Container): HoistableRoot {
Expand Down Expand Up @@ -148,8 +156,101 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
return root.ownerDocument || root;
}

function preconnectAs(
rel: 'preconnect' | 'dns-prefetch',
crossOrigin: null | '' | 'use-credentials',
href: string,
) {
const ownerDocument = getDocumentForPreloads();
if (typeof href === 'string' && href && ownerDocument) {
const limitedEscapedHref =
escapeSelectorAttributeValueInsideDoubleQuotes(href);
let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
if (typeof crossOrigin === 'string') {
key += `[crossorigin="${crossOrigin}"]`;
}
if (!preconnectsSet.has(key)) {
preconnectsSet.add(key);

const preconnectProps = {rel, crossOrigin, href};
if (null === ownerDocument.querySelector(key)) {
const preloadInstance = createElement(
'link',
preconnectProps,
ownerDocument,
HTML_NAMESPACE,
);
setInitialProperties(preloadInstance, 'link', preconnectProps);
markNodeAsResource(preloadInstance);
(ownerDocument.head: any).appendChild(preloadInstance);
}
}
}
}

// --------------------------------------
// ReactDOM.prefetchDNS
// --------------------------------------
function prefetchDNS(href: string, options?: mixed) {
if (__DEV__) {
if (typeof href !== 'string' || !href) {
console.error(
'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
getValueDescriptorExpectingObjectForWarning(href),
);
} else if (options != null) {
if (
typeof options === 'object' &&
options.hasOwnProperty('crossOrigin')
) {
console.error(
'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. It looks like the you are attempting to set a crossOrigin property for this DNS lookup hint. Browsers do not perform DNS queries using CORS and setting this attribute on the resource hint has no effect. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.',
getValueDescriptorExpectingEnumForWarning(options),
);
} else {
console.error(
'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.',
getValueDescriptorExpectingEnumForWarning(options),
);
}
}
}
preconnectAs('dns-prefetch', null, href);
}

// --------------------------------------
// ReactDOM.preconnect
// --------------------------------------
function preconnect(href: string, options?: {crossOrigin?: string}) {
if (__DEV__) {
if (typeof href !== 'string' || !href) {
console.error(
'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
getValueDescriptorExpectingObjectForWarning(href),
);
} else if (options != null && typeof options !== 'object') {
console.error(
'ReactDOM.preconnect(): Expected the `options` argument (second) to be an object but encountered %s instead. The only supported option at this time is `crossOrigin` which accepts a string.',
getValueDescriptorExpectingEnumForWarning(options),
);
} else if (options != null && typeof options.crossOrigin !== 'string') {
console.error(
'ReactDOM.preconnect(): Expected the `crossOrigin` option (second argument) to be a string but encountered %s instead. Try removing this option or passing a string value instead.',
getValueDescriptorExpectingObjectForWarning(options.crossOrigin),
);
}
}
const crossOrigin =
options == null || typeof options.crossOrigin !== 'string'
? null
: options.crossOrigin === 'use-credentials'
? 'use-credentials'
: '';
preconnectAs('preconnect', crossOrigin, href);
}

// --------------------------------------
// ReactDOM.Preload
// ReactDOM.preload
// --------------------------------------
type PreloadAs = ResourceType;
type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: string};
Expand Down
145 changes: 143 additions & 2 deletions packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;

const ReactDOMServerDispatcher = enableFloat
? {
prefetchDNS,
preconnect,
preload,
preinit,
}
Expand Down Expand Up @@ -3464,6 +3466,10 @@ export function writePreamble(
}
charsetChunks.length = 0;

// emit preconnect resources
resources.preconnects.forEach(flushResourceInPreamble, destination);
resources.preconnects.clear();

const preconnectChunks = responseState.preconnectChunks;
for (i = 0; i < preconnectChunks.length; i++) {
writeChunk(destination, preconnectChunks[i]);
Expand Down Expand Up @@ -3559,6 +3565,9 @@ export function writeHoistables(
// We omit charsetChunks because we have already sent the shell and if it wasn't
// already sent it is too late now.

resources.preconnects.forEach(flushResourceLate, destination);
resources.preconnects.clear();

const preconnectChunks = responseState.preconnectChunks;
for (i = 0; i < preconnectChunks.length; i++) {
writeChunk(destination, preconnectChunks[i]);
Expand Down Expand Up @@ -4068,7 +4077,10 @@ const Blocked /* */ = 0b0100;
// This generally only makes sense for Resources other than PreloadResource
const PreloadFlushed /* */ = 0b1000;

type TResource<T: 'stylesheet' | 'style' | 'script' | 'preload', P> = {
type TResource<
T: 'stylesheet' | 'style' | 'script' | 'preload' | 'preconnect',
P,
> = {
type: T,
chunks: Array<Chunk | PrecomputedChunk>,
state: ResourceStateTag,
Expand Down Expand Up @@ -4099,6 +4111,13 @@ type ResourceDEV =
| ImperativeResourceDEV
| ImplicitResourceDEV;

type PreconnectProps = {
rel: 'preconnect' | 'dns-prefetch',
href: string,
[string]: mixed,
};
type PreconnectResource = TResource<'preconnect', null>;

type PreloadProps = {
rel: 'preload',
as: string,
Expand Down Expand Up @@ -4131,15 +4150,21 @@ type ScriptProps = {
};
type ScriptResource = TResource<'script', null>;

type Resource = StyleResource | ScriptResource | PreloadResource;
type Resource =
| StyleResource
| ScriptResource
| PreloadResource
| PreconnectResource;

export type Resources = {
// Request local cache
preloadsMap: Map<string, PreloadResource>,
preconnectsMap: Map<string, PreconnectResource>,
stylesMap: Map<string, StyleResource>,
scriptsMap: Map<string, ScriptResource>,

// Flushing queues for Resource dependencies
preconnects: Set<PreconnectResource>,
fontPreloads: Set<PreloadResource>,
// usedImagePreloads: Set<PreloadResource>,
precedences: Map<string, Set<StyleResource>>,
Expand All @@ -4161,10 +4186,12 @@ export function createResources(): Resources {
return {
// persistent
preloadsMap: new Map(),
preconnectsMap: new Map(),
stylesMap: new Map(),
scriptsMap: new Map(),

// cleared on flush
preconnects: new Set(),
fontPreloads: new Set(),
// usedImagePreloads: new Set(),
precedences: new Map(),
Expand Down Expand Up @@ -4198,6 +4225,120 @@ function getResourceKey(as: string, href: string): string {
return `[${as}]${href}`;
}

export function prefetchDNS(href: string, options?: mixed) {
if (!currentResources) {
// While we expect that preconnect calls are primarily going to be observed
// during render because effects and events don't run on the server it is
// still possible that these get called in module scope. This is valid on
// the client since there is still a document to interact with but on the
// server we need a request to associate the call to. Because of this we
// simply return and do not warn.
return;
}
const resources = currentResources;
if (__DEV__) {
if (typeof href !== 'string' || !href) {
console.error(
'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
getValueDescriptorExpectingObjectForWarning(href),
);
} else if (options != null) {
if (
typeof options === 'object' &&
options.hasOwnProperty('crossOrigin')
) {
console.error(
'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. It looks like the you are attempting to set a crossOrigin property for this DNS lookup hint. Browsers do not perform DNS queries using CORS and setting this attribute on the resource hint has no effect. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.',
getValueDescriptorExpectingEnumForWarning(options),
);
} else {
console.error(
'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.',
getValueDescriptorExpectingEnumForWarning(options),
);
}
}
}

if (typeof href === 'string' && href) {
const key = getResourceKey('prefetchDNS', href);
let resource = resources.preconnectsMap.get(key);
if (!resource) {
resource = {
type: 'preconnect',
chunks: [],
state: NoState,
props: null,
};
resources.preconnectsMap.set(key, resource);
pushLinkImpl(
resource.chunks,
({href, rel: 'dns-prefetch'}: PreconnectProps),
);
}
resources.preconnects.add(resource);
}
}

export function preconnect(href: string, options?: {crossOrigin?: string}) {
if (!currentResources) {
// While we expect that preconnect calls are primarily going to be observed
// during render because effects and events don't run on the server it is
// still possible that these get called in module scope. This is valid on
// the client since there is still a document to interact with but on the
// server we need a request to associate the call to. Because of this we
// simply return and do not warn.
return;
}
const resources = currentResources;
if (__DEV__) {
if (typeof href !== 'string' || !href) {
console.error(
'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
getValueDescriptorExpectingObjectForWarning(href),
);
} else if (options != null && typeof options !== 'object') {
console.error(
'ReactDOM.preconnect(): Expected the `options` argument (second) to be an object but encountered %s instead. The only supported option at this time is `crossOrigin` which accepts a string.',
getValueDescriptorExpectingEnumForWarning(options),
);
} else if (options != null && typeof options.crossOrigin !== 'string') {
console.error(
'ReactDOM.preconnect(): Expected the `crossOrigin` option (second argument) to be a string but encountered %s instead. Try removing this option or passing a string value instead.',
getValueDescriptorExpectingObjectForWarning(options.crossOrigin),
);
}
}

if (typeof href === 'string' && href) {
const crossOrigin =
options == null || typeof options.crossOrigin !== 'string'
? null
: options.crossOrigin === 'use-credentials'
? 'use-credentials'
: '';

const key = `[preconnect][${
crossOrigin === null ? 'null' : crossOrigin
}]${href}`;
let resource = resources.preconnectsMap.get(key);
if (!resource) {
resource = {
type: 'preconnect',
chunks: [],
state: NoState,
props: null,
};
resources.preconnectsMap.set(key, resource);
pushLinkImpl(
resource.chunks,
({rel: 'preconnect', href, crossOrigin}: PreconnectProps),
);
}
resources.preconnects.add(resource);
}
}

type PreloadAs = 'style' | 'font' | 'script';
type PreloadOptions = {
as: PreloadAs,
Expand Down
26 changes: 23 additions & 3 deletions packages/react-dom-bindings/src/shared/ReactDOMFloat.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';

export function preinit() {
export function prefetchDNS() {
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
if (dispatcher) {
dispatcher.preinit.apply(this, arguments);
dispatcher.prefetchDNS.apply(this, arguments);
}
// We don't error because preinit needs to be resilient to being called in a variety of scopes
// We don't error because preconnect needs to be resilient to being called in a variety of scopes
// and the runtime may not be capable of responding. The function is optimistic and not critical
// so we favor silent bailout over warning or erroring.
}

export function preconnect() {
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
if (dispatcher) {
dispatcher.preconnect.apply(this, arguments);
}
// We don't error because preconnect needs to be resilient to being called in a variety of scopes
// and the runtime may not be capable of responding. The function is optimistic and not critical
// so we favor silent bailout over warning or erroring.
}
Expand All @@ -19,3 +29,13 @@ export function preload() {
// and the runtime may not be capable of responding. The function is optimistic and not critical
// so we favor silent bailout over warning or erroring.
}

export function preinit() {
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
if (dispatcher) {
dispatcher.preinit.apply(this, arguments);
}
// We don't error because preinit needs to be resilient to being called in a variety of scopes
// and the runtime may not be capable of responding. The function is optimistic and not critical
// so we favor silent bailout over warning or erroring.
}
4 changes: 3 additions & 1 deletion packages/react-dom/index.classic.fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ export {
unstable_flushControlled,
unstable_renderSubtreeIntoContainer,
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
preinit,
prefetchDNS,
preconnect,
preload,
preinit,
version,
} from './src/client/ReactDOM';

Expand Down
Loading

0 comments on commit 1173a17

Please sign in to comment.