Skip to content

Commit 1173a17

Browse files
authored
[Float][Fizz][Fiber] implement preconnect and prefetchDNS float methods (#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.
1 parent e7d7d4c commit 1173a17

File tree

10 files changed

+432
-14
lines changed

10 files changed

+432
-14
lines changed

packages/react-dom-bindings/src/client/ReactDOMFloatClient.js

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
1818
import {
1919
validatePreloadArguments,
2020
validatePreinitArguments,
21+
getValueDescriptorExpectingObjectForWarning,
22+
getValueDescriptorExpectingEnumForWarning,
2123
} from '../shared/ReactDOMResourceValidation';
2224
import {createElement, setInitialProperties} from './ReactDOMComponent';
2325
import {
@@ -103,12 +105,18 @@ export function cleanupAfterRenderResources() {
103105
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
104106
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
105107
// from Internals -> ReactDOM -> FloatClient -> Internals so this doesn't introduce a new one.
106-
export const ReactDOMClientDispatcher = {preload, preinit};
108+
export const ReactDOMClientDispatcher = {
109+
prefetchDNS,
110+
preconnect,
111+
preload,
112+
preinit,
113+
};
107114

108115
export type HoistableRoot = Document | ShadowRoot;
109116

110-
// global maps of Resources
117+
// global collections of Resources
111118
const preloadPropsMap: Map<string, PreloadProps> = new Map();
119+
const preconnectsSet: Set<string> = new Set();
112120

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

159+
function preconnectAs(
160+
rel: 'preconnect' | 'dns-prefetch',
161+
crossOrigin: null | '' | 'use-credentials',
162+
href: string,
163+
) {
164+
const ownerDocument = getDocumentForPreloads();
165+
if (typeof href === 'string' && href && ownerDocument) {
166+
const limitedEscapedHref =
167+
escapeSelectorAttributeValueInsideDoubleQuotes(href);
168+
let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
169+
if (typeof crossOrigin === 'string') {
170+
key += `[crossorigin="${crossOrigin}"]`;
171+
}
172+
if (!preconnectsSet.has(key)) {
173+
preconnectsSet.add(key);
174+
175+
const preconnectProps = {rel, crossOrigin, href};
176+
if (null === ownerDocument.querySelector(key)) {
177+
const preloadInstance = createElement(
178+
'link',
179+
preconnectProps,
180+
ownerDocument,
181+
HTML_NAMESPACE,
182+
);
183+
setInitialProperties(preloadInstance, 'link', preconnectProps);
184+
markNodeAsResource(preloadInstance);
185+
(ownerDocument.head: any).appendChild(preloadInstance);
186+
}
187+
}
188+
}
189+
}
190+
191+
// --------------------------------------
192+
// ReactDOM.prefetchDNS
193+
// --------------------------------------
194+
function prefetchDNS(href: string, options?: mixed) {
195+
if (__DEV__) {
196+
if (typeof href !== 'string' || !href) {
197+
console.error(
198+
'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
199+
getValueDescriptorExpectingObjectForWarning(href),
200+
);
201+
} else if (options != null) {
202+
if (
203+
typeof options === 'object' &&
204+
options.hasOwnProperty('crossOrigin')
205+
) {
206+
console.error(
207+
'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`.',
208+
getValueDescriptorExpectingEnumForWarning(options),
209+
);
210+
} else {
211+
console.error(
212+
'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`.',
213+
getValueDescriptorExpectingEnumForWarning(options),
214+
);
215+
}
216+
}
217+
}
218+
preconnectAs('dns-prefetch', null, href);
219+
}
220+
221+
// --------------------------------------
222+
// ReactDOM.preconnect
223+
// --------------------------------------
224+
function preconnect(href: string, options?: {crossOrigin?: string}) {
225+
if (__DEV__) {
226+
if (typeof href !== 'string' || !href) {
227+
console.error(
228+
'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
229+
getValueDescriptorExpectingObjectForWarning(href),
230+
);
231+
} else if (options != null && typeof options !== 'object') {
232+
console.error(
233+
'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.',
234+
getValueDescriptorExpectingEnumForWarning(options),
235+
);
236+
} else if (options != null && typeof options.crossOrigin !== 'string') {
237+
console.error(
238+
'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.',
239+
getValueDescriptorExpectingObjectForWarning(options.crossOrigin),
240+
);
241+
}
242+
}
243+
const crossOrigin =
244+
options == null || typeof options.crossOrigin !== 'string'
245+
? null
246+
: options.crossOrigin === 'use-credentials'
247+
? 'use-credentials'
248+
: '';
249+
preconnectAs('preconnect', crossOrigin, href);
250+
}
251+
151252
// --------------------------------------
152-
// ReactDOM.Preload
253+
// ReactDOM.preload
153254
// --------------------------------------
154255
type PreloadAs = ResourceType;
155256
type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: string};

packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
8686

8787
const ReactDOMServerDispatcher = enableFloat
8888
? {
89+
prefetchDNS,
90+
preconnect,
8991
preload,
9092
preinit,
9193
}
@@ -3464,6 +3466,10 @@ export function writePreamble(
34643466
}
34653467
charsetChunks.length = 0;
34663468

3469+
// emit preconnect resources
3470+
resources.preconnects.forEach(flushResourceInPreamble, destination);
3471+
resources.preconnects.clear();
3472+
34673473
const preconnectChunks = responseState.preconnectChunks;
34683474
for (i = 0; i < preconnectChunks.length; i++) {
34693475
writeChunk(destination, preconnectChunks[i]);
@@ -3559,6 +3565,9 @@ export function writeHoistables(
35593565
// We omit charsetChunks because we have already sent the shell and if it wasn't
35603566
// already sent it is too late now.
35613567

3568+
resources.preconnects.forEach(flushResourceLate, destination);
3569+
resources.preconnects.clear();
3570+
35623571
const preconnectChunks = responseState.preconnectChunks;
35633572
for (i = 0; i < preconnectChunks.length; i++) {
35643573
writeChunk(destination, preconnectChunks[i]);
@@ -4068,7 +4077,10 @@ const Blocked /* */ = 0b0100;
40684077
// This generally only makes sense for Resources other than PreloadResource
40694078
const PreloadFlushed /* */ = 0b1000;
40704079

4071-
type TResource<T: 'stylesheet' | 'style' | 'script' | 'preload', P> = {
4080+
type TResource<
4081+
T: 'stylesheet' | 'style' | 'script' | 'preload' | 'preconnect',
4082+
P,
4083+
> = {
40724084
type: T,
40734085
chunks: Array<Chunk | PrecomputedChunk>,
40744086
state: ResourceStateTag,
@@ -4099,6 +4111,13 @@ type ResourceDEV =
40994111
| ImperativeResourceDEV
41004112
| ImplicitResourceDEV;
41014113

4114+
type PreconnectProps = {
4115+
rel: 'preconnect' | 'dns-prefetch',
4116+
href: string,
4117+
[string]: mixed,
4118+
};
4119+
type PreconnectResource = TResource<'preconnect', null>;
4120+
41024121
type PreloadProps = {
41034122
rel: 'preload',
41044123
as: string,
@@ -4131,15 +4150,21 @@ type ScriptProps = {
41314150
};
41324151
type ScriptResource = TResource<'script', null>;
41334152

4134-
type Resource = StyleResource | ScriptResource | PreloadResource;
4153+
type Resource =
4154+
| StyleResource
4155+
| ScriptResource
4156+
| PreloadResource
4157+
| PreconnectResource;
41354158

41364159
export type Resources = {
41374160
// Request local cache
41384161
preloadsMap: Map<string, PreloadResource>,
4162+
preconnectsMap: Map<string, PreconnectResource>,
41394163
stylesMap: Map<string, StyleResource>,
41404164
scriptsMap: Map<string, ScriptResource>,
41414165

41424166
// Flushing queues for Resource dependencies
4167+
preconnects: Set<PreconnectResource>,
41434168
fontPreloads: Set<PreloadResource>,
41444169
// usedImagePreloads: Set<PreloadResource>,
41454170
precedences: Map<string, Set<StyleResource>>,
@@ -4161,10 +4186,12 @@ export function createResources(): Resources {
41614186
return {
41624187
// persistent
41634188
preloadsMap: new Map(),
4189+
preconnectsMap: new Map(),
41644190
stylesMap: new Map(),
41654191
scriptsMap: new Map(),
41664192

41674193
// cleared on flush
4194+
preconnects: new Set(),
41684195
fontPreloads: new Set(),
41694196
// usedImagePreloads: new Set(),
41704197
precedences: new Map(),
@@ -4198,6 +4225,120 @@ function getResourceKey(as: string, href: string): string {
41984225
return `[${as}]${href}`;
41994226
}
42004227

4228+
export function prefetchDNS(href: string, options?: mixed) {
4229+
if (!currentResources) {
4230+
// While we expect that preconnect calls are primarily going to be observed
4231+
// during render because effects and events don't run on the server it is
4232+
// still possible that these get called in module scope. This is valid on
4233+
// the client since there is still a document to interact with but on the
4234+
// server we need a request to associate the call to. Because of this we
4235+
// simply return and do not warn.
4236+
return;
4237+
}
4238+
const resources = currentResources;
4239+
if (__DEV__) {
4240+
if (typeof href !== 'string' || !href) {
4241+
console.error(
4242+
'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
4243+
getValueDescriptorExpectingObjectForWarning(href),
4244+
);
4245+
} else if (options != null) {
4246+
if (
4247+
typeof options === 'object' &&
4248+
options.hasOwnProperty('crossOrigin')
4249+
) {
4250+
console.error(
4251+
'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`.',
4252+
getValueDescriptorExpectingEnumForWarning(options),
4253+
);
4254+
} else {
4255+
console.error(
4256+
'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`.',
4257+
getValueDescriptorExpectingEnumForWarning(options),
4258+
);
4259+
}
4260+
}
4261+
}
4262+
4263+
if (typeof href === 'string' && href) {
4264+
const key = getResourceKey('prefetchDNS', href);
4265+
let resource = resources.preconnectsMap.get(key);
4266+
if (!resource) {
4267+
resource = {
4268+
type: 'preconnect',
4269+
chunks: [],
4270+
state: NoState,
4271+
props: null,
4272+
};
4273+
resources.preconnectsMap.set(key, resource);
4274+
pushLinkImpl(
4275+
resource.chunks,
4276+
({href, rel: 'dns-prefetch'}: PreconnectProps),
4277+
);
4278+
}
4279+
resources.preconnects.add(resource);
4280+
}
4281+
}
4282+
4283+
export function preconnect(href: string, options?: {crossOrigin?: string}) {
4284+
if (!currentResources) {
4285+
// While we expect that preconnect calls are primarily going to be observed
4286+
// during render because effects and events don't run on the server it is
4287+
// still possible that these get called in module scope. This is valid on
4288+
// the client since there is still a document to interact with but on the
4289+
// server we need a request to associate the call to. Because of this we
4290+
// simply return and do not warn.
4291+
return;
4292+
}
4293+
const resources = currentResources;
4294+
if (__DEV__) {
4295+
if (typeof href !== 'string' || !href) {
4296+
console.error(
4297+
'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
4298+
getValueDescriptorExpectingObjectForWarning(href),
4299+
);
4300+
} else if (options != null && typeof options !== 'object') {
4301+
console.error(
4302+
'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.',
4303+
getValueDescriptorExpectingEnumForWarning(options),
4304+
);
4305+
} else if (options != null && typeof options.crossOrigin !== 'string') {
4306+
console.error(
4307+
'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.',
4308+
getValueDescriptorExpectingObjectForWarning(options.crossOrigin),
4309+
);
4310+
}
4311+
}
4312+
4313+
if (typeof href === 'string' && href) {
4314+
const crossOrigin =
4315+
options == null || typeof options.crossOrigin !== 'string'
4316+
? null
4317+
: options.crossOrigin === 'use-credentials'
4318+
? 'use-credentials'
4319+
: '';
4320+
4321+
const key = `[preconnect][${
4322+
crossOrigin === null ? 'null' : crossOrigin
4323+
}]${href}`;
4324+
let resource = resources.preconnectsMap.get(key);
4325+
if (!resource) {
4326+
resource = {
4327+
type: 'preconnect',
4328+
chunks: [],
4329+
state: NoState,
4330+
props: null,
4331+
};
4332+
resources.preconnectsMap.set(key, resource);
4333+
pushLinkImpl(
4334+
resource.chunks,
4335+
({rel: 'preconnect', href, crossOrigin}: PreconnectProps),
4336+
);
4337+
}
4338+
resources.preconnects.add(resource);
4339+
}
4340+
}
4341+
42014342
type PreloadAs = 'style' | 'font' | 'script';
42024343
type PreloadOptions = {
42034344
as: PreloadAs,

packages/react-dom-bindings/src/shared/ReactDOMFloat.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
22

3-
export function preinit() {
3+
export function prefetchDNS() {
44
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
55
if (dispatcher) {
6-
dispatcher.preinit.apply(this, arguments);
6+
dispatcher.prefetchDNS.apply(this, arguments);
77
}
8-
// We don't error because preinit needs to be resilient to being called in a variety of scopes
8+
// We don't error because preconnect needs to be resilient to being called in a variety of scopes
9+
// and the runtime may not be capable of responding. The function is optimistic and not critical
10+
// so we favor silent bailout over warning or erroring.
11+
}
12+
13+
export function preconnect() {
14+
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
15+
if (dispatcher) {
16+
dispatcher.preconnect.apply(this, arguments);
17+
}
18+
// We don't error because preconnect needs to be resilient to being called in a variety of scopes
919
// and the runtime may not be capable of responding. The function is optimistic and not critical
1020
// so we favor silent bailout over warning or erroring.
1121
}
@@ -19,3 +29,13 @@ export function preload() {
1929
// and the runtime may not be capable of responding. The function is optimistic and not critical
2030
// so we favor silent bailout over warning or erroring.
2131
}
32+
33+
export function preinit() {
34+
const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
35+
if (dispatcher) {
36+
dispatcher.preinit.apply(this, arguments);
37+
}
38+
// We don't error because preinit needs to be resilient to being called in a variety of scopes
39+
// and the runtime may not be capable of responding. The function is optimistic and not critical
40+
// so we favor silent bailout over warning or erroring.
41+
}

packages/react-dom/index.classic.fb.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ export {
3232
unstable_flushControlled,
3333
unstable_renderSubtreeIntoContainer,
3434
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
35-
preinit,
35+
prefetchDNS,
36+
preconnect,
3637
preload,
38+
preinit,
3739
version,
3840
} from './src/client/ReactDOM';
3941

0 commit comments

Comments
 (0)