[Float][Fizz][Fiber]: Refactor <style> Resource implementation to group on flush (#26280)

There is a problem with <style> as resource. For css-in-js libs there
may be an very large number of these hoistables being created. The
number of style tags can grow quickly and to help reduce the prevalence
of this FIzz now aggregates all style tags for a given precedence into a
single tag. The client can 'hydrate' against these compound tags but
currently on the client insertions are done individually.

additionally drops the implementation where style tags are embedding in
a template for one where `media="not all"` is set. The idea is to have
the browser construct the underlying stylesheet eagerly which does not
happen if the tag is embedded in a template

Key Decision:
One choice made in this PR is that we flush style tags eagerly even if a
boundary is blocked that is the only thing that depends on that style
rule. The reason we are starting with this implementation is that it
allows a very condensed representation of the style resources. If we
tracked which rules were used in which boundaries we would need a style
resource for every rendered <style> tag. This could be problematic for
css-in-js libs that might render hundreds or thousands of style tags.
The tradeoff here is we slightly delay content reveal in some cases (we
send extra bytes) but we have fewer DOM tags and faster SSR runtime
This commit is contained in:
Josh Story 2023-03-03 12:50:35 -08:00 committed by GitHub
parent 5c633a48f9
commit 1f1f8eb559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 703 additions and 379 deletions

View File

@ -663,7 +663,7 @@ function styleTagPropsFromRawProps(
function getStyleKey(href: string) {
const limitedEscapedHref =
escapeSelectorAttributeValueInsideDoubleQuotes(href);
return `href="${limitedEscapedHref}"`;
return `href~="${limitedEscapedHref}"`;
}
function getStyleTagSelectorFromKey(key: string) {

View File

@ -153,6 +153,11 @@ export type ResponseState = {
preloadChunks: Array<Chunk | PrecomputedChunk>,
hoistableChunks: Array<Chunk | PrecomputedChunk>,
// Module-global-like reference for flushing/hoisting state of style resources
// We need to track whether the current request has flushed any style resources
// without sending an instruction to hoist them. we do that here
stylesToHoist: boolean,
// We allow the legacy renderer to extend this object.
...
@ -300,6 +305,7 @@ export function createResponseState(
preconnectChunks: [],
preloadChunks: [],
hoistableChunks: [],
stylesToHoist: false,
};
}
@ -1421,7 +1427,7 @@ function pushLink(
resource = {
type: 'stylesheet',
chunks: ([]: Array<Chunk | PrecomputedChunk>),
state: resources.boundaryResources ? Blocked : NoState,
state: NoState,
props: resourceProps,
};
resources.stylesMap.set(key, resource);
@ -1432,6 +1438,25 @@ function pushLink(
if (!precedenceSet) {
precedenceSet = new Set();
resources.precedences.set(precedence, precedenceSet);
const emptyStyleResource = {
type: 'style',
chunks: ([]: Array<Chunk | PrecomputedChunk>),
state: NoState,
props: {
precedence,
hrefs: ([]: Array<string>),
},
};
precedenceSet.add(emptyStyleResource);
if (__DEV__) {
if (resources.stylePrecedences.has(precedence)) {
console.error(
'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
precedence,
);
}
}
resources.stylePrecedences.set(precedence, emptyStyleResource);
}
precedenceSet.add(resource);
}
@ -1556,30 +1581,49 @@ function pushStyle(
return pushStyleImpl(target, props);
}
if (__DEV__) {
if (href.includes(' ')) {
console.error(
'React expected the `href` prop for a <style> tag opting into hoisting semantics using the `precedence` prop to not have any spaces but ecountered spaces instead. using spaces in this prop will cause hydration of this style to fail on the client. The href for the <style> where this ocurred is "%s".',
href,
);
}
}
const key = getResourceKey('style', href);
let resource = resources.stylesMap.get(key);
if (!resource) {
resource = {
type: 'style',
chunks: ([]: Array<Chunk | PrecomputedChunk>),
state: resources.boundaryResources ? Blocked : NoState,
props: styleTagPropsFromRawProps(props),
};
resources.stylesMap.set(key, resource);
if (__DEV__) {
markAsRenderedResourceDEV(resource, props);
}
pushStyleImpl(resource.chunks, resource.props);
let precedenceSet = resources.precedences.get(precedence);
if (!precedenceSet) {
precedenceSet = new Set();
resource = resources.stylePrecedences.get(precedence);
if (!resource) {
resource = {
type: 'style',
chunks: [],
state: NoState,
props: {
precedence,
hrefs: [href],
},
};
resources.stylePrecedences.set(precedence, resource);
const precedenceSet: Set<StyleResource> = new Set();
precedenceSet.add(resource);
if (__DEV__) {
if (resources.precedences.has(precedence)) {
console.error(
'React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.',
precedence,
);
}
}
resources.precedences.set(precedence, precedenceSet);
} else {
resource.props.hrefs.push(href);
}
precedenceSet.add(resource);
resources.stylesMap.set(key, resource);
if (resources.boundaryResources) {
resources.boundaryResources.add(resource);
}
pushStyleContents(resource.chunks, props);
}
if (textEmbedded) {
@ -1640,6 +1684,47 @@ function pushStyleImpl(
return null;
}
function pushStyleContents(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
): void {
let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
}
}
}
const child = Array.isArray(children)
? children.length < 2
? children[0]
: null
: children;
if (
typeof child !== 'function' &&
typeof child !== 'symbol' &&
child !== null &&
child !== undefined
) {
// eslint-disable-next-line react-internal/safe-string-coercion
target.push(stringToChunk(escapeTextForBrowser('' + child)));
}
pushInnerHTML(target, innerHTML, children);
return;
}
function pushSelfClosing(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
@ -2931,6 +3016,7 @@ const completeBoundaryWithStylesScript1FullBoth = stringToPrecomputedChunk(
const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
styleInsertionFunction + '$RR("',
);
const completeBoundaryWithStylesScript1Partial =
stringToPrecomputedChunk('$RR("');
const completeBoundaryScript2 = stringToPrecomputedChunk('","');
@ -2955,16 +3041,21 @@ export function writeCompletedBoundaryInstruction(
contentSegmentID: number,
boundaryResources: BoundaryResources,
): boolean {
let hasStyleDependencies;
let requiresStyleInsertion;
if (enableFloat) {
hasStyleDependencies = hasStyleResourceDependencies(boundaryResources);
requiresStyleInsertion = responseState.stylesToHoist;
// If necessary stylesheets will be flushed with this instruction.
// Any style tags not yet hoisted in the Document will also be hoisted.
// We reset this state since after this instruction executes all styles
// up to this point will have been hoisted
responseState.stylesToHoist = false;
}
const scriptFormat =
!enableFizzExternalRuntime ||
responseState.streamingFormat === ScriptStreamingFormat;
if (scriptFormat) {
writeChunk(destination, responseState.startInlineScript);
if (enableFloat && hasStyleDependencies) {
if (enableFloat && requiresStyleInsertion) {
if (
(responseState.instructions & SentCompleteBoundaryFunction) ===
NothingSent
@ -2996,7 +3087,7 @@ export function writeCompletedBoundaryInstruction(
}
}
} else {
if (enableFloat && hasStyleDependencies) {
if (enableFloat && requiresStyleInsertion) {
writeChunk(destination, completeBoundaryWithStylesData1);
} else {
writeChunk(destination, completeBoundaryData1);
@ -3019,7 +3110,7 @@ export function writeCompletedBoundaryInstruction(
}
writeChunk(destination, responseState.segmentPrefix);
writeChunk(destination, formattedContentID);
if (enableFloat && hasStyleDependencies) {
if (enableFloat && requiresStyleInsertion) {
// Script and data writers must format this differently:
// - script writer emits an array literal, whose string elements are
// escaped for javascript e.g. ["A", "B"]
@ -3214,51 +3305,85 @@ function escapeJSObjectForInstructionScripts(input: Object): string {
});
}
const styleTagTemplateOpen = stringToPrecomputedChunk(
'<template data-precedence="">',
const lateStyleTagResourceOpen1 = stringToPrecomputedChunk(
'<style media="not all" data-precedence="',
);
const styleTagTemplateClose = stringToPrecomputedChunk('</template>');
const lateStyleTagResourceOpen2 = stringToPrecomputedChunk('" data-href="');
const lateStyleTagResourceOpen3 = stringToPrecomputedChunk('">');
const lateStyleTagTemplateClose = stringToPrecomputedChunk('</style>');
// Tracks whether we wrote any late style tags. We use this to determine
// whether we need to emit a closing template tag after flushing late style tags
let didWrite = false;
// Tracks whether the boundary currently flushing is flushign style tags or has any
// stylesheet dependencies not flushed in the Preamble.
let currentlyRenderingBoundaryHasStylesToHoist = false;
// Acts as a return value for the forEach execution of style tag flushing.
let destinationHasCapacity = true;
function flushStyleTagsLateForBoundary(
this: Destination,
resource: StyleResource,
) {
if (resource.type === 'style' && (resource.state & Flushed) === NoState) {
if (didWrite === false) {
// we are going to write so we need to emit the open tag
didWrite = true;
writeChunk(this, styleTagTemplateOpen);
}
// This <style> tag can be flushed now
if (
resource.type === 'stylesheet' &&
(resource.state & FlushedInPreamble) === NoState
) {
currentlyRenderingBoundaryHasStylesToHoist = true;
} else if (resource.type === 'style') {
const chunks = resource.chunks;
for (let i = 0; i < chunks.length; i++) {
writeChunk(this, chunks[i]);
const hrefs = resource.props.hrefs;
let i = 0;
if (chunks.length) {
writeChunk(this, lateStyleTagResourceOpen1);
writeChunk(
this,
stringToChunk(escapeTextForBrowser(resource.props.precedence)),
);
if (hrefs.length) {
writeChunk(this, lateStyleTagResourceOpen2);
for (; i < hrefs.length - 1; i++) {
writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
writeChunk(this, spaceSeparator);
}
writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
}
writeChunk(this, lateStyleTagResourceOpen3);
for (i = 0; i < chunks.length; i++) {
writeChunk(this, chunks[i]);
}
destinationHasCapacity = writeChunkAndReturn(
this,
lateStyleTagTemplateClose,
);
// We wrote style tags for this boundary and we may need to emit a script
// to hoist them.
currentlyRenderingBoundaryHasStylesToHoist = true;
// style resources can flush continuously since more rules may be written into
// them with new hrefs. Instead of marking it flushed, we simply reset the chunks
// and hrefs
chunks.length = 0;
hrefs.length = 0;
}
resource.state |= FlushedLate;
}
}
export function writeResourcesForBoundary(
destination: Destination,
boundaryResources: BoundaryResources,
responseState: ResponseState,
): boolean {
didWrite = false;
boundaryResources.forEach(flushStyleTagsLateForBoundary, destination);
if (didWrite) {
return writeChunkAndReturn(destination, styleTagTemplateClose);
} else {
return true;
}
}
// Reset these on each invocation, they are only safe to read in this function
currentlyRenderingBoundaryHasStylesToHoist = false;
destinationHasCapacity = true;
const precedencePlaceholderStart = stringToPrecomputedChunk(
'<style data-precedence="',
);
const precedencePlaceholderEnd = stringToPrecomputedChunk('"></style>');
// Flush each Boundary resource
boundaryResources.forEach(flushStyleTagsLateForBoundary, destination);
if (currentlyRenderingBoundaryHasStylesToHoist) {
responseState.stylesToHoist = true;
}
return destinationHasCapacity;
}
function flushResourceInPreamble<T: Resource>(this: Destination, resource: T) {
if ((resource.state & (Flushed | Blocked)) === NoState) {
@ -3271,7 +3396,7 @@ function flushResourceInPreamble<T: Resource>(this: Destination, resource: T) {
}
function flushResourceLate<T: Resource>(this: Destination, resource: T) {
if ((resource.state & Flushed) === NoState) {
if ((resource.state & (Flushed | Blocked)) === NoState) {
const chunks = resource.chunks;
for (let i = 0; i < chunks.length; i++) {
writeChunk(this, chunks[i]);
@ -3280,9 +3405,16 @@ function flushResourceLate<T: Resource>(this: Destination, resource: T) {
}
}
let didFlush = false;
// This must always be read after flushing stylesheet styles. we know we will encounter a style resource
// per precedence and it will be set before ready so we cast this to avoid an extra check at runtime
let precedenceStyleTagResource: StyleTagResource = (null: any);
function flushUnblockedStyle(
// This flags let's us opt out of flushing a placeholder style tag to emit the precedence in the right order.
// If a stylesheet was flushed then we have the precedence order preserved and only need to emit <style> tags
// if there are actual chunks to flush
let didFlushPrecedence = false;
function flushStyleInPreamble(
this: Destination,
resource: StyleResource,
key: mixed,
@ -3294,81 +3426,70 @@ function flushUnblockedStyle(
// Set on flush but to ensure correct semantics we don't emit
// anything if we are in this state.
set.delete(resource);
} else if (resource.state & Blocked) {
// We can't flush but we can preload. We will do this in a second pass
} else {
didFlush = true;
// We can emit this style or stylesheet as is.
if (resource.type === 'stylesheet') {
// We still need to encode stylesheet chunks
// because unlike most Hoistables and Resources we do not eagerly encode
// them during render. This is because if we flush late we have to send a
// different encoding and we don't want to encode multiple times
pushLinkImpl(chunks, resource.props);
if (resource.type === 'style') {
precedenceStyleTagResource = resource;
return;
}
// We still need to encode stylesheet chunks
// because unlike most Hoistables and Resources we do not eagerly encode
// them during render. This is because if we flush late we have to send a
// different encoding and we don't want to encode multiple times
pushLinkImpl(chunks, resource.props);
for (let i = 0; i < chunks.length; i++) {
writeChunk(this, chunks[i]);
}
resource.state |= FlushedInPreamble;
set.delete(resource);
didFlushPrecedence = true;
}
}
function flushUnblockedStyles(
const styleTagResourceOpen1 = stringToPrecomputedChunk(
'<style data-precedence="',
);
const styleTagResourceOpen2 = stringToPrecomputedChunk('" data-href="');
const spaceSeparator = stringToPrecomputedChunk(' ');
const styleTagResourceOpen3 = stringToPrecomputedChunk('">');
const styleTagResourceClose = stringToPrecomputedChunk('</style>');
function flushAllStylesInPreamble(
this: Destination,
set: Set<StyleResource>,
precedence: string,
) {
didFlush = false;
set.forEach(flushUnblockedStyle, this);
if (!didFlush) {
// if we did not flush anything for this precedence slot we emit
// an empty <style data-precedence="..." /> tag to ensure the
// precedence remains in the correct order
writeChunk(this, precedencePlaceholderStart);
writeChunk(this, stringToChunk(escapeTextForBrowser(precedence)));
writeChunk(this, precedencePlaceholderEnd);
}
}
function preloadBlockedStyle(this: Destination, resource: StyleResource) {
// The only Resources that should remain are Blocked resources
if (__DEV__) {
if ((resource.state & Blocked) === NoState) {
console.error(
'React encountered a Stylesheet Resource that was not Blocked when it was expected to be. This is a bug in React.',
);
} else if (resource.state & PreloadFlushed) {
console.error(
'React encountered a Stylesheet Resource that already flushed a Preload when it was not expected to. This is a bug in React.',
);
}
}
if (resource.type === 'style') {
// <style> tags do not need to be preloaded
return;
}
const chunks = resource.chunks;
const preloadProps = preloadAsStylePropsFromProps(
resource.props.href,
resource.props,
);
pushLinkImpl(chunks, preloadProps);
for (let i = 0; i < chunks.length; i++) {
writeChunk(this, chunks[i]);
}
resource.state |= PreloadFlushed;
chunks.length = 0;
}
function preloadBlockedStyles(
this: Destination,
set: Set<StyleResource>,
precedence: string,
) {
set.forEach(preloadBlockedStyle, this);
didFlushPrecedence = false;
set.forEach(flushStyleInPreamble, this);
set.clear();
const chunks = precedenceStyleTagResource.chunks;
const hrefs = precedenceStyleTagResource.props.hrefs;
if (didFlushPrecedence === false || chunks.length) {
writeChunk(this, styleTagResourceOpen1);
writeChunk(this, stringToChunk(escapeTextForBrowser(precedence)));
let i = 0;
if (hrefs.length) {
writeChunk(this, styleTagResourceOpen2);
for (; i < hrefs.length - 1; i++) {
writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
writeChunk(this, spaceSeparator);
}
writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
}
writeChunk(this, styleTagResourceOpen3);
for (i = 0; i < chunks.length; i++) {
writeChunk(this, chunks[i]);
}
writeChunk(this, styleTagResourceClose);
// style resources can flush continuously since more rules may be written into
// them with new hrefs. Instead of marking it flushed, we simply reset the chunks
// and hrefs
chunks.length = 0;
hrefs.length = 0;
}
}
function preloadLateStyle(this: Destination, resource: StyleResource) {
@ -3480,10 +3601,7 @@ export function writePreamble(
resources.fontPreloads.clear();
// Flush unblocked stylesheets by precedence
resources.precedences.forEach(flushUnblockedStyles, destination);
// Flush preloads for Blocked stylesheets
resources.precedences.forEach(preloadBlockedStyles, destination);
resources.precedences.forEach(flushAllStylesInPreamble, destination);
resources.usedStylesheets.forEach(resource => {
const key = getResourceKey(resource.props.as, resource.props.href);
@ -3643,25 +3761,6 @@ export function writePostamble(
}
}
function hasStyleResourceDependencies(
boundaryResources: BoundaryResources,
): boolean {
const iter = boundaryResources.values();
// At the moment boundaries only accumulate style resources
// so we assume the type is correct and don't check it
while (true) {
const {value: resource} = iter.next();
if (!resource) break;
// If every style Resource flushed in the shell we do not need to send
// any dependencies
if ((resource.state & FlushedInPreamble) === NoState) {
return true;
}
}
return false;
}
const arrayFirstOpenBracket = stringToPrecomputedChunk('[');
const arraySubsequentOpenBracket = stringToPrecomputedChunk(',[');
const arrayInterstitial = stringToPrecomputedChunk(',');
@ -3678,7 +3777,9 @@ function writeStyleResourceDependenciesInJS(
let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
boundaryResources.forEach(resource => {
if (resource.state & FlushedInPreamble) {
if (resource.type === 'style') {
// Style dependencies don't require coordinated reveal and can be omitted
} else if (resource.state & FlushedInPreamble) {
// We can elide this dependency because it was flushed in the shell and
// should be ready before content is shown on the client
} else if (resource.state & Flushed) {
@ -3688,9 +3789,7 @@ function writeStyleResourceDependenciesInJS(
writeChunk(destination, nextArrayOpenBrackChunk);
writeStyleResourceDependencyHrefOnlyInJS(
destination,
resource.type === 'style'
? resource.props['data-href']
: resource.props.href,
resource.props.href,
);
writeChunk(destination, arrayCloseBracket);
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
@ -3875,7 +3974,9 @@ function writeStyleResourceDependenciesInAttr(
let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
boundaryResources.forEach(resource => {
if (resource.state & FlushedInPreamble) {
if (resource.type === 'style') {
// Style dependencies don't require coordinated reveal and can be omitted
} else if (resource.state & FlushedInPreamble) {
// We can elide this dependency because it was flushed in the shell and
// should be ready before content is shown on the client
} else if (resource.state & Flushed) {
@ -3885,9 +3986,7 @@ function writeStyleResourceDependenciesInAttr(
writeChunk(destination, nextArrayOpenBrackChunk);
writeStyleResourceDependencyHrefOnlyInAttr(
destination,
resource.type === 'style'
? resource.props['data-href']
: resource.props.href,
resource.props.href,
);
writeChunk(destination, arrayCloseBracket);
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
@ -4135,9 +4234,8 @@ type StylesheetProps = {
type StylesheetResource = TResource<'stylesheet', StylesheetProps>;
type StyleTagProps = {
'data-href': string,
'data-precedence': string,
[string]: mixed,
hrefs: Array<string>,
precedence: string,
};
type StyleTagResource = TResource<'style', StyleTagProps>;
@ -4168,6 +4266,7 @@ export type Resources = {
fontPreloads: Set<PreloadResource>,
// usedImagePreloads: Set<PreloadResource>,
precedences: Map<string, Set<StyleResource>>,
stylePrecedences: Map<string, StyleTagResource>,
usedStylesheets: Set<PreloadResource>,
scripts: Set<ScriptResource>,
usedScripts: Set<PreloadResource>,
@ -4195,6 +4294,7 @@ export function createResources(): Resources {
fontPreloads: new Set(),
// usedImagePreloads: new Set(),
precedences: new Map(),
stylePrecedences: new Map(),
usedStylesheets: new Set(),
scripts: new Set(),
usedScripts: new Set(),
@ -4615,6 +4715,25 @@ function preinitImpl(
if (!precedenceSet) {
precedenceSet = new Set();
resources.precedences.set(precedence, precedenceSet);
const emptyStyleResource = {
type: 'style',
chunks: ([]: Array<Chunk | PrecomputedChunk>),
state: NoState,
props: {
precedence,
hrefs: ([]: Array<string>),
},
};
precedenceSet.add(emptyStyleResource);
if (__DEV__) {
if (resources.stylePrecedences.has(precedence)) {
console.error(
'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
precedence,
);
}
}
resources.stylePrecedences.set(precedence, emptyStyleResource);
}
precedenceSet.add(resource);
}
@ -4769,16 +4888,6 @@ function adoptPreloadPropsForStylesheetProps(
resourceProps.integrity = preloadProps.integrity;
}
function styleTagPropsFromRawProps(rawProps: any): StyleTagProps {
return {
...rawProps,
'data-precedence': rawProps.precedence,
precedence: null,
'data-href': rawProps.href,
href: null,
};
}
function scriptPropsFromPreinitOptions(
src: string,
options: PreinitOptions,
@ -4801,10 +4910,7 @@ function adoptPreloadPropsForScriptProps(
resourceProps.integrity = preloadProps.integrity;
}
function hoistStylesheetResource(
this: BoundaryResources,
resource: StyleResource,
) {
function hoistStyleResource(this: BoundaryResources, resource: StyleResource) {
this.add(resource);
}
@ -4814,23 +4920,10 @@ export function hoistResources(
): void {
const currentBoundaryResources = resources.boundaryResources;
if (currentBoundaryResources) {
source.forEach(hoistStylesheetResource, currentBoundaryResources);
source.clear();
source.forEach(hoistStyleResource, currentBoundaryResources);
}
}
function unblockStylesheet(resource: StyleResource) {
resource.state &= ~Blocked;
}
export function hoistResourcesToRoot(
resources: Resources,
boundaryResources: BoundaryResources,
): void {
boundaryResources.forEach(unblockStylesheet);
boundaryResources.clear();
}
function markAsRenderedResourceDEV(
resource: Resource,
originalProps: any,

View File

@ -52,6 +52,7 @@ export type ResponseState = {
preconnectChunks: Array<Chunk | PrecomputedChunk>,
preloadChunks: Array<Chunk | PrecomputedChunk>,
hoistableChunks: Array<Chunk | PrecomputedChunk>,
stylesToHoist: boolean,
// This is an extra field for the legacy renderer
generateStaticMarkup: boolean,
};
@ -88,6 +89,7 @@ export function createResponseState(
preconnectChunks: responseState.preconnectChunks,
preloadChunks: responseState.preloadChunks,
hoistableChunks: responseState.hoistableChunks,
stylesToHoist: responseState.stylesToHoist,
// This is an extra field for the legacy renderer
generateStaticMarkup,
@ -134,7 +136,6 @@ export {
writeHoistables,
writePostamble,
hoistResources,
hoistResourcesToRoot,
setCurrentlyRenderingBoundaryResourcesTarget,
prepareToRender,
cleanupAfterRender,

View File

@ -20,74 +20,69 @@ const resourceMap = new Map();
export function completeBoundaryWithStyles(
suspenseBoundaryID,
contentID,
styles,
stylesheetDescriptors,
) {
const stylesToHoist = new Map();
const precedences = new Map();
const thisDocument = document;
let lastResource, node;
let nodes = thisDocument.querySelectorAll('template[data-precedence]');
for (let i = 0; (node = nodes[i++]); ) {
let child = node.content.firstChild;
for (; child; child = child.nextSibling) {
stylesToHoist.set(child.getAttribute('data-href'), child);
}
node.parentNode.removeChild(node);
}
// Seed the precedence list with existing resources
nodes = thisDocument.querySelectorAll(
// 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++]); ) {
// We populate the resourceMap from found nodes so we can incorporate any
// resources the client runtime adds when the two runtimes are running concurrently
resourceMap.set(
node.getAttribute(node.nodeName === 'STYLE' ? 'data-href' : 'href'),
node,
);
precedences.set(node.dataset['precedence'], (lastResource = node));
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 style, href, precedence, attr, loadingState, resourceEl, media;
let href, precedence, attr, loadingState, resourceEl, media;
function setStatus(s) {
this['s'] = s;
}
while ((style = styles[i++])) {
let j = 0;
href = style[j++];
// 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;
}
if ((resourceEl = resourceMap.get(href))) {
// We have an already known resource. It could be a <style>, a <link> created
// by this runtime (which will have a loadingState) or a <link> created by
// the client runtime (which will also have a loadingState). We look for a
// loadingState and test whether it is not loaded yet and whether the media matches
// before using it as a dependency. If it is a <style> there will be no loadingState
// and we can avoid tracking it as a dependency because these tags don't load
} else {
// We haven't already processed this href so we need to hoist an element. It will
// either be a <style> that was sent in a <template> and prepped in `stylesToHoist`
// or we will need to create a <link>
if ((resourceEl = stylesToHoist.get(href))) {
// We have a <style> which needs to be hoisted to the correct precedence
// We set it in the resourceMap so we can bail out on future passes
// if this is depended on more than once
precedence = resourceEl.getAttribute('data-precedence');
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 {
// If we got this far we are depending on a <link> which is not yet in the document.
// 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 = style[j++];
while ((attr = style[j++])) {
resourceEl.setAttribute(attr, style[j++]);
resourceEl.dataset['precedence'] = precedence =
stylesheetDescriptor[j++];
while ((attr = stylesheetDescriptor[j++])) {
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
}
loadingState = resourceEl['_p'] = new Promise((re, rj) => {
resourceEl.onload = re;
@ -97,42 +92,47 @@ export function completeBoundaryWithStyles(
setStatus.bind(loadingState, LOADED),
setStatus.bind(loadingState, ERRORED),
);
// Save this resource element so we can bailout if it is used again
resourceMap.set(href, resourceEl);
}
media = resourceEl.getAttribute('media');
if (
loadingState &&
loadingState['s'] !== 'l' &&
(!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;
}
// Save this resource element so we can bailout if it is used again
resourceMap.set(href, resourceEl);
// 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);
}
precedence = resourceEl.getAttribute('data-precedence');
resourceEl.removeAttribute('media');
}
// If we are a <link> we will have a loadingState and we can use this
// combined with matchMedia to decide if we need to await this dependency
// loading. <style> tags won't have a loadingState so they are never awaited
loadingState = resourceEl['_p'];
media = resourceEl.getAttribute('media');
if (
loadingState &&
loadingState['s'] !== 'l' &&
(!media || window['matchMedia'](media).matches)
) {
dependencies.push(loadingState);
// 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);
}
}

View File

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

View File

@ -20,77 +20,72 @@ export {clientRenderBoundary, completeBoundary, completeSegment};
export function completeBoundaryWithStyles(
suspenseBoundaryID,
contentID,
styles,
stylesheetDescriptors,
) {
const completeBoundaryImpl = window['$RC'];
const resourceMap = window['$RM'];
const stylesToHoist = new Map();
const precedences = new Map();
const thisDocument = document;
let lastResource, node;
let nodes = thisDocument.querySelectorAll('template[data-precedence]');
for (let i = 0; (node = nodes[i++]); ) {
let child = node.content.firstChild;
for (; child; child = child.nextSibling) {
stylesToHoist.set(child.getAttribute('data-href'), child);
}
node.parentNode.removeChild(node);
}
// Seed the precedence list with existing resources
nodes = thisDocument.querySelectorAll(
// 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++]); ) {
// We populate the resourceMap from found nodes so we can incorporate any
// resources the client runtime adds when the two runtimes are running concurrently
resourceMap.set(
node.getAttribute(node.nodeName === 'STYLE' ? 'data-href' : 'href'),
node,
);
precedences.set(node.dataset['precedence'], (lastResource = node));
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 style, href, precedence, attr, loadingState, resourceEl, media;
let href, precedence, attr, loadingState, resourceEl, media;
function setStatus(s) {
this['s'] = s;
}
while ((style = styles[i++])) {
let j = 0;
href = style[j++];
// 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;
}
if ((resourceEl = resourceMap.get(href))) {
// We have an already known resource. It could be a <style>, a <link> created
// by this runtime (which will have a loadingState) or a <link> created by
// the client runtime (which will also have a loadingState). We look for a
// loadingState and test whether it is not loaded yet and whether the media matches
// before using it as a dependency. If it is a <style> there will be no loadingState
// and we can avoid tracking it as a dependency because these tags don't load
} else {
// We haven't already processed this href so we need to hoist an element. It will
// either be a <style> that was sent in a <template> and prepped in `stylesToHoist`
// or we will need to create a <link>
if ((resourceEl = stylesToHoist.get(href))) {
// We have a <style> which needs to be hoisted to the correct precedence
// We set it in the resourceMap so we can bail out on future passes
// if this is depended on more than once
precedence = resourceEl.getAttribute('data-precedence');
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 {
// If we got this far we are depending on a <link> which is not yet in the document.
// 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 = style[j++];
while ((attr = style[j++])) {
resourceEl.setAttribute(attr, style[j++]);
resourceEl.dataset['precedence'] = precedence =
stylesheetDescriptor[j++];
while ((attr = stylesheetDescriptor[j++])) {
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
}
loadingState = resourceEl['_p'] = new Promise((re, rj) => {
resourceEl.onload = re;
@ -100,42 +95,47 @@ export function completeBoundaryWithStyles(
setStatus.bind(loadingState, LOADED),
setStatus.bind(loadingState, ERRORED),
);
// Save this resource element so we can bailout if it is used again
resourceMap.set(href, resourceEl);
}
media = resourceEl.getAttribute('media');
if (
loadingState &&
loadingState['s'] !== 'l' &&
(!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;
}
// Save this resource element so we can bailout if it is used again
resourceMap.set(href, resourceEl);
// 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);
}
precedence = resourceEl.getAttribute('data-precedence');
resourceEl.removeAttribute('media');
}
// If we are a <link> we will have a loadingState and we can use this
// combined with matchMedia to decide if we need to await this dependency
// loading. <style> tags won't have a loadingState so they are never awaited
loadingState = resourceEl['_p'];
media = resourceEl.getAttribute('media');
if (
loadingState &&
loadingState['s'] !== 'l' &&
(!media || window['matchMedia'](media).matches)
) {
dependencies.push(loadingState);
// 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);
}
}

View File

@ -1281,8 +1281,8 @@ body {
<html>
<head>
<link rel="stylesheet" href="initial" data-precedence="one" />
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>loading foo bar...</div>
@ -1304,7 +1304,6 @@ body {
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>loading foo bar...</div>
@ -1329,7 +1328,6 @@ body {
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>loading foo bar...</div>
@ -1354,7 +1352,6 @@ body {
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>
@ -1379,7 +1376,6 @@ body {
<link rel="stylesheet" href="foo" data-precedence="one" />
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>
@ -1407,7 +1403,6 @@ body {
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="stylesheet" href="baz" data-precedence="two" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>
@ -1449,7 +1444,6 @@ body {
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="stylesheet" href="baz" data-precedence="two" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>
@ -1482,7 +1476,6 @@ body {
<link rel="stylesheet" href="preset" data-precedence="preset" />
<link rel="stylesheet" href="bar" data-precedence="default" />
<link rel="stylesheet" href="baz" data-precedence="two" />
<link rel="preload" href="foo" as="style" />
</head>
<body>
<div>
@ -3499,6 +3492,7 @@ body {
<div>1</div>,
]);
});
// @gate enableFloat
it('escapes hrefs when selecting matching elements in the document when rendering Resources', async () => {
function App() {
@ -4157,19 +4151,14 @@ background-color: green;
resolveText('first');
});
const styleTemplates = document.querySelectorAll(
'template[data-precedence]',
);
expect(styleTemplates.length).toBe(1);
expect(getMeaningfulChildren(styleTemplates[0].content)).toEqual(
<style data-href="foo" data-precedence="default">
{css}
</style>,
);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body />
<body>
<style data-href="foo" data-precedence="default" media="not all">
{css}
</style>
</body>
</html>,
);
@ -4194,6 +4183,266 @@ background-color: green;
</html>,
);
});
it('can hoist styles flushed early even when no other style dependencies are flushed on completion', async () => {
await actIntoEmptyDocument(() => {
renderToPipeableStream(
<html>
<body>
<Suspense fallback="loading...">
<BlockedOn value="first">
<style href="foo" precedence="default">
some css
</style>
<div>first</div>
<BlockedOn value="second">
<div>second</div>
</BlockedOn>
</BlockedOn>
</Suspense>
</body>
</html>,
).pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>loading...</body>
</html>,
);
// When we resolve first we flush the style tag because it is ready but we aren't yet ready to
// flush the entire boundary and reveal it.
await act(() => {
resolveText('first');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head />
<body>
loading...
<style data-href="foo" data-precedence="default" media="not all">
some css
</style>
</body>
</html>,
);
// When we resolve second we flush the rest of the boundary segments and reveal the boundary. The style tag
// is hoisted during this reveal process even though no other styles flushed during this tick
await act(() => {
resolveText('second');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<style data-href="foo" data-precedence="default">
some css
</style>
</head>
<body>
<div>first</div>
<div>second</div>
</body>
</html>,
);
});
it('can emit multiple style rules into a single style tag for a given precedence', async () => {
await actIntoEmptyDocument(() => {
renderToPipeableStream(
<html>
<body>
<style href="1" precedence="default">
1
</style>
<style href="2" precedence="foo">
foo2
</style>
<style href="3" precedence="default">
3
</style>
<style href="4" precedence="default">
4
</style>
<style href="5" precedence="foo">
foo5
</style>
<div>initial</div>
<Suspense fallback="loading...">
<BlockedOn value="first">
<style href="6" precedence="default">
6
</style>
<style href="7" precedence="foo">
foo7
</style>
<style href="8" precedence="default">
8
</style>
<style href="9" precedence="default">
9
</style>
<style href="10" precedence="foo">
foo10
</style>
<div>first</div>
<BlockedOn value="second">
<style href="11" precedence="default">
11
</style>
<style href="12" precedence="foo">
foo12
</style>
<style href="13" precedence="default">
13
</style>
<style href="14" precedence="default">
14
</style>
<style href="15" precedence="foo">
foo15
</style>
<div>second</div>
</BlockedOn>
</BlockedOn>
</Suspense>
</body>
</html>,
).pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<style data-href="1 3 4" data-precedence="default">
134
</style>
<style data-href="2 5" data-precedence="foo">
foo2foo5
</style>
</head>
<body>
<div>initial</div>loading...
</body>
</html>,
);
// When we resolve first we flush the style tag because it is ready but we aren't yet ready to
// flush the entire boundary and reveal it.
await act(() => {
resolveText('first');
});
await act(() => {
resolveText('second');
});
// Some sets of styles were ready before the entire boundary and they got emitted as early as they were
// ready. The remaining styles were ready when the boundary finished and they got grouped as well
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<style data-href="1 3 4" data-precedence="default">
134
</style>
<style data-href="6 8 9" data-precedence="default">
689
</style>
<style data-href="11 13 14" data-precedence="default">
111314
</style>
<style data-href="2 5" data-precedence="foo">
foo2foo5
</style>
<style data-href="7 10" data-precedence="foo">
foo7foo10
</style>
<style data-href="12 15" data-precedence="foo">
foo12foo15
</style>
</head>
<body>
<div>initial</div>
<div>first</div>
<div>second</div>
</body>
</html>,
);
// Client inserted style tags are not grouped together but can hydrate against a grouped set
ReactDOMClient.hydrateRoot(
document,
<html>
<body>
<style href="1" precedence="default">
1
</style>
<style href="2" precedence="foo">
foo2
</style>
<style href="16" precedence="default">
16
</style>
<style href="17" precedence="default">
17
</style>
</body>
</html>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<style data-href="1 3 4" data-precedence="default">
134
</style>
<style data-href="6 8 9" data-precedence="default">
689
</style>
<style data-href="11 13 14" data-precedence="default">
111314
</style>
<style data-href="16" data-precedence="default">
16
</style>
<style data-href="17" data-precedence="default">
17
</style>
<style data-href="2 5" data-precedence="foo">
foo2foo5
</style>
<style data-href="7 10" data-precedence="foo">
foo7foo10
</style>
<style data-href="12 15" data-precedence="foo">
foo12foo15
</style>
</head>
<body>
<div>initial</div>
<div>first</div>
<div>second</div>
</body>
</html>,
);
});
it('warns if you render a <style> with an href with a space on the server', async () => {
await expect(async () => {
await actIntoEmptyDocument(() => {
renderToPipeableStream(
<html>
<body>
<style href="foo bar" precedence="default">
style
</style>
</body>
</html>,
).pipe(writable);
});
}).toErrorDev(
'React expected the `href` prop for a <style> tag opting into hoisting semantics using the `precedence` prop to not have any spaces but ecountered spaces instead. using spaces in this prop will cause hydration of this style to fail on the client. The href for the <style> where this ocurred is "foo bar".',
);
});
});
describe('Script Resources', () => {

View File

@ -339,11 +339,6 @@ export function hoistResources(
boundaryResources: BoundaryResources,
) {}
export function hoistResourcesToRoot(
resources: Resources,
boundaryResources: BoundaryResources,
) {}
export function prepareToRender(resources: Resources) {}
export function cleanupAfterRender(previousDispatcher: mixed) {}
export function createResources() {}
@ -356,6 +351,7 @@ export function setCurrentlyRenderingBoundaryResourcesTarget(
export function writeResourcesForBoundary(
destination: Destination,
boundaryResources: BoundaryResources,
responseState: ResponseState,
): boolean {
return true;
}

View File

@ -70,7 +70,6 @@ import {
writeHoistables,
writePostamble,
hoistResources,
hoistResourcesToRoot,
prepareToRender,
cleanupAfterRender,
setCurrentlyRenderingBoundaryResourcesTarget,
@ -594,11 +593,6 @@ function renderSuspenseBoundary(
contentRootSegment.textEmbedded,
);
contentRootSegment.status = COMPLETED;
if (enableFloat) {
if (newBoundary.pendingTasks === 0) {
hoistCompletedBoundaryResources(request, newBoundary);
}
}
queueCompletedSegment(newBoundary, contentRootSegment);
if (newBoundary.pendingTasks === 0) {
// This must have been the last segment we were waiting on. This boundary is now complete.
@ -652,19 +646,6 @@ function renderSuspenseBoundary(
popComponentStackInDEV(task);
}
function hoistCompletedBoundaryResources(
request: Request,
completedBoundary: SuspenseBoundary,
): void {
if (request.completedRootSegment !== null || request.pendingRootTasks > 0) {
// The Shell has not flushed yet. we can hoist Resources for this boundary
// all the way to the Root.
hoistResourcesToRoot(request.resources, completedBoundary.resources);
}
// We don't hoist if the root already flushed because late resources will be hoisted
// as boundaries flush
}
function renderBackupSuspenseBoundary(
request: Request,
task: Task,
@ -1802,9 +1783,6 @@ function finishedTask(
queueCompletedSegment(boundary, segment);
}
}
if (enableFloat) {
hoistCompletedBoundaryResources(request, boundary);
}
if (boundary.parentFlushed) {
// The segment might be part of a segment that didn't flush yet, but if the boundary's
// parent flushed, we need to schedule the boundary to be emitted.
@ -2177,7 +2155,11 @@ function flushCompletedBoundary(
completedSegments.length = 0;
if (enableFloat) {
writeResourcesForBoundary(destination, boundary.resources);
writeResourcesForBoundary(
destination,
boundary.resources,
request.responseState,
);
}
return writeCompletedBoundaryInstruction(
@ -2221,7 +2203,11 @@ function flushPartialBoundary(
// if there is no backpressure. Later before we complete the boundary we
// will write resources regardless of backpressure before we emit the
// completion instruction
return writeResourcesForBoundary(destination, boundary.resources);
return writeResourcesForBoundary(
destination,
boundary.resources,
request.responseState,
);
} else {
return true;
}

View File

@ -78,7 +78,6 @@ export const writePreamble = $$$hostConfig.writePreamble;
export const writeHoistables = $$$hostConfig.writeHoistables;
export const writePostamble = $$$hostConfig.writePostamble;
export const hoistResources = $$$hostConfig.hoistResources;
export const hoistResourcesToRoot = $$$hostConfig.hoistResourcesToRoot;
export const createResources = $$$hostConfig.createResources;
export const createBoundaryResources = $$$hostConfig.createBoundaryResources;
export const setCurrentlyRenderingBoundaryResourcesTarget =

View File

@ -113,7 +113,7 @@ export const enableUseEffectEventHook = __EXPERIMENTAL__;
// Test in www before enabling in open source.
// Enables DOM-server to stream its instruction set as data-attributes
// (handled with an MutationObserver) instead of inline-scripts
export const enableFizzExternalRuntime = false;
export const enableFizzExternalRuntime = true;
// -----------------------------------------------------------------------------
// Chopping Block