Skip to content

Commit 0b97441

Browse files
authored
[Fizz] Fork Fizz instruction set for inline script and external runtime (#25862)
~~[Fizz] Duplicate completeBoundaryWithStyles to not reference globals~~ ## Summary Follow-up / cleanup PR to #25437 - `completeBoundaryWithStylesInlineLocals` is used by the Fizz external runtime, which bundles together all Fizz instruction functions (and is able to reference / rename `completeBoundary` and `resourceMap` as locals). - `completeBoundaryWithStylesInlineGlobals` is used by the Fizz inline script writer, which sends Fizz instruction functions on an as-needed basis. This version needs to reference `completeBoundary($RC)` and `resourceMap($RM)` as globals. Ideally, Closure would take care of inlining a shared implementation, but I couldn't figure out a zero-overhead inline due to lack of an `@inline` compiler directive. It seems that Closure thinks that a shared `completeBoundaryWithStyles` is too large and will always keep it as a separate function. I've also tried currying / writing a higher order function (`getCompleteBoundaryWithStyles`) with no luck ## How did you test this change? - generated Fizz inline instructions should be unchanged - bundle size for unstable_external_runtime should be slightly smaller (due to lack of globals) - `ReactDOMFizzServer-test.js` and `ReactDOMFloat-test.js` should be unaffected
1 parent 5379b61 commit 0b97441

9 files changed

+249
-113
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
completeBoundaryWithStyles,
1313
completeBoundary,
1414
completeSegment,
15-
} from './fizz-instruction-set/ReactDOMFizzInstructionSet';
15+
} from './fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime';
1616

1717
if (!window.$RC) {
1818
// TODO: Eventually remove, we currently need to set these globals for

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineClientRenderBoundary.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {clientRenderBoundary} from './ReactDOMFizzInstructionSet';
1+
import {clientRenderBoundary} from './ReactDOMFizzInstructionSetInlineSource';
22

33
// This is a string so Closure's advanced compilation mode doesn't mangle it.
44
// eslint-disable-next-line dot-notation

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {completeBoundary} from './ReactDOMFizzInstructionSet';
1+
import {completeBoundary} from './ReactDOMFizzInstructionSetInlineSource';
22

33
// This is a string so Closure's advanced compilation mode doesn't mangle it.
44
// eslint-disable-next-line dot-notation

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {completeBoundaryWithStyles} from './ReactDOMFizzInstructionSet';
1+
import {completeBoundaryWithStyles} from './ReactDOMFizzInstructionSetInlineSource';
22

33
// This is a string so Closure's advanced compilation mode doesn't mangle it.
44
// eslint-disable-next-line dot-notation

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteSegment.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {completeSegment} from './ReactDOMFizzInstructionSet';
1+
import {completeSegment} from './ReactDOMFizzInstructionSetInlineSource';
22

33
// This is a string so Closure's advanced compilation mode doesn't mangle it.
44
// eslint-disable-next-line dot-notation
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/* eslint-disable dot-notation */
2+
3+
// Instruction set for the Fizz external runtime
4+
5+
import {
6+
clientRenderBoundary,
7+
completeBoundary,
8+
completeSegment,
9+
LOADED,
10+
ERRORED,
11+
} from './ReactDOMFizzInstructionSetShared';
12+
13+
export {clientRenderBoundary, completeBoundary, completeSegment};
14+
15+
const resourceMap = new Map();
16+
17+
// This function is almost identical to the version used by inline scripts
18+
// (ReactDOMFizzInstructionSetInlineSource), with the exception of how we read
19+
// completeBoundary and resourceMap
20+
export function completeBoundaryWithStyles(
21+
suspenseBoundaryID,
22+
contentID,
23+
styles,
24+
) {
25+
const precedences = new Map();
26+
const thisDocument = document;
27+
let lastResource, node;
28+
29+
// Seed the precedence list with existing resources
30+
const nodes = thisDocument.querySelectorAll(
31+
'link[data-precedence],style[data-precedence]',
32+
);
33+
for (let i = 0; (node = nodes[i++]); ) {
34+
precedences.set(node.dataset['precedence'], (lastResource = node));
35+
}
36+
37+
let i = 0;
38+
const dependencies = [];
39+
let style, href, precedence, attr, loadingState, resourceEl;
40+
41+
function setStatus(s) {
42+
this['s'] = s;
43+
}
44+
45+
while ((style = styles[i++])) {
46+
let j = 0;
47+
href = style[j++];
48+
// We check if this resource is already in our resourceMap and reuse it if so.
49+
// If it is already loaded we don't return it as a depenendency since there is nothing
50+
// to wait for
51+
loadingState = resourceMap.get(href);
52+
if (loadingState) {
53+
if (loadingState['s'] !== 'l') {
54+
dependencies.push(loadingState);
55+
}
56+
continue;
57+
}
58+
59+
// We construct our new resource element, looping over remaining attributes if any
60+
// setting them to the Element.
61+
resourceEl = thisDocument.createElement('link');
62+
resourceEl.href = href;
63+
resourceEl.rel = 'stylesheet';
64+
resourceEl.dataset['precedence'] = precedence = style[j++];
65+
while ((attr = style[j++])) {
66+
resourceEl.setAttribute(attr, style[j++]);
67+
}
68+
69+
// We stash a pending promise in our map by href which will resolve or reject
70+
// when the underlying resource loads or errors. We add it to the dependencies
71+
// array to be returned.
72+
loadingState = resourceEl['_p'] = new Promise((re, rj) => {
73+
resourceEl.onload = re;
74+
resourceEl.onerror = rj;
75+
});
76+
loadingState.then(
77+
setStatus.bind(loadingState, LOADED),
78+
setStatus.bind(loadingState, ERRORED),
79+
);
80+
resourceMap.set(href, loadingState);
81+
dependencies.push(loadingState);
82+
83+
// The prior style resource is the last one placed at a given
84+
// precedence or the last resource itself which may be null.
85+
// We grab this value and then update the last resource for this
86+
// precedence to be the inserted element, updating the lastResource
87+
// pointer if needed.
88+
const prior = precedences.get(precedence) || lastResource;
89+
if (prior === lastResource) {
90+
lastResource = resourceEl;
91+
}
92+
precedences.set(precedence, resourceEl);
93+
94+
// Finally, we insert the newly constructed instance at an appropriate location
95+
// in the Document.
96+
if (prior) {
97+
prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
98+
} else {
99+
const head = thisDocument.head;
100+
head.insertBefore(resourceEl, head.firstChild);
101+
}
102+
}
103+
104+
Promise.all(dependencies).then(
105+
completeBoundary.bind(null, suspenseBoundaryID, contentID, ''),
106+
completeBoundary.bind(
107+
null,
108+
suspenseBoundaryID,
109+
contentID,
110+
'Resource failed to load',
111+
),
112+
);
113+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/* eslint-disable dot-notation */
2+
3+
// Instruction set for Fizz inline scripts.
4+
// DO NOT DIRECTLY IMPORT THIS FILE. This is the source for the compiled and
5+
// minified code in ReactDOMFizzInstructionSetInlineCodeStrings.
6+
7+
import {
8+
clientRenderBoundary,
9+
completeBoundary,
10+
completeSegment,
11+
LOADED,
12+
ERRORED,
13+
} from './ReactDOMFizzInstructionSetShared';
14+
15+
export {clientRenderBoundary, completeBoundary, completeSegment};
16+
17+
// This function is almost identical to the version used by the external
18+
// runtime (ReactDOMFizzInstructionSetExternalRuntime), with the exception of
19+
// how we read completeBoundaryImpl and resourceMap
20+
export function completeBoundaryWithStyles(
21+
suspenseBoundaryID,
22+
contentID,
23+
styles,
24+
) {
25+
const completeBoundaryImpl = window['$RC'];
26+
const resourceMap = window['$RM'];
27+
28+
const precedences = new Map();
29+
const thisDocument = document;
30+
let lastResource, node;
31+
32+
// Seed the precedence list with existing resources
33+
const nodes = thisDocument.querySelectorAll(
34+
'link[data-precedence],style[data-precedence]',
35+
);
36+
for (let i = 0; (node = nodes[i++]); ) {
37+
precedences.set(node.dataset['precedence'], (lastResource = node));
38+
}
39+
40+
let i = 0;
41+
const dependencies = [];
42+
let style, href, precedence, attr, loadingState, resourceEl;
43+
44+
function setStatus(s) {
45+
this['s'] = s;
46+
}
47+
48+
while ((style = styles[i++])) {
49+
let j = 0;
50+
href = style[j++];
51+
// We check if this resource is already in our resourceMap and reuse it if so.
52+
// If it is already loaded we don't return it as a depenendency since there is nothing
53+
// to wait for
54+
loadingState = resourceMap.get(href);
55+
if (loadingState) {
56+
if (loadingState['s'] !== 'l') {
57+
dependencies.push(loadingState);
58+
}
59+
continue;
60+
}
61+
62+
// We construct our new resource element, looping over remaining attributes if any
63+
// setting them to the Element.
64+
resourceEl = thisDocument.createElement('link');
65+
resourceEl.href = href;
66+
resourceEl.rel = 'stylesheet';
67+
resourceEl.dataset['precedence'] = precedence = style[j++];
68+
while ((attr = style[j++])) {
69+
resourceEl.setAttribute(attr, style[j++]);
70+
}
71+
72+
// We stash a pending promise in our map by href which will resolve or reject
73+
// when the underlying resource loads or errors. We add it to the dependencies
74+
// array to be returned.
75+
loadingState = resourceEl['_p'] = new Promise((re, rj) => {
76+
resourceEl.onload = re;
77+
resourceEl.onerror = rj;
78+
});
79+
loadingState.then(
80+
setStatus.bind(loadingState, LOADED),
81+
setStatus.bind(loadingState, ERRORED),
82+
);
83+
resourceMap.set(href, loadingState);
84+
dependencies.push(loadingState);
85+
86+
// The prior style resource is the last one placed at a given
87+
// precedence or the last resource itself which may be null.
88+
// We grab this value and then update the last resource for this
89+
// precedence to be the inserted element, updating the lastResource
90+
// pointer if needed.
91+
const prior = precedences.get(precedence) || lastResource;
92+
if (prior === lastResource) {
93+
lastResource = resourceEl;
94+
}
95+
precedences.set(precedence, resourceEl);
96+
97+
// Finally, we insert the newly constructed instance at an appropriate location
98+
// in the Document.
99+
if (prior) {
100+
prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
101+
} else {
102+
const head = thisDocument.head;
103+
head.insertBefore(resourceEl, head.firstChild);
104+
}
105+
}
106+
107+
Promise.all(dependencies).then(
108+
completeBoundaryImpl.bind(null, suspenseBoundaryID, contentID, ''),
109+
completeBoundaryImpl.bind(
110+
null,
111+
suspenseBoundaryID,
112+
contentID,
113+
'Resource failed to load',
114+
),
115+
);
116+
}

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSet.js renamed to packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js

+10-107
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
/* eslint-disable dot-notation */
22

3-
const COMMENT_NODE = 8;
4-
const SUSPENSE_START_DATA = '$';
5-
const SUSPENSE_END_DATA = '/$';
6-
const SUSPENSE_PENDING_START_DATA = '$?';
7-
const SUSPENSE_FALLBACK_START_DATA = '$!';
8-
const LOADED = 'l';
9-
const ERRORED = 'e';
3+
// Shared implementation and constants between the inline script and external
4+
// runtime instruction sets.
5+
6+
export const COMMENT_NODE = 8;
7+
export const SUSPENSE_START_DATA = '$';
8+
export const SUSPENSE_END_DATA = '/$';
9+
export const SUSPENSE_PENDING_START_DATA = '$?';
10+
export const SUSPENSE_FALLBACK_START_DATA = '$!';
11+
export const LOADED = 'l';
12+
export const ERRORED = 'e';
1013

1114
// TODO: Symbols that are referenced outside this module use dynamic accessor
1215
// notation instead of dot notation to prevent Closure's advanced compilation
@@ -42,106 +45,6 @@ export function clientRenderBoundary(
4245
}
4346
}
4447

45-
export function completeBoundaryWithStyles(
46-
suspenseBoundaryID,
47-
contentID,
48-
styles,
49-
) {
50-
// TODO: In the non-inline version of the runtime, these don't need to be read
51-
// from the global scope.
52-
const completeBoundaryImpl = window['$RC'];
53-
const resourceMap = window['$RM'];
54-
55-
const precedences = new Map();
56-
const thisDocument = document;
57-
let lastResource, node;
58-
59-
// Seed the precedence list with existing resources
60-
const nodes = thisDocument.querySelectorAll(
61-
'link[data-precedence],style[data-precedence]',
62-
);
63-
for (let i = 0; (node = nodes[i++]); ) {
64-
precedences.set(node.dataset['precedence'], (lastResource = node));
65-
}
66-
67-
let i = 0;
68-
const dependencies = [];
69-
let style, href, precedence, attr, loadingState, resourceEl;
70-
71-
function setStatus(s) {
72-
this['s'] = s;
73-
}
74-
75-
while ((style = styles[i++])) {
76-
let j = 0;
77-
href = style[j++];
78-
// We check if this resource is already in our resourceMap and reuse it if so.
79-
// If it is already loaded we don't return it as a depenendency since there is nothing
80-
// to wait for
81-
loadingState = resourceMap.get(href);
82-
if (loadingState) {
83-
if (loadingState['s'] !== 'l') {
84-
dependencies.push(loadingState);
85-
}
86-
continue;
87-
}
88-
89-
// We construct our new resource element, looping over remaining attributes if any
90-
// setting them to the Element.
91-
resourceEl = thisDocument.createElement('link');
92-
resourceEl.href = href;
93-
resourceEl.rel = 'stylesheet';
94-
resourceEl.dataset['precedence'] = precedence = style[j++];
95-
while ((attr = style[j++])) {
96-
resourceEl.setAttribute(attr, style[j++]);
97-
}
98-
99-
// We stash a pending promise in our map by href which will resolve or reject
100-
// when the underlying resource loads or errors. We add it to the dependencies
101-
// array to be returned.
102-
loadingState = resourceEl['_p'] = new Promise((re, rj) => {
103-
resourceEl.onload = re;
104-
resourceEl.onerror = rj;
105-
});
106-
loadingState.then(
107-
setStatus.bind(loadingState, LOADED),
108-
setStatus.bind(loadingState, ERRORED),
109-
);
110-
resourceMap.set(href, loadingState);
111-
dependencies.push(loadingState);
112-
113-
// The prior style resource is the last one placed at a given
114-
// precedence or the last resource itself which may be null.
115-
// We grab this value and then update the last resource for this
116-
// precedence to be the inserted element, updating the lastResource
117-
// pointer if needed.
118-
const prior = precedences.get(precedence) || lastResource;
119-
if (prior === lastResource) {
120-
lastResource = resourceEl;
121-
}
122-
precedences.set(precedence, resourceEl);
123-
124-
// Finally, we insert the newly constructed instance at an appropriate location
125-
// in the Document.
126-
if (prior) {
127-
prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
128-
} else {
129-
const head = thisDocument.head;
130-
head.insertBefore(resourceEl, head.firstChild);
131-
}
132-
}
133-
134-
Promise.all(dependencies).then(
135-
completeBoundaryImpl.bind(null, suspenseBoundaryID, contentID, ''),
136-
completeBoundaryImpl.bind(
137-
null,
138-
suspenseBoundaryID,
139-
contentID,
140-
'Resource failed to load',
141-
),
142-
);
143-
}
144-
14548
export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
14649
const contentNode = document.getElementById(contentID);
14750
// We'll detach the content node so that regardless of what happens next we don't leave in the tree.

0 commit comments

Comments
 (0)