[Fizz] Share code between inline and external runtime (#33066)

Stacked on #33065.

The runtime is about to be a lot more complicated so we need to start
sharing some more code.

The problem with sharing code is that we want the inline runtime to as
much as possible be isolated in its scope using only a few global
variables to refer across runtimes.

A problem with Closure Compiler is that it refuses to inline functions
if they have closures inside of them. Which makes sense because of how
VMs work it can cause memory leaks. However, in our cases this doesn't
matter and code size matters more. So we can't use many clever tricks.

So this just favors writing the source in the inline form. Then we add
an extra compiler pass to turn those global variables into local
variables in the external runtime.
This commit is contained in:
Sebastian Markbåge
2025-05-01 14:25:10 -04:00
committed by GitHub
parent e9db3cc2d4
commit bb57fa7351
12 changed files with 190 additions and 291 deletions

View File

@@ -7,12 +7,7 @@
// Imports are resolved statically by the closure compiler in release bundles
// and by rollup in jest unit tests
import {
clientRenderBoundary,
completeBoundaryWithStyles,
completeBoundary,
completeSegment,
} from './fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime';
import './fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime';
if (document.body != null) {
if (document.readyState === 'loading') {
@@ -82,7 +77,7 @@ function handleNode(node_: Node) {
const node = (node_: HTMLElement);
const dataset = node.dataset;
if (dataset['rxi'] != null) {
clientRenderBoundary(
window['$RX'](
dataset['bid'],
dataset['dgst'],
dataset['msg'],
@@ -92,17 +87,13 @@ function handleNode(node_: Node) {
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']),
);
window['$RR'](dataset['bid'], dataset['sid'], JSON.parse(dataset['sty']));
node.remove();
} else if (dataset['rci'] != null) {
completeBoundary(dataset['bid'], dataset['sid']);
window['$RC'](dataset['bid'], dataset['sid']);
node.remove();
} else if (dataset['rsi'] != null) {
completeSegment(dataset['sid'], dataset['pid']);
window['$RS'](dataset['sid'], dataset['pid']);
node.remove();
}
}

View File

@@ -1,4 +1,4 @@
import {clientRenderBoundary} from './ReactDOMFizzInstructionSetInlineSource';
import {clientRenderBoundary} from './ReactDOMFizzInstructionSetShared';
// 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 './ReactDOMFizzInstructionSetInlineSource';
import {completeBoundary} from './ReactDOMFizzInstructionSetShared';
// 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 './ReactDOMFizzInstructionSetInlineSource';
import {completeBoundaryWithStyles} from './ReactDOMFizzInstructionSetShared';
// 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 './ReactDOMFizzInstructionSetInlineSource';
import {completeSegment} from './ReactDOMFizzInstructionSetShared';
// This is a string so Closure's advanced compilation mode doesn't mangle it.
// eslint-disable-next-line dot-notation

View File

@@ -5,138 +5,17 @@
import {
clientRenderBoundary,
completeBoundary,
completeBoundaryWithStyles,
completeSegment,
listenToFormSubmissionsForReplaying,
} 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,
stylesheetDescriptors,
) {
const precedences = new Map();
const thisDocument = document;
let lastResource, node;
// Seed the precedence list with existing resources and collect hoistable style tags
const nodes = thisDocument.querySelectorAll(
'link[data-precedence],style[data-precedence]',
);
const styleTagsToHoist = [];
for (let i = 0; (node = nodes[i++]); ) {
if (node.getAttribute('media') === 'not all') {
styleTagsToHoist.push(node);
} else {
if (node.tagName === 'LINK') {
resourceMap.set(node.getAttribute('href'), node);
}
precedences.set(node.dataset['precedence'], (lastResource = node));
}
}
let i = 0;
const dependencies = [];
let href, precedence, attr, loadingState, resourceEl, media;
function cleanupWith(cb) {
this['_p'] = null;
cb();
}
// Sheets Mode
let sheetMode = true;
while (true) {
if (sheetMode) {
// Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for
// dependency if they already existed
const stylesheetDescriptor = stylesheetDescriptors[i++];
if (!stylesheetDescriptor) {
// enter <style> Mode
sheetMode = false;
i = 0;
continue;
}
let avoidInsert = false;
let j = 0;
href = stylesheetDescriptor[j++];
if ((resourceEl = resourceMap.get(href))) {
// We have an already inserted stylesheet.
loadingState = resourceEl['_p'];
avoidInsert = true;
} else {
// We haven't already processed this href so we need to construct a stylesheet and hoist it
// We construct it here and attach a loadingState. We also check whether it matches
// media before we include it in the dependency array.
resourceEl = thisDocument.createElement('link');
resourceEl.href = href;
resourceEl.rel = 'stylesheet';
resourceEl.dataset['precedence'] = precedence =
stylesheetDescriptor[j++];
while ((attr = stylesheetDescriptor[j++])) {
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
}
loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
resourceEl.onload = cleanupWith.bind(resourceEl, resolve);
resourceEl.onerror = cleanupWith.bind(resourceEl, reject);
});
// Save this resource element so we can bailout if it is used again
resourceMap.set(href, resourceEl);
}
media = resourceEl.getAttribute('media');
if (loadingState && (!media || window['matchMedia'](media).matches)) {
dependencies.push(loadingState);
}
if (avoidInsert) {
// We have a link that is already in the document. We don't want to fall through to the insert path
continue;
}
} else {
// <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.
resourceEl = styleTagsToHoist[i++];
if (!resourceEl) {
// we are done with all style tags
break;
}
precedence = resourceEl.getAttribute('data-precedence');
resourceEl.removeAttribute('media');
}
// resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting
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',
),
);
}
// This is a string so Closure's advanced compilation mode doesn't mangle it.
// These will be renamed to local references by the external-runtime-plugin.
window['$RM'] = new Map();
window['$RX'] = clientRenderBoundary;
window['$RC'] = completeBoundary;
window['$RR'] = completeBoundaryWithStyles;
window['$RS'] = completeSegment;
listenToFormSubmissionsForReplaying();

View File

@@ -6,7 +6,7 @@ export const clientRenderBoundary =
export const completeBoundary =
'$RC=function(b,d,e){if(d=document.getElementById(d)){d.parentNode.removeChild(d);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var c=a.data;if("/$"===c||"/&"===c)if(0===f)break;else f--;else"$"!==c&&"$?"!==c&&"$!"!==c&&"&"!==c||f++}c=a.nextSibling;e.removeChild(a);a=c}while(a);for(;d.firstChild;)e.insertBefore(d.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}}};';
export const completeBoundaryWithStyles =
'$RM=new Map;\n$RR=function(t,u,y){function v(n){this._p=null;n()}for(var w=$RC,p=$RM,q=new Map,r=document,g,b,h=r.querySelectorAll("link[data-precedence],style[data-precedence]"),x=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?x.push(b):("LINK"===b.tagName&&p.set(b.getAttribute("href"),b),q.set(b.dataset.precedence,g=b));b=0;h=[];var l,a;for(k=!0;;){if(k){var e=y[b++];if(!e){k=!1;b=0;continue}var c=!1,m=0;var d=e[m++];if(a=p.get(d)){var f=a._p;c=!0}else{a=r.createElement("link");a.href=\nd;a.rel="stylesheet";for(a.dataset.precedence=l=e[m++];f=e[m++];)a.setAttribute(f,e[m++]);f=a._p=new Promise(function(n,z){a.onload=v.bind(a,n);a.onerror=v.bind(a,z)});p.set(d,a)}d=a.getAttribute("media");!f||d&&!matchMedia(d).matches||h.push(f);if(c)continue}else{a=x[b++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=q.get(l)||g;c===g&&(g=a);q.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=r.head,c.insertBefore(a,c.firstChild))}Promise.all(h).then(w.bind(null,\nt,u,""),w.bind(null,t,u,"Resource failed to load"))};';
'$RM=new Map;\n$RR=function(r,t,w){function u(n){this._p=null;n()}for(var p=new Map,q=document,g,b,h=q.querySelectorAll("link[data-precedence],style[data-precedence]"),v=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?v.push(b):("LINK"===b.tagName&&$RM.set(b.getAttribute("href"),b),p.set(b.dataset.precedence,g=b));b=0;h=[];var l,a;for(k=!0;;){if(k){var e=w[b++];if(!e){k=!1;b=0;continue}var c=!1,m=0;var d=e[m++];if(a=$RM.get(d)){var f=a._p;c=!0}else{a=q.createElement("link");a.href=d;a.rel=\n"stylesheet";for(a.dataset.precedence=l=e[m++];f=e[m++];)a.setAttribute(f,e[m++]);f=a._p=new Promise(function(n,x){a.onload=u.bind(a,n);a.onerror=u.bind(a,x)});$RM.set(d,a)}d=a.getAttribute("media");!f||d&&!matchMedia(d).matches||h.push(f);if(c)continue}else{a=v[b++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=p.get(l)||g;c===g&&(g=a);p.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=q.head,c.insertBefore(a,c.firstChild))}Promise.all(h).then($RC.bind(null,\nr,t,""),$RC.bind(null,r,t,"Resource failed to load"))};';
export const completeSegment =
'$RS=function(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)};';
export const formReplaying =

View File

@@ -1,142 +0,0 @@
/* 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,
} 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,
stylesheetDescriptors,
) {
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 and collect hoistable style tags
const nodes = thisDocument.querySelectorAll(
'link[data-precedence],style[data-precedence]',
);
const styleTagsToHoist = [];
for (let i = 0; (node = nodes[i++]); ) {
if (node.getAttribute('media') === 'not all') {
styleTagsToHoist.push(node);
} else {
if (node.tagName === 'LINK') {
resourceMap.set(node.getAttribute('href'), node);
}
precedences.set(node.dataset['precedence'], (lastResource = node));
}
}
let i = 0;
const dependencies = [];
let href, precedence, attr, loadingState, resourceEl, media;
function cleanupWith(cb) {
this['_p'] = null;
cb();
}
// Sheets Mode
let sheetMode = true;
while (true) {
if (sheetMode) {
// Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for
// dependency if they already existed
const stylesheetDescriptor = stylesheetDescriptors[i++];
if (!stylesheetDescriptor) {
// enter <style> Mode
sheetMode = false;
i = 0;
continue;
}
let avoidInsert = false;
let j = 0;
href = stylesheetDescriptor[j++];
if ((resourceEl = resourceMap.get(href))) {
// We have an already inserted stylesheet.
loadingState = resourceEl['_p'];
avoidInsert = true;
} else {
// We haven't already processed this href so we need to construct a stylesheet and hoist it
// We construct it here and attach a loadingState. We also check whether it matches
// media before we include it in the dependency array.
resourceEl = thisDocument.createElement('link');
resourceEl.href = href;
resourceEl.rel = 'stylesheet';
resourceEl.dataset['precedence'] = precedence =
stylesheetDescriptor[j++];
while ((attr = stylesheetDescriptor[j++])) {
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
}
loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
resourceEl.onload = cleanupWith.bind(resourceEl, resolve);
resourceEl.onerror = cleanupWith.bind(resourceEl, reject);
});
// Save this resource element so we can bailout if it is used again
resourceMap.set(href, resourceEl);
}
media = resourceEl.getAttribute('media');
if (loadingState && (!media || window['matchMedia'](media).matches)) {
dependencies.push(loadingState);
}
if (avoidInsert) {
// We have a link that is already in the document. We don't want to fall through to the insert path
continue;
}
} else {
// <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.
resourceEl = styleTagsToHoist[i++];
if (!resourceEl) {
// we are done with all style tags
break;
}
precedence = resourceEl.getAttribute('data-precedence');
resourceEl.removeAttribute('media');
}
// resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting
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

@@ -121,6 +121,129 @@ export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
}
}
export function completeBoundaryWithStyles(
suspenseBoundaryID,
contentID,
stylesheetDescriptors,
) {
const precedences = new Map();
const thisDocument = document;
let lastResource, node;
// Seed the precedence list with existing resources and collect hoistable style tags
const nodes = thisDocument.querySelectorAll(
'link[data-precedence],style[data-precedence]',
);
const styleTagsToHoist = [];
for (let i = 0; (node = nodes[i++]); ) {
if (node.getAttribute('media') === 'not all') {
styleTagsToHoist.push(node);
} else {
if (node.tagName === 'LINK') {
window['$RM'].set(node.getAttribute('href'), node);
}
precedences.set(node.dataset['precedence'], (lastResource = node));
}
}
let i = 0;
const dependencies = [];
let href, precedence, attr, loadingState, resourceEl, media;
function cleanupWith(cb) {
this['_p'] = null;
cb();
}
// Sheets Mode
let sheetMode = true;
while (true) {
if (sheetMode) {
// Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for
// dependency if they already existed
const stylesheetDescriptor = stylesheetDescriptors[i++];
if (!stylesheetDescriptor) {
// enter <style> Mode
sheetMode = false;
i = 0;
continue;
}
let avoidInsert = false;
let j = 0;
href = stylesheetDescriptor[j++];
if ((resourceEl = window['$RM'].get(href))) {
// We have an already inserted stylesheet.
loadingState = resourceEl['_p'];
avoidInsert = true;
} else {
// We haven't already processed this href so we need to construct a stylesheet and hoist it
// We construct it here and attach a loadingState. We also check whether it matches
// media before we include it in the dependency array.
resourceEl = thisDocument.createElement('link');
resourceEl.href = href;
resourceEl.rel = 'stylesheet';
resourceEl.dataset['precedence'] = precedence =
stylesheetDescriptor[j++];
while ((attr = stylesheetDescriptor[j++])) {
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
}
loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
resourceEl.onload = cleanupWith.bind(resourceEl, resolve);
resourceEl.onerror = cleanupWith.bind(resourceEl, reject);
});
// Save this resource element so we can bailout if it is used again
window['$RM'].set(href, resourceEl);
}
media = resourceEl.getAttribute('media');
if (loadingState && (!media || window['matchMedia'](media).matches)) {
dependencies.push(loadingState);
}
if (avoidInsert) {
// We have a link that is already in the document. We don't want to fall through to the insert path
continue;
}
} else {
// <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.
resourceEl = styleTagsToHoist[i++];
if (!resourceEl) {
// we are done with all style tags
break;
}
precedence = resourceEl.getAttribute('data-precedence');
resourceEl.removeAttribute('media');
}
// resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting
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(
window['$RC'].bind(null, suspenseBoundaryID, contentID, ''),
window['$RC'].bind(
null,
suspenseBoundaryID,
contentID,
'Resource failed to load',
),
);
}
export function completeSegment(containerID, placeholderID) {
const segmentContainer = document.getElementById(containerID);
const placeholderNode = document.getElementById(placeholderID);

View File

@@ -21,6 +21,7 @@ const Sync = require('./sync');
const sizes = require('./plugins/sizes-plugin');
const useForks = require('./plugins/use-forks-plugin');
const dynamicImports = require('./plugins/dynamic-imports');
const externalRuntime = require('./plugins/external-runtime-plugin');
const Packaging = require('./packaging');
const {asyncRimRaf} = require('./utils');
const codeFrame = require('@babel/code-frame').default;
@@ -441,6 +442,8 @@ function getPlugins(
__EXPERIMENTAL__,
},
}),
// For the external runtime we turn global identifiers into local.
entry.includes('server-external-runtime') && externalRuntime(),
{
name: 'top-level-definitions',
renderChunk(source) {

View File

@@ -46,7 +46,6 @@ async function main() {
js: [
require.resolve('./externs/closure-externs.js'),
fullEntryPath,
instructionDir + '/ReactDOMFizzInstructionSetInlineSource.js',
instructionDir + '/ReactDOMFizzInstructionSetShared.js',
],
compilation_level: 'ADVANCED',

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
module.exports = function externalRuntime() {
// When generating the source code for the Fizz runtime chunks we use global identifiers to refer
// to different parts of the implementation. When generating the external runtime we need to
// replace those with local identifiers instead.
return {
name: 'scripts/rollup/plugins/dynamic-imports',
renderChunk(source) {
// This replaces "window['$globalVar']" with "$globalVar".
const variables = new Set();
source = source.replace(
/window\[['"](\$[A-z0-9_]*)['"]\]/g,
(_, variableName) => {
variables.add(variableName);
return variableName;
}
);
const startOfFn = 'use strict';
let index = source.indexOf(startOfFn);
if (index === -1) {
return source;
}
index += startOfFn.length + 2;
// Insert the declarations in the beginning of the function closure
// to scope them to inside the runtime.
let declarations = 'let ';
variables.forEach(variable => {
if (declarations !== 'let ') {
declarations += ', ';
}
declarations += variable;
});
declarations += ';';
source = source.slice(0, index) + declarations + source.slice(index);
return source;
},
};
};