@samitouri / QOS-React-2 / commits / 0b974418c9

[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

mofeiZ committed Jan 6, 2023 at 14:28 UTC 0b974418c9a56f6c560298560265dcf4b65784bc
9 files changed +249 -113
packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js
+1 -1
@@ -12,7 +12,7 @@ import {
12 completeBoundaryWithStyles,
13 completeBoundary,
14 completeSegment,
15 -} from './fizz-instruction-set/ReactDOMFizzInstructionSet';
15 +} from './fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime';
16
17 if (!window.$RC) {
18 // TODO: Eventually remove, we currently need to set these globals for
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineClientRenderBoundary.js
+1 -1
@@ -1,4 +1,4 @@
1 -import {clientRenderBoundary} from './ReactDOMFizzInstructionSet';
1 +import {clientRenderBoundary} from './ReactDOMFizzInstructionSetInlineSource';
2
3 // This is a string so Closure's advanced compilation mode doesn't mangle it.
4 // eslint-disable-next-line dot-notation
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js
+1 -1
@@ -1,4 +1,4 @@
1 -import {completeBoundary} from './ReactDOMFizzInstructionSet';
1 +import {completeBoundary} from './ReactDOMFizzInstructionSetInlineSource';
2
3 // This is a string so Closure's advanced compilation mode doesn't mangle it.
4 // eslint-disable-next-line dot-notation
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js
+1 -1
@@ -1,4 +1,4 @@
1 -import {completeBoundaryWithStyles} from './ReactDOMFizzInstructionSet';
1 +import {completeBoundaryWithStyles} from './ReactDOMFizzInstructionSetInlineSource';
2
3 // This is a string so Closure's advanced compilation mode doesn't mangle it.
4 // eslint-disable-next-line dot-notation
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteSegment.js
+1 -1
@@ -1,4 +1,4 @@
1 -import {completeSegment} from './ReactDOMFizzInstructionSet';
1 +import {completeSegment} from './ReactDOMFizzInstructionSetInlineSource';
2
3 // This is a string so Closure's advanced compilation mode doesn't mangle it.
4 // eslint-disable-next-line dot-notation
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js new
+113
@@ -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 +}
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js new
+116
@@ -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/ReactDOMFizzInstructionSetShared.js renamed
+10 -107
@@ -1,12 +1,15 @@
1 /* eslint-disable dot-notation */
2
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';
13
14 // TODO: Symbols that are referenced outside this module use dynamic accessor
15 // notation instead of dot notation to prevent Closure's advanced compilation
@@ -42,106 +45,6 @@ export function clientRenderBoundary(
45 }
46 }
47
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 -
48 export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
49 const contentNode = document.getElementById(contentID);
50 // We'll detach the content node so that regardless of what happens next we don't leave in the tree.
scripts/rollup/generate-inline-fizz-runtime.js
+5 -1
@@ -39,7 +39,11 @@ async function main() {
39 const fullEntryPath = instructionDir + '/' + entry;
40 const compiler = new ClosureCompiler({
41 entry_point: fullEntryPath,
42 - js: [fullEntryPath, instructionDir + '/ReactDOMFizzInstructionSet.js'],
42 + js: [
43 + fullEntryPath,
44 + instructionDir + '/ReactDOMFizzInstructionSetInlineSource.js',
45 + instructionDir + '/ReactDOMFizzInstructionSetShared.js',
46 + ],
47 compilation_level: 'ADVANCED',
48 module_resolution: 'NODE',
49 // This is necessary to prevent Closure from inlining a Promise polyfill