Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Float][Fizz][Fiber] implement preconnect and prefetchDNS float methods #26237

Merged
merged 2 commits into from
Feb 25, 2023
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
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