From 0b974418c9a56f6c560298560265dcf4b65784bc Mon Sep 17 00:00:00 2001 From: mofeiZ <34200447+mofeiZ@users.noreply.github.com> Date: Fri, 6 Jan 2023 14:28:55 -0500 Subject: [PATCH] [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 --- .../server/ReactDOMServerExternalRuntime.js | 2 +- .../ReactDOMFizzInlineClientRenderBoundary.js | 2 +- .../ReactDOMFizzInlineCompleteBoundary.js | 2 +- ...DOMFizzInlineCompleteBoundaryWithStyles.js | 2 +- .../ReactDOMFizzInlineCompleteSegment.js | 2 +- ...actDOMFizzInstructionSetExternalRuntime.js | 113 +++++++++++++++++ .../ReactDOMFizzInstructionSetInlineSource.js | 116 +++++++++++++++++ ...js => ReactDOMFizzInstructionSetShared.js} | 117 ++---------------- .../rollup/generate-inline-fizz-runtime.js | 6 +- 9 files changed, 249 insertions(+), 113 deletions(-) create mode 100644 packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js create mode 100644 packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js rename packages/react-dom-bindings/src/server/fizz-instruction-set/{ReactDOMFizzInstructionSet.js => ReactDOMFizzInstructionSetShared.js} (56%) diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js index 63f5b64d93..830db39970 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js @@ -12,7 +12,7 @@ import { completeBoundaryWithStyles, completeBoundary, completeSegment, -} from './fizz-instruction-set/ReactDOMFizzInstructionSet'; +} from './fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime'; if (!window.$RC) { // TODO: Eventually remove, we currently need to set these globals for diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineClientRenderBoundary.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineClientRenderBoundary.js index 96f750a8a4..8cfd59e8d1 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineClientRenderBoundary.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineClientRenderBoundary.js @@ -1,4 +1,4 @@ -import {clientRenderBoundary} from './ReactDOMFizzInstructionSet'; +import {clientRenderBoundary} from './ReactDOMFizzInstructionSetInlineSource'; // This is a string so Closure's advanced compilation mode doesn't mangle it. // eslint-disable-next-line dot-notation diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js index ed85f4e70a..403fe847d9 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js @@ -1,4 +1,4 @@ -import {completeBoundary} from './ReactDOMFizzInstructionSet'; +import {completeBoundary} from './ReactDOMFizzInstructionSetInlineSource'; // This is a string so Closure's advanced compilation mode doesn't mangle it. // eslint-disable-next-line dot-notation diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js index 62760ee543..e3ffd1f584 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js @@ -1,4 +1,4 @@ -import {completeBoundaryWithStyles} from './ReactDOMFizzInstructionSet'; +import {completeBoundaryWithStyles} from './ReactDOMFizzInstructionSetInlineSource'; // This is a string so Closure's advanced compilation mode doesn't mangle it. // eslint-disable-next-line dot-notation diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteSegment.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteSegment.js index dbccb338b5..d3dbd9eb50 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteSegment.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteSegment.js @@ -1,4 +1,4 @@ -import {completeSegment} from './ReactDOMFizzInstructionSet'; +import {completeSegment} from './ReactDOMFizzInstructionSetInlineSource'; // This is a string so Closure's advanced compilation mode doesn't mangle it. // eslint-disable-next-line dot-notation diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js new file mode 100644 index 0000000000..4598accc5d --- /dev/null +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js @@ -0,0 +1,113 @@ +/* eslint-disable dot-notation */ + +// Instruction set for the Fizz external runtime + +import { + clientRenderBoundary, + completeBoundary, + completeSegment, + LOADED, + ERRORED, +} from './ReactDOMFizzInstructionSetShared'; + +export {clientRenderBoundary, completeBoundary, completeSegment}; + +const resourceMap = new Map(); + +// This function is almost identical to the version used by inline scripts +// (ReactDOMFizzInstructionSetInlineSource), with the exception of how we read +// completeBoundary and resourceMap +export function completeBoundaryWithStyles( + suspenseBoundaryID, + contentID, + styles, +) { + const precedences = new Map(); + const thisDocument = document; + let lastResource, node; + + // Seed the precedence list with existing resources + const nodes = thisDocument.querySelectorAll( + 'link[data-precedence],style[data-precedence]', + ); + for (let i = 0; (node = nodes[i++]); ) { + precedences.set(node.dataset['precedence'], (lastResource = node)); + } + + let i = 0; + const dependencies = []; + let style, href, precedence, attr, loadingState, resourceEl; + + function setStatus(s) { + this['s'] = s; + } + + while ((style = styles[i++])) { + let j = 0; + href = style[j++]; + // We check if this resource is already in our resourceMap and reuse it if so. + // If it is already loaded we don't return it as a depenendency since there is nothing + // to wait for + loadingState = resourceMap.get(href); + if (loadingState) { + if (loadingState['s'] !== 'l') { + dependencies.push(loadingState); + } + continue; + } + + // We construct our new resource element, looping over remaining attributes if any + // setting them to the Element. + resourceEl = thisDocument.createElement('link'); + resourceEl.href = href; + resourceEl.rel = 'stylesheet'; + resourceEl.dataset['precedence'] = precedence = style[j++]; + while ((attr = style[j++])) { + resourceEl.setAttribute(attr, style[j++]); + } + + // We stash a pending promise in our map by href which will resolve or reject + // when the underlying resource loads or errors. We add it to the dependencies + // array to be returned. + loadingState = resourceEl['_p'] = new Promise((re, rj) => { + resourceEl.onload = re; + resourceEl.onerror = rj; + }); + loadingState.then( + setStatus.bind(loadingState, LOADED), + setStatus.bind(loadingState, ERRORED), + ); + resourceMap.set(href, loadingState); + dependencies.push(loadingState); + + // The prior style resource is the last one placed at a given + // precedence or the last resource itself which may be null. + // We grab this value and then update the last resource for this + // precedence to be the inserted element, updating the lastResource + // pointer if needed. + const prior = precedences.get(precedence) || lastResource; + if (prior === lastResource) { + lastResource = resourceEl; + } + precedences.set(precedence, resourceEl); + + // Finally, we insert the newly constructed instance at an appropriate location + // in the Document. + if (prior) { + prior.parentNode.insertBefore(resourceEl, prior.nextSibling); + } else { + const head = thisDocument.head; + head.insertBefore(resourceEl, head.firstChild); + } + } + + Promise.all(dependencies).then( + completeBoundary.bind(null, suspenseBoundaryID, contentID, ''), + completeBoundary.bind( + null, + suspenseBoundaryID, + contentID, + 'Resource failed to load', + ), + ); +} diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js new file mode 100644 index 0000000000..3c96d1a15a --- /dev/null +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js @@ -0,0 +1,116 @@ +/* eslint-disable dot-notation */ + +// Instruction set for Fizz inline scripts. +// DO NOT DIRECTLY IMPORT THIS FILE. This is the source for the compiled and +// minified code in ReactDOMFizzInstructionSetInlineCodeStrings. + +import { + clientRenderBoundary, + completeBoundary, + completeSegment, + LOADED, + ERRORED, +} from './ReactDOMFizzInstructionSetShared'; + +export {clientRenderBoundary, completeBoundary, completeSegment}; + +// This function is almost identical to the version used by the external +// runtime (ReactDOMFizzInstructionSetExternalRuntime), with the exception of +// how we read completeBoundaryImpl and resourceMap +export function completeBoundaryWithStyles( + suspenseBoundaryID, + contentID, + styles, +) { + const completeBoundaryImpl = window['$RC']; + const resourceMap = window['$RM']; + + const precedences = new Map(); + const thisDocument = document; + let lastResource, node; + + // Seed the precedence list with existing resources + const nodes = thisDocument.querySelectorAll( + 'link[data-precedence],style[data-precedence]', + ); + for (let i = 0; (node = nodes[i++]); ) { + precedences.set(node.dataset['precedence'], (lastResource = node)); + } + + let i = 0; + const dependencies = []; + let style, href, precedence, attr, loadingState, resourceEl; + + function setStatus(s) { + this['s'] = s; + } + + while ((style = styles[i++])) { + let j = 0; + href = style[j++]; + // We check if this resource is already in our resourceMap and reuse it if so. + // If it is already loaded we don't return it as a depenendency since there is nothing + // to wait for + loadingState = resourceMap.get(href); + if (loadingState) { + if (loadingState['s'] !== 'l') { + dependencies.push(loadingState); + } + continue; + } + + // We construct our new resource element, looping over remaining attributes if any + // setting them to the Element. + resourceEl = thisDocument.createElement('link'); + resourceEl.href = href; + resourceEl.rel = 'stylesheet'; + resourceEl.dataset['precedence'] = precedence = style[j++]; + while ((attr = style[j++])) { + resourceEl.setAttribute(attr, style[j++]); + } + + // We stash a pending promise in our map by href which will resolve or reject + // when the underlying resource loads or errors. We add it to the dependencies + // array to be returned. + loadingState = resourceEl['_p'] = new Promise((re, rj) => { + resourceEl.onload = re; + resourceEl.onerror = rj; + }); + loadingState.then( + setStatus.bind(loadingState, LOADED), + setStatus.bind(loadingState, ERRORED), + ); + resourceMap.set(href, loadingState); + dependencies.push(loadingState); + + // The prior style resource is the last one placed at a given + // precedence or the last resource itself which may be null. + // We grab this value and then update the last resource for this + // precedence to be the inserted element, updating the lastResource + // pointer if needed. + const prior = precedences.get(precedence) || lastResource; + if (prior === lastResource) { + lastResource = resourceEl; + } + precedences.set(precedence, resourceEl); + + // Finally, we insert the newly constructed instance at an appropriate location + // in the Document. + if (prior) { + prior.parentNode.insertBefore(resourceEl, prior.nextSibling); + } else { + const head = thisDocument.head; + head.insertBefore(resourceEl, head.firstChild); + } + } + + Promise.all(dependencies).then( + completeBoundaryImpl.bind(null, suspenseBoundaryID, contentID, ''), + completeBoundaryImpl.bind( + null, + suspenseBoundaryID, + contentID, + 'Resource failed to load', + ), + ); +} diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSet.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js similarity index 56% rename from packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSet.js rename to packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js index 4abe722309..a2a8e402e2 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSet.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js @@ -1,12 +1,15 @@ /* eslint-disable dot-notation */ -const COMMENT_NODE = 8; -const SUSPENSE_START_DATA = '$'; -const SUSPENSE_END_DATA = '/$'; -const SUSPENSE_PENDING_START_DATA = '$?'; -const SUSPENSE_FALLBACK_START_DATA = '$!'; -const LOADED = 'l'; -const ERRORED = 'e'; +// Shared implementation and constants between the inline script and external +// runtime instruction sets. + +export const COMMENT_NODE = 8; +export const SUSPENSE_START_DATA = '$'; +export const SUSPENSE_END_DATA = '/$'; +export const SUSPENSE_PENDING_START_DATA = '$?'; +export const SUSPENSE_FALLBACK_START_DATA = '$!'; +export const LOADED = 'l'; +export const ERRORED = 'e'; // TODO: Symbols that are referenced outside this module use dynamic accessor // notation instead of dot notation to prevent Closure's advanced compilation @@ -42,106 +45,6 @@ export function clientRenderBoundary( } } -export function completeBoundaryWithStyles( - suspenseBoundaryID, - contentID, - styles, -) { - // TODO: In the non-inline version of the runtime, these don't need to be read - // from the global scope. - const completeBoundaryImpl = window['$RC']; - const resourceMap = window['$RM']; - - const precedences = new Map(); - const thisDocument = document; - let lastResource, node; - - // Seed the precedence list with existing resources - const nodes = thisDocument.querySelectorAll( - 'link[data-precedence],style[data-precedence]', - ); - for (let i = 0; (node = nodes[i++]); ) { - precedences.set(node.dataset['precedence'], (lastResource = node)); - } - - let i = 0; - const dependencies = []; - let style, href, precedence, attr, loadingState, resourceEl; - - function setStatus(s) { - this['s'] = s; - } - - while ((style = styles[i++])) { - let j = 0; - href = style[j++]; - // We check if this resource is already in our resourceMap and reuse it if so. - // If it is already loaded we don't return it as a depenendency since there is nothing - // to wait for - loadingState = resourceMap.get(href); - if (loadingState) { - if (loadingState['s'] !== 'l') { - dependencies.push(loadingState); - } - continue; - } - - // We construct our new resource element, looping over remaining attributes if any - // setting them to the Element. - resourceEl = thisDocument.createElement('link'); - resourceEl.href = href; - resourceEl.rel = 'stylesheet'; - resourceEl.dataset['precedence'] = precedence = style[j++]; - while ((attr = style[j++])) { - resourceEl.setAttribute(attr, style[j++]); - } - - // We stash a pending promise in our map by href which will resolve or reject - // when the underlying resource loads or errors. We add it to the dependencies - // array to be returned. - loadingState = resourceEl['_p'] = new Promise((re, rj) => { - resourceEl.onload = re; - resourceEl.onerror = rj; - }); - loadingState.then( - setStatus.bind(loadingState, LOADED), - setStatus.bind(loadingState, ERRORED), - ); - resourceMap.set(href, loadingState); - dependencies.push(loadingState); - - // The prior style resource is the last one placed at a given - // precedence or the last resource itself which may be null. - // We grab this value and then update the last resource for this - // precedence to be the inserted element, updating the lastResource - // pointer if needed. - const prior = precedences.get(precedence) || lastResource; - if (prior === lastResource) { - lastResource = resourceEl; - } - precedences.set(precedence, resourceEl); - - // Finally, we insert the newly constructed instance at an appropriate location - // in the Document. - if (prior) { - prior.parentNode.insertBefore(resourceEl, prior.nextSibling); - } else { - const head = thisDocument.head; - head.insertBefore(resourceEl, head.firstChild); - } - } - - Promise.all(dependencies).then( - completeBoundaryImpl.bind(null, suspenseBoundaryID, contentID, ''), - completeBoundaryImpl.bind( - null, - suspenseBoundaryID, - contentID, - 'Resource failed to load', - ), - ); -} - export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) { const contentNode = document.getElementById(contentID); // We'll detach the content node so that regardless of what happens next we don't leave in the tree. diff --git a/scripts/rollup/generate-inline-fizz-runtime.js b/scripts/rollup/generate-inline-fizz-runtime.js index d116c0620f..226c844561 100644 --- a/scripts/rollup/generate-inline-fizz-runtime.js +++ b/scripts/rollup/generate-inline-fizz-runtime.js @@ -39,7 +39,11 @@ async function main() { const fullEntryPath = instructionDir + '/' + entry; const compiler = new ClosureCompiler({ entry_point: fullEntryPath, - js: [fullEntryPath, instructionDir + '/ReactDOMFizzInstructionSet.js'], + js: [ + fullEntryPath, + instructionDir + '/ReactDOMFizzInstructionSetInlineSource.js', + instructionDir + '/ReactDOMFizzInstructionSetShared.js', + ], compilation_level: 'ADVANCED', module_resolution: 'NODE', // This is necessary to prevent Closure from inlining a Promise polyfill