[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
This commit is contained in:
mofeiZ 2023-01-06 14:28:55 -05:00 committed by GitHub
parent 5379b6123f
commit 0b974418c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 249 additions and 113 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',
),
);
}

View File

@ -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',
),
);
}

View File

@ -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.

View File

@ -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