Skip to content

Commit

Permalink
[ServerRenderer] Add option to send instructions as data attributes (f…
Browse files Browse the repository at this point in the history
…acebook#25437)

### Changes made:
- Running with enableFizzExternalRuntime (feature flag) and
unstable_externalRuntimeSrc (param) will generate html nodes with data
attributes that encode Fizz instructions.
```
<div 
  hidden data-rxi=""
  data-bid="param0"
  data-dgst="param1"
></div>
```
- Added an external runtime browser script
`ReactDOMServerExternalRuntime`, which processes and removes these nodes
- This runtime should be passed as to renderInto[...] via
`unstable_externalRuntimeSrc`
- Since this runtime is render blocking (for all streamed suspense
boundaries and segments), we want this to reach the client as early as
possible. By default, Fizz will send this script at the end of the shell
when it detects dynamic content (e.g. suspenseful pending tasks), but it
can be sent even earlier by calling `preinit(...)` inside a component.
- The current implementation relies on Float to dedupe sending
`unstable_externalRuntimeSrc`, so `enableFizzExternalRuntime` is only
valid when `enableFloat` is also set.
  • Loading branch information
mofeiZ committed Dec 5, 2022
1 parent 0e858a5 commit 83b74c9
Show file tree
Hide file tree
Showing 12 changed files with 845 additions and 151 deletions.
14 changes: 12 additions & 2 deletions packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ type PreinitOptions = {
crossOrigin?: string,
integrity?: string,
};
function preinit(href: string, options: PreinitOptions) {
function preinit(href: string, options: PreinitOptions): void {
if (!currentResources) {
// While we expect that preinit calls are primarily going to be observed
// during render because effects and events don't run on the server it is
Expand All @@ -285,7 +285,17 @@ function preinit(href: string, options: PreinitOptions) {
// simply return and do not warn.
return;
}
const resources = currentResources;
preinitImpl(currentResources, href, options);
}

// On the server, preinit may be called outside of render when sending an
// external SSR runtime as part of the initial resources payload. Since this
// is an internal React call, we do not need to use the resources stack.
export function preinitImpl(
resources: Resources,
href: string,
options: PreinitOptions,
): void {
if (__DEV__) {
validatePreinitArguments(href, options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* clients. Therefore, it should be fast and not have many external dependencies.
* @flow
*/
/* eslint-disable dot-notation */

// Imports are resolved statically by the closure compiler in release bundles
// and by rollup in jest unit tests
Expand All @@ -13,13 +14,94 @@ import {
completeSegment,
} from './fizz-instruction-set/ReactDOMFizzInstructionSet';

// Intentionally does nothing. Implementation will be added in future PR.
// eslint-disable-next-line no-unused-vars
const observer = new MutationObserver(mutations => {
// These are only called so I can check what the module output looks like. The
// code is unreachable.
clientRenderBoundary();
completeBoundaryWithStyles();
completeBoundary();
completeSegment();
});
if (!window.$RC) {
// TODO: Eventually remove, we currently need to set these globals for
// compatibility with ReactDOMFizzInstructionSet
window.$RC = completeBoundary;
window.$RM = new Map();
}

if (document.readyState === 'loading') {
if (document.body != null) {
installFizzInstrObserver(document.body);
} else {
// body may not exist yet if the fizz runtime is sent in <head>
// (e.g. as a preinit resource)
const domBodyObserver = new MutationObserver(() => {
// We expect the body node to be stable once parsed / created
if (document.body) {
if (document.readyState === 'loading') {
installFizzInstrObserver(document.body);
}
handleExistingNodes();
domBodyObserver.disconnect();
}
});
// documentElement must already exist at this point
// $FlowFixMe[incompatible-call]
domBodyObserver.observe(document.documentElement, {childList: true});
}
}

handleExistingNodes();

function handleExistingNodes() {
const existingNodes = document.getElementsByTagName('template');
for (let i = 0; i < existingNodes.length; i++) {
handleNode(existingNodes[i]);
}
}

function installFizzInstrObserver(target /*: Node */) {
const fizzInstrObserver = new MutationObserver(mutations => {
for (let i = 0; i < mutations.length; i++) {
const addedNodes = mutations[i].addedNodes;
for (let j = 0; j < addedNodes.length; j++) {
if (addedNodes.item(j).parentNode) {
handleNode(addedNodes.item(j));
}
}
}
});
// We assume that instruction data nodes are eventually appended to the
// body, even if Fizz is streaming to a shell / subtree.
fizzInstrObserver.observe(target, {
childList: true,
});
window.addEventListener('DOMContentLoaded', () => {
fizzInstrObserver.disconnect();
});
}

function handleNode(node_ /*: Node */) {
// $FlowFixMe[incompatible-cast]
if (node_.nodeType !== 1 || !(node_ /*: HTMLElement*/).dataset) {
return;
}
// $FlowFixMe[incompatible-cast]
const node = (node_ /*: HTMLElement*/);
const dataset = node.dataset;
if (dataset['rxi'] != null) {
clientRenderBoundary(
dataset['bid'],
dataset['dgst'],
dataset['msg'],
dataset['stck'],
);
node.remove();
} else if (dataset['rri'] != null) {
// Convert styles here, since its type is Array<Array<string>>
completeBoundaryWithStyles(
dataset['bid'],
dataset['sid'],
JSON.parse(dataset['sty']),
);
node.remove();
} else if (dataset['rci'] != null) {
completeBoundary(dataset['bid'], dataset['sid']);
node.remove();
} else if (dataset['rsi'] != null) {
completeSegment(dataset['sid'], dataset['pid']);
node.remove();
}
}
Loading

0 comments on commit 83b74c9

Please sign in to comment.