Skip to content

Commit

Permalink
[Fizz] Fork Fizz instruction set for inline script and external runti…
Browse files Browse the repository at this point in the history
…me (#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
  • Loading branch information
mofeiZ committed Jan 6, 2023
1 parent 5379b61 commit 0b97441
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
),
);
}
Original file line number Diff line number Diff line change
@@ -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',
),
);
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 0b97441

Please sign in to comment.