@samitouri / QOS-React-2 / commits / 1f1f8eb559

[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

Josh Story committed Mar 3, 2023 at 12:50 UTC 1f1f8eb559d064c5b1954e5dde7517d87d594e7a
11 files changed +697 -373
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+1 -1
@@ -663,7 +663,7 @@ function styleTagPropsFromRawProps(
663 function getStyleKey(href: string) {
664 const limitedEscapedHref =
665 escapeSelectorAttributeValueInsideDoubleQuotes(href);
666 - return `href="${limitedEscapedHref}"`;
666 + return `href~="${limitedEscapedHref}"`;
667 }
668
669 function getStyleTagSelectorFromKey(key: string) {
packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+264 -171
@@ -153,6 +153,11 @@ export type ResponseState = {
153 preloadChunks: Array<Chunk | PrecomputedChunk>,
154 hoistableChunks: Array<Chunk | PrecomputedChunk>,
155
156 + // Module-global-like reference for flushing/hoisting state of style resources
157 + // We need to track whether the current request has flushed any style resources
158 + // without sending an instruction to hoist them. we do that here
159 + stylesToHoist: boolean,
160 +
161 // We allow the legacy renderer to extend this object.
162
163 ...
@@ -300,6 +305,7 @@ export function createResponseState(
305 preconnectChunks: [],
306 preloadChunks: [],
307 hoistableChunks: [],
308 + stylesToHoist: false,
309 };
310 }
311
@@ -1421,7 +1427,7 @@ function pushLink(
1427 resource = {
1428 type: 'stylesheet',
1429 chunks: ([]: Array<Chunk | PrecomputedChunk>),
1424 - state: resources.boundaryResources ? Blocked : NoState,
1430 + state: NoState,
1431 props: resourceProps,
1432 };
1433 resources.stylesMap.set(key, resource);
@@ -1432,6 +1438,25 @@ function pushLink(
1438 if (!precedenceSet) {
1439 precedenceSet = new Set();
1440 resources.precedences.set(precedence, precedenceSet);
1441 + const emptyStyleResource = {
1442 + type: 'style',
1443 + chunks: ([]: Array<Chunk | PrecomputedChunk>),
1444 + state: NoState,
1445 + props: {
1446 + precedence,
1447 + hrefs: ([]: Array<string>),
1448 + },
1449 + };
1450 + precedenceSet.add(emptyStyleResource);
1451 + if (__DEV__) {
1452 + if (resources.stylePrecedences.has(precedence)) {
1453 + console.error(
1454 + 'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
1455 + precedence,
1456 + );
1457 + }
1458 + }
1459 + resources.stylePrecedences.set(precedence, emptyStyleResource);
1460 }
1461 precedenceSet.add(resource);
1462 }
@@ -1556,30 +1581,49 @@ function pushStyle(
1581 return pushStyleImpl(target, props);
1582 }
1583
1584 + if (__DEV__) {
1585 + if (href.includes(' ')) {
1586 + console.error(
1587 + '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".',
1588 + href,
1589 + );
1590 + }
1591 + }
1592 +
1593 const key = getResourceKey('style', href);
1594 let resource = resources.stylesMap.get(key);
1595 if (!resource) {
1562 - resource = {
1563 - type: 'style',
1564 - chunks: ([]: Array<Chunk | PrecomputedChunk>),
1565 - state: resources.boundaryResources ? Blocked : NoState,
1566 - props: styleTagPropsFromRawProps(props),
1567 - };
1568 - resources.stylesMap.set(key, resource);
1569 - if (__DEV__) {
1570 - markAsRenderedResourceDEV(resource, props);
1571 - }
1572 - pushStyleImpl(resource.chunks, resource.props);
1573 -
1574 - let precedenceSet = resources.precedences.get(precedence);
1575 - if (!precedenceSet) {
1576 - precedenceSet = new Set();
1596 + resource = resources.stylePrecedences.get(precedence);
1597 + if (!resource) {
1598 + resource = {
1599 + type: 'style',
1600 + chunks: [],
1601 + state: NoState,
1602 + props: {
1603 + precedence,
1604 + hrefs: [href],
1605 + },
1606 + };
1607 + resources.stylePrecedences.set(precedence, resource);
1608 + const precedenceSet: Set<StyleResource> = new Set();
1609 + precedenceSet.add(resource);
1610 + if (__DEV__) {
1611 + if (resources.precedences.has(precedence)) {
1612 + console.error(
1613 + 'React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.',
1614 + precedence,
1615 + );
1616 + }
1617 + }
1618 resources.precedences.set(precedence, precedenceSet);
1619 + } else {
1620 + resource.props.hrefs.push(href);
1621 }
1579 - precedenceSet.add(resource);
1622 + resources.stylesMap.set(key, resource);
1623 if (resources.boundaryResources) {
1624 resources.boundaryResources.add(resource);
1625 }
1626 + pushStyleContents(resource.chunks, props);
1627 }
1628
1629 if (textEmbedded) {
@@ -1640,6 +1684,47 @@ function pushStyleImpl(
1684 return null;
1685 }
1686
1687 +function pushStyleContents(
1688 + target: Array<Chunk | PrecomputedChunk>,
1689 + props: Object,
1690 +): void {
1691 + let children = null;
1692 + let innerHTML = null;
1693 + for (const propKey in props) {
1694 + if (hasOwnProperty.call(props, propKey)) {
1695 + const propValue = props[propKey];
1696 + if (propValue == null) {
1697 + continue;
1698 + }
1699 + switch (propKey) {
1700 + case 'children':
1701 + children = propValue;
1702 + break;
1703 + case 'dangerouslySetInnerHTML':
1704 + innerHTML = propValue;
1705 + break;
1706 + }
1707 + }
1708 + }
1709 +
1710 + const child = Array.isArray(children)
1711 + ? children.length < 2
1712 + ? children[0]
1713 + : null
1714 + : children;
1715 + if (
1716 + typeof child !== 'function' &&
1717 + typeof child !== 'symbol' &&
1718 + child !== null &&
1719 + child !== undefined
1720 + ) {
1721 + // eslint-disable-next-line react-internal/safe-string-coercion
1722 + target.push(stringToChunk(escapeTextForBrowser('' + child)));
1723 + }
1724 + pushInnerHTML(target, innerHTML, children);
1725 + return;
1726 +}
1727 +
1728 function pushSelfClosing(
1729 target: Array<Chunk | PrecomputedChunk>,
1730 props: Object,
@@ -2931,6 +3016,7 @@ const completeBoundaryWithStylesScript1FullBoth = stringToPrecomputedChunk(
3016 const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
3017 styleInsertionFunction + '$RR("',
3018 );
3019 +
3020 const completeBoundaryWithStylesScript1Partial =
3021 stringToPrecomputedChunk('$RR("');
3022 const completeBoundaryScript2 = stringToPrecomputedChunk('","');
@@ -2955,16 +3041,21 @@ export function writeCompletedBoundaryInstruction(
3041 contentSegmentID: number,
3042 boundaryResources: BoundaryResources,
3043 ): boolean {
2958 - let hasStyleDependencies;
3044 + let requiresStyleInsertion;
3045 if (enableFloat) {
2960 - hasStyleDependencies = hasStyleResourceDependencies(boundaryResources);
3046 + requiresStyleInsertion = responseState.stylesToHoist;
3047 + // If necessary stylesheets will be flushed with this instruction.
3048 + // Any style tags not yet hoisted in the Document will also be hoisted.
3049 + // We reset this state since after this instruction executes all styles
3050 + // up to this point will have been hoisted
3051 + responseState.stylesToHoist = false;
3052 }
3053 const scriptFormat =
3054 !enableFizzExternalRuntime ||
3055 responseState.streamingFormat === ScriptStreamingFormat;
3056 if (scriptFormat) {
3057 writeChunk(destination, responseState.startInlineScript);
2967 - if (enableFloat && hasStyleDependencies) {
3058 + if (enableFloat && requiresStyleInsertion) {
3059 if (
3060 (responseState.instructions & SentCompleteBoundaryFunction) ===
3061 NothingSent
@@ -2996,7 +3087,7 @@ export function writeCompletedBoundaryInstruction(
3087 }
3088 }
3089 } else {
2999 - if (enableFloat && hasStyleDependencies) {
3090 + if (enableFloat && requiresStyleInsertion) {
3091 writeChunk(destination, completeBoundaryWithStylesData1);
3092 } else {
3093 writeChunk(destination, completeBoundaryData1);
@@ -3019,7 +3110,7 @@ export function writeCompletedBoundaryInstruction(
3110 }
3111 writeChunk(destination, responseState.segmentPrefix);
3112 writeChunk(destination, formattedContentID);
3022 - if (enableFloat && hasStyleDependencies) {
3113 + if (enableFloat && requiresStyleInsertion) {
3114 // Script and data writers must format this differently:
3115 // - script writer emits an array literal, whose string elements are
3116 // escaped for javascript e.g. ["A", "B"]
@@ -3214,52 +3305,86 @@ function escapeJSObjectForInstructionScripts(input: Object): string {
3305 });
3306 }
3307
3217 -const styleTagTemplateOpen = stringToPrecomputedChunk(
3218 - '<template data-precedence="">',
3308 +const lateStyleTagResourceOpen1 = stringToPrecomputedChunk(
3309 + '<style media="not all" data-precedence="',
3310 );
3220 -const styleTagTemplateClose = stringToPrecomputedChunk('</template>');
3311 +const lateStyleTagResourceOpen2 = stringToPrecomputedChunk('" data-href="');
3312 +const lateStyleTagResourceOpen3 = stringToPrecomputedChunk('">');
3313 +const lateStyleTagTemplateClose = stringToPrecomputedChunk('</style>');
3314 +
3315 +// Tracks whether the boundary currently flushing is flushign style tags or has any
3316 +// stylesheet dependencies not flushed in the Preamble.
3317 +let currentlyRenderingBoundaryHasStylesToHoist = false;
3318
3222 -// Tracks whether we wrote any late style tags. We use this to determine
3223 -// whether we need to emit a closing template tag after flushing late style tags
3224 -let didWrite = false;
3319 +// Acts as a return value for the forEach execution of style tag flushing.
3320 +let destinationHasCapacity = true;
3321
3322 function flushStyleTagsLateForBoundary(
3323 this: Destination,
3324 resource: StyleResource,
3325 ) {
3230 - if (resource.type === 'style' && (resource.state & Flushed) === NoState) {
3231 - if (didWrite === false) {
3232 - // we are going to write so we need to emit the open tag
3233 - didWrite = true;
3234 - writeChunk(this, styleTagTemplateOpen);
3235 - }
3236 - // This <style> tag can be flushed now
3326 + if (
3327 + resource.type === 'stylesheet' &&
3328 + (resource.state & FlushedInPreamble) === NoState
3329 + ) {
3330 + currentlyRenderingBoundaryHasStylesToHoist = true;
3331 + } else if (resource.type === 'style') {
3332 const chunks = resource.chunks;
3238 - for (let i = 0; i < chunks.length; i++) {
3239 - writeChunk(this, chunks[i]);
3333 + const hrefs = resource.props.hrefs;
3334 + let i = 0;
3335 + if (chunks.length) {
3336 + writeChunk(this, lateStyleTagResourceOpen1);
3337 + writeChunk(
3338 + this,
3339 + stringToChunk(escapeTextForBrowser(resource.props.precedence)),
3340 + );
3341 + if (hrefs.length) {
3342 + writeChunk(this, lateStyleTagResourceOpen2);
3343 + for (; i < hrefs.length - 1; i++) {
3344 + writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
3345 + writeChunk(this, spaceSeparator);
3346 + }
3347 + writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
3348 + }
3349 + writeChunk(this, lateStyleTagResourceOpen3);
3350 + for (i = 0; i < chunks.length; i++) {
3351 + writeChunk(this, chunks[i]);
3352 + }
3353 + destinationHasCapacity = writeChunkAndReturn(
3354 + this,
3355 + lateStyleTagTemplateClose,
3356 + );
3357 +
3358 + // We wrote style tags for this boundary and we may need to emit a script
3359 + // to hoist them.
3360 + currentlyRenderingBoundaryHasStylesToHoist = true;
3361 +
3362 + // style resources can flush continuously since more rules may be written into
3363 + // them with new hrefs. Instead of marking it flushed, we simply reset the chunks
3364 + // and hrefs
3365 + chunks.length = 0;
3366 + hrefs.length = 0;
3367 }
3241 - resource.state |= FlushedLate;
3368 }
3369 }
3370
3371 export function writeResourcesForBoundary(
3372 destination: Destination,
3373 boundaryResources: BoundaryResources,
3374 + responseState: ResponseState,
3375 ): boolean {
3249 - didWrite = false;
3376 + // Reset these on each invocation, they are only safe to read in this function
3377 + currentlyRenderingBoundaryHasStylesToHoist = false;
3378 + destinationHasCapacity = true;
3379 +
3380 + // Flush each Boundary resource
3381 boundaryResources.forEach(flushStyleTagsLateForBoundary, destination);
3251 - if (didWrite) {
3252 - return writeChunkAndReturn(destination, styleTagTemplateClose);
3253 - } else {
3254 - return true;
3382 + if (currentlyRenderingBoundaryHasStylesToHoist) {
3383 + responseState.stylesToHoist = true;
3384 }
3385 + return destinationHasCapacity;
3386 }
3387
3258 -const precedencePlaceholderStart = stringToPrecomputedChunk(
3259 - '<style data-precedence="',
3260 -);
3261 -const precedencePlaceholderEnd = stringToPrecomputedChunk('"></style>');
3262 -
3388 function flushResourceInPreamble<T: Resource>(this: Destination, resource: T) {
3389 if ((resource.state & (Flushed | Blocked)) === NoState) {
3390 const chunks = resource.chunks;
@@ -3271,7 +3396,7 @@ function flushResourceInPreamble<T: Resource>(this: Destination, resource: T) {
3396 }
3397
3398 function flushResourceLate<T: Resource>(this: Destination, resource: T) {
3274 - if ((resource.state & Flushed) === NoState) {
3399 + if ((resource.state & (Flushed | Blocked)) === NoState) {
3400 const chunks = resource.chunks;
3401 for (let i = 0; i < chunks.length; i++) {
3402 writeChunk(this, chunks[i]);
@@ -3280,9 +3405,16 @@ function flushResourceLate<T: Resource>(this: Destination, resource: T) {
3405 }
3406 }
3407
3283 -let didFlush = false;
3408 +// This must always be read after flushing stylesheet styles. we know we will encounter a style resource
3409 +// per precedence and it will be set before ready so we cast this to avoid an extra check at runtime
3410 +let precedenceStyleTagResource: StyleTagResource = (null: any);
3411 +
3412 +// This flags let's us opt out of flushing a placeholder style tag to emit the precedence in the right order.
3413 +// If a stylesheet was flushed then we have the precedence order preserved and only need to emit <style> tags
3414 +// if there are actual chunks to flush
3415 +let didFlushPrecedence = false;
3416
3285 -function flushUnblockedStyle(
3417 +function flushStyleInPreamble(
3418 this: Destination,
3419 resource: StyleResource,
3420 key: mixed,
@@ -3294,81 +3426,70 @@ function flushUnblockedStyle(
3426 // Set on flush but to ensure correct semantics we don't emit
3427 // anything if we are in this state.
3428 set.delete(resource);
3297 - } else if (resource.state & Blocked) {
3298 - // We can't flush but we can preload. We will do this in a second pass
3429 } else {
3300 - didFlush = true;
3430 // We can emit this style or stylesheet as is.
3302 -
3303 - if (resource.type === 'stylesheet') {
3304 - // We still need to encode stylesheet chunks
3305 - // because unlike most Hoistables and Resources we do not eagerly encode
3306 - // them during render. This is because if we flush late we have to send a
3307 - // different encoding and we don't want to encode multiple times
3308 - pushLinkImpl(chunks, resource.props);
3431 + if (resource.type === 'style') {
3432 + precedenceStyleTagResource = resource;
3433 + return;
3434 }
3435 +
3436 + // We still need to encode stylesheet chunks
3437 + // because unlike most Hoistables and Resources we do not eagerly encode
3438 + // them during render. This is because if we flush late we have to send a
3439 + // different encoding and we don't want to encode multiple times
3440 + pushLinkImpl(chunks, resource.props);
3441 for (let i = 0; i < chunks.length; i++) {
3442 writeChunk(this, chunks[i]);
3443 }
3444 resource.state |= FlushedInPreamble;
3314 - set.delete(resource);
3445 + didFlushPrecedence = true;
3446 }
3447 }
3448
3318 -function flushUnblockedStyles(
3319 - this: Destination,
3320 - set: Set<StyleResource>,
3321 - precedence: string,
3322 -) {
3323 - didFlush = false;
3324 - set.forEach(flushUnblockedStyle, this);
3325 - if (!didFlush) {
3326 - // if we did not flush anything for this precedence slot we emit
3327 - // an empty <style data-precedence="..." /> tag to ensure the
3328 - // precedence remains in the correct order
3329 - writeChunk(this, precedencePlaceholderStart);
3330 - writeChunk(this, stringToChunk(escapeTextForBrowser(precedence)));
3331 - writeChunk(this, precedencePlaceholderEnd);
3332 - }
3333 -}
3449 +const styleTagResourceOpen1 = stringToPrecomputedChunk(
3450 + '<style data-precedence="',
3451 +);
3452 +const styleTagResourceOpen2 = stringToPrecomputedChunk('" data-href="');
3453 +const spaceSeparator = stringToPrecomputedChunk(' ');
3454 +const styleTagResourceOpen3 = stringToPrecomputedChunk('">');
3455
3335 -function preloadBlockedStyle(this: Destination, resource: StyleResource) {
3336 - // The only Resources that should remain are Blocked resources
3337 - if (__DEV__) {
3338 - if ((resource.state & Blocked) === NoState) {
3339 - console.error(
3340 - 'React encountered a Stylesheet Resource that was not Blocked when it was expected to be. This is a bug in React.',
3341 - );
3342 - } else if (resource.state & PreloadFlushed) {
3343 - console.error(
3344 - 'React encountered a Stylesheet Resource that already flushed a Preload when it was not expected to. This is a bug in React.',
3345 - );
3346 - }
3347 - }
3348 - if (resource.type === 'style') {
3349 - // <style> tags do not need to be preloaded
3350 - return;
3351 - }
3352 - const chunks = resource.chunks;
3353 - const preloadProps = preloadAsStylePropsFromProps(
3354 - resource.props.href,
3355 - resource.props,
3356 - );
3357 - pushLinkImpl(chunks, preloadProps);
3358 - for (let i = 0; i < chunks.length; i++) {
3359 - writeChunk(this, chunks[i]);
3360 - }
3361 - resource.state |= PreloadFlushed;
3362 - chunks.length = 0;
3363 -}
3456 +const styleTagResourceClose = stringToPrecomputedChunk('</style>');
3457
3365 -function preloadBlockedStyles(
3458 +function flushAllStylesInPreamble(
3459 this: Destination,
3460 set: Set<StyleResource>,
3461 precedence: string,
3462 ) {
3370 - set.forEach(preloadBlockedStyle, this);
3463 + didFlushPrecedence = false;
3464 + set.forEach(flushStyleInPreamble, this);
3465 set.clear();
3466 +
3467 + const chunks = precedenceStyleTagResource.chunks;
3468 + const hrefs = precedenceStyleTagResource.props.hrefs;
3469 + if (didFlushPrecedence === false || chunks.length) {
3470 + writeChunk(this, styleTagResourceOpen1);
3471 + writeChunk(this, stringToChunk(escapeTextForBrowser(precedence)));
3472 + let i = 0;
3473 + if (hrefs.length) {
3474 + writeChunk(this, styleTagResourceOpen2);
3475 + for (; i < hrefs.length - 1; i++) {
3476 + writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
3477 + writeChunk(this, spaceSeparator);
3478 + }
3479 + writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
3480 + }
3481 + writeChunk(this, styleTagResourceOpen3);
3482 + for (i = 0; i < chunks.length; i++) {
3483 + writeChunk(this, chunks[i]);
3484 + }
3485 + writeChunk(this, styleTagResourceClose);
3486 +
3487 + // style resources can flush continuously since more rules may be written into
3488 + // them with new hrefs. Instead of marking it flushed, we simply reset the chunks
3489 + // and hrefs
3490 + chunks.length = 0;
3491 + hrefs.length = 0;
3492 + }
3493 }
3494
3495 function preloadLateStyle(this: Destination, resource: StyleResource) {
@@ -3480,10 +3601,7 @@ export function writePreamble(
3601 resources.fontPreloads.clear();
3602
3603 // Flush unblocked stylesheets by precedence
3483 - resources.precedences.forEach(flushUnblockedStyles, destination);
3484 -
3485 - // Flush preloads for Blocked stylesheets
3486 - resources.precedences.forEach(preloadBlockedStyles, destination);
3604 + resources.precedences.forEach(flushAllStylesInPreamble, destination);
3605
3606 resources.usedStylesheets.forEach(resource => {
3607 const key = getResourceKey(resource.props.as, resource.props.href);
@@ -3643,25 +3761,6 @@ export function writePostamble(
3761 }
3762 }
3763
3646 -function hasStyleResourceDependencies(
3647 - boundaryResources: BoundaryResources,
3648 -): boolean {
3649 - const iter = boundaryResources.values();
3650 - // At the moment boundaries only accumulate style resources
3651 - // so we assume the type is correct and don't check it
3652 - while (true) {
3653 - const {value: resource} = iter.next();
3654 - if (!resource) break;
3655 -
3656 - // If every style Resource flushed in the shell we do not need to send
3657 - // any dependencies
3658 - if ((resource.state & FlushedInPreamble) === NoState) {
3659 - return true;
3660 - }
3661 - }
3662 - return false;
3663 -}
3664 -
3764 const arrayFirstOpenBracket = stringToPrecomputedChunk('[');
3765 const arraySubsequentOpenBracket = stringToPrecomputedChunk(',[');
3766 const arrayInterstitial = stringToPrecomputedChunk(',');
@@ -3678,7 +3777,9 @@ function writeStyleResourceDependenciesInJS(
3777
3778 let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
3779 boundaryResources.forEach(resource => {
3681 - if (resource.state & FlushedInPreamble) {
3780 + if (resource.type === 'style') {
3781 + // Style dependencies don't require coordinated reveal and can be omitted
3782 + } else if (resource.state & FlushedInPreamble) {
3783 // We can elide this dependency because it was flushed in the shell and
3784 // should be ready before content is shown on the client
3785 } else if (resource.state & Flushed) {
@@ -3688,9 +3789,7 @@ function writeStyleResourceDependenciesInJS(
3789 writeChunk(destination, nextArrayOpenBrackChunk);
3790 writeStyleResourceDependencyHrefOnlyInJS(
3791 destination,
3691 - resource.type === 'style'
3692 - ? resource.props['data-href']
3693 - : resource.props.href,
3792 + resource.props.href,
3793 );
3794 writeChunk(destination, arrayCloseBracket);
3795 nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
@@ -3875,7 +3974,9 @@ function writeStyleResourceDependenciesInAttr(
3974
3975 let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
3976 boundaryResources.forEach(resource => {
3878 - if (resource.state & FlushedInPreamble) {
3977 + if (resource.type === 'style') {
3978 + // Style dependencies don't require coordinated reveal and can be omitted
3979 + } else if (resource.state & FlushedInPreamble) {
3980 // We can elide this dependency because it was flushed in the shell and
3981 // should be ready before content is shown on the client
3982 } else if (resource.state & Flushed) {
@@ -3885,9 +3986,7 @@ function writeStyleResourceDependenciesInAttr(
3986 writeChunk(destination, nextArrayOpenBrackChunk);
3987 writeStyleResourceDependencyHrefOnlyInAttr(
3988 destination,
3888 - resource.type === 'style'
3889 - ? resource.props['data-href']
3890 - : resource.props.href,
3989 + resource.props.href,
3990 );
3991 writeChunk(destination, arrayCloseBracket);
3992 nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
@@ -4135,9 +4234,8 @@ type StylesheetProps = {
4234 type StylesheetResource = TResource<'stylesheet', StylesheetProps>;
4235
4236 type StyleTagProps = {
4138 - 'data-href': string,
4139 - 'data-precedence': string,
4140 - [string]: mixed,
4237 + hrefs: Array<string>,
4238 + precedence: string,
4239 };
4240 type StyleTagResource = TResource<'style', StyleTagProps>;
4241
@@ -4168,6 +4266,7 @@ export type Resources = {
4266 fontPreloads: Set<PreloadResource>,
4267 // usedImagePreloads: Set<PreloadResource>,
4268 precedences: Map<string, Set<StyleResource>>,
4269 + stylePrecedences: Map<string, StyleTagResource>,
4270 usedStylesheets: Set<PreloadResource>,
4271 scripts: Set<ScriptResource>,
4272 usedScripts: Set<PreloadResource>,
@@ -4195,6 +4294,7 @@ export function createResources(): Resources {
4294 fontPreloads: new Set(),
4295 // usedImagePreloads: new Set(),
4296 precedences: new Map(),
4297 + stylePrecedences: new Map(),
4298 usedStylesheets: new Set(),
4299 scripts: new Set(),
4300 usedScripts: new Set(),
@@ -4615,6 +4715,25 @@ function preinitImpl(
4715 if (!precedenceSet) {
4716 precedenceSet = new Set();
4717 resources.precedences.set(precedence, precedenceSet);
4718 + const emptyStyleResource = {
4719 + type: 'style',
4720 + chunks: ([]: Array<Chunk | PrecomputedChunk>),
4721 + state: NoState,
4722 + props: {
4723 + precedence,
4724 + hrefs: ([]: Array<string>),
4725 + },
4726 + };
4727 + precedenceSet.add(emptyStyleResource);
4728 + if (__DEV__) {
4729 + if (resources.stylePrecedences.has(precedence)) {
4730 + console.error(
4731 + 'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
4732 + precedence,
4733 + );
4734 + }
4735 + }
4736 + resources.stylePrecedences.set(precedence, emptyStyleResource);
4737 }
4738 precedenceSet.add(resource);
4739 }
@@ -4769,16 +4888,6 @@ function adoptPreloadPropsForStylesheetProps(
4888 resourceProps.integrity = preloadProps.integrity;
4889 }
4890
4772 -function styleTagPropsFromRawProps(rawProps: any): StyleTagProps {
4773 - return {
4774 - ...rawProps,
4775 - 'data-precedence': rawProps.precedence,
4776 - precedence: null,
4777 - 'data-href': rawProps.href,
4778 - href: null,
4779 - };
4780 -}
4781 -
4891 function scriptPropsFromPreinitOptions(
4892 src: string,
4893 options: PreinitOptions,
@@ -4801,10 +4910,7 @@ function adoptPreloadPropsForScriptProps(
4910 resourceProps.integrity = preloadProps.integrity;
4911 }
4912
4804 -function hoistStylesheetResource(
4805 - this: BoundaryResources,
4806 - resource: StyleResource,
4807 -) {
4913 +function hoistStyleResource(this: BoundaryResources, resource: StyleResource) {
4914 this.add(resource);
4915 }
4916
@@ -4814,23 +4920,10 @@ export function hoistResources(
4920 ): void {
4921 const currentBoundaryResources = resources.boundaryResources;
4922 if (currentBoundaryResources) {
4817 - source.forEach(hoistStylesheetResource, currentBoundaryResources);
4818 - source.clear();
4923 + source.forEach(hoistStyleResource, currentBoundaryResources);
4924 }
4925 }
4926
4822 -function unblockStylesheet(resource: StyleResource) {
4823 - resource.state &= ~Blocked;
4824 -}
4825 -
4826 -export function hoistResourcesToRoot(
4827 - resources: Resources,
4828 - boundaryResources: BoundaryResources,
4829 -): void {
4830 - boundaryResources.forEach(unblockStylesheet);
4831 - boundaryResources.clear();
4832 -}
4833 -
4927 function markAsRenderedResourceDEV(
4928 resource: Resource,
4929 originalProps: any,
packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js
+2 -1
@@ -52,6 +52,7 @@ export type ResponseState = {
52 preconnectChunks: Array<Chunk | PrecomputedChunk>,
53 preloadChunks: Array<Chunk | PrecomputedChunk>,
54 hoistableChunks: Array<Chunk | PrecomputedChunk>,
55 + stylesToHoist: boolean,
56 // This is an extra field for the legacy renderer
57 generateStaticMarkup: boolean,
58 };
@@ -88,6 +89,7 @@ export function createResponseState(
89 preconnectChunks: responseState.preconnectChunks,
90 preloadChunks: responseState.preloadChunks,
91 hoistableChunks: responseState.hoistableChunks,
92 + stylesToHoist: responseState.stylesToHoist,
93
94 // This is an extra field for the legacy renderer
95 generateStaticMarkup,
@@ -134,7 +136,6 @@ export {
136 writeHoistables,
137 writePostamble,
138 hoistResources,
137 - hoistResourcesToRoot,
139 setCurrentlyRenderingBoundaryResourcesTarget,
140 prepareToRender,
141 cleanupAfterRender,
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js
+75 -75
@@ -20,74 +20,69 @@ const resourceMap = new Map();
20 export function completeBoundaryWithStyles(
21 suspenseBoundaryID,
22 contentID,
23 - styles,
23 + stylesheetDescriptors,
24 ) {
25 - const stylesToHoist = new Map();
25 const precedences = new Map();
26 const thisDocument = document;
27 let lastResource, node;
28
30 - let nodes = thisDocument.querySelectorAll('template[data-precedence]');
31 - for (let i = 0; (node = nodes[i++]); ) {
32 - let child = node.content.firstChild;
33 - for (; child; child = child.nextSibling) {
34 - stylesToHoist.set(child.getAttribute('data-href'), child);
35 - }
36 - node.parentNode.removeChild(node);
37 - }
38 -
39 - // Seed the precedence list with existing resources
40 - nodes = thisDocument.querySelectorAll(
29 + // Seed the precedence list with existing resources and collect hoistable style tags
30 + const nodes = thisDocument.querySelectorAll(
31 'link[data-precedence],style[data-precedence]',
32 );
33 + const styleTagsToHoist = [];
34 for (let i = 0; (node = nodes[i++]); ) {
44 - // We populate the resourceMap from found nodes so we can incorporate any
45 - // resources the client runtime adds when the two runtimes are running concurrently
46 - resourceMap.set(
47 - node.getAttribute(node.nodeName === 'STYLE' ? 'data-href' : 'href'),
48 - node,
49 - );
50 - precedences.set(node.dataset['precedence'], (lastResource = node));
35 + if (node.getAttribute('media') === 'not all') {
36 + styleTagsToHoist.push(node);
37 + } else {
38 + if (node.tagName === 'LINK') {
39 + resourceMap.set(node.getAttribute('href'), node);
40 + }
41 + precedences.set(node.dataset['precedence'], (lastResource = node));
42 + }
43 }
44
45 let i = 0;
46 const dependencies = [];
55 - let style, href, precedence, attr, loadingState, resourceEl, media;
47 + let href, precedence, attr, loadingState, resourceEl, media;
48
49 function setStatus(s) {
50 this['s'] = s;
51 }
52
61 - while ((style = styles[i++])) {
62 - let j = 0;
63 - href = style[j++];
53 + // Sheets Mode
54 + let sheetMode = true;
55 + while (true) {
56 + if (sheetMode) {
57 + // Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for
58 + // dependency if they already existed
59 + const stylesheetDescriptor = stylesheetDescriptors[i++];
60 + if (!stylesheetDescriptor) {
61 + // enter <style> Mode
62 + sheetMode = false;
63 + i = 0;
64 + continue;
65 + }
66
65 - if ((resourceEl = resourceMap.get(href))) {
66 - // We have an already known resource. It could be a <style>, a <link> created
67 - // by this runtime (which will have a loadingState) or a <link> created by
68 - // the client runtime (which will also have a loadingState). We look for a
69 - // loadingState and test whether it is not loaded yet and whether the media matches
70 - // before using it as a dependency. If it is a <style> there will be no loadingState
71 - // and we can avoid tracking it as a dependency because these tags don't load
72 - } else {
73 - // We haven't already processed this href so we need to hoist an element. It will
74 - // either be a <style> that was sent in a <template> and prepped in `stylesToHoist`
75 - // or we will need to create a <link>
76 - if ((resourceEl = stylesToHoist.get(href))) {
77 - // We have a <style> which needs to be hoisted to the correct precedence
78 - // We set it in the resourceMap so we can bail out on future passes
79 - // if this is depended on more than once
80 - precedence = resourceEl.getAttribute('data-precedence');
67 + let avoidInsert = false;
68 + let j = 0;
69 + href = stylesheetDescriptor[j++];
70 +
71 + if ((resourceEl = resourceMap.get(href))) {
72 + // We have an already inserted stylesheet.
73 + loadingState = resourceEl['_p'];
74 + avoidInsert = true;
75 } else {
82 - // If we got this far we are depending on a <link> which is not yet in the document.
76 + // We haven't already processed this href so we need to construct a stylesheet and hoist it
77 // We construct it here and attach a loadingState. We also check whether it matches
78 // media before we include it in the dependency array.
79 resourceEl = thisDocument.createElement('link');
80 resourceEl.href = href;
81 resourceEl.rel = 'stylesheet';
88 - resourceEl.dataset['precedence'] = precedence = style[j++];
89 - while ((attr = style[j++])) {
90 - resourceEl.setAttribute(attr, style[j++]);
82 + resourceEl.dataset['precedence'] = precedence =
83 + stylesheetDescriptor[j++];
84 + while ((attr = stylesheetDescriptor[j++])) {
85 + resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
86 }
87 loadingState = resourceEl['_p'] = new Promise((re, rj) => {
88 resourceEl.onload = re;
@@ -97,42 +92,47 @@ export function completeBoundaryWithStyles(
92 setStatus.bind(loadingState, LOADED),
93 setStatus.bind(loadingState, ERRORED),
94 );
95 + // Save this resource element so we can bailout if it is used again
96 + resourceMap.set(href, resourceEl);
97 }
101 - // Save this resource element so we can bailout if it is used again
102 - resourceMap.set(href, resourceEl);
103 -
104 - // The prior style resource is the last one placed at a given
105 - // precedence or the last resource itself which may be null.
106 - // We grab this value and then update the last resource for this
107 - // precedence to be the inserted element, updating the lastResource
108 - // pointer if needed.
109 - const prior = precedences.get(precedence) || lastResource;
110 - if (prior === lastResource) {
111 - lastResource = resourceEl;
98 + media = resourceEl.getAttribute('media');
99 + if (
100 + loadingState &&
101 + loadingState['s'] !== 'l' &&
102 + (!media || window['matchMedia'](media).matches)
103 + ) {
104 + dependencies.push(loadingState);
105 }
113 - precedences.set(precedence, resourceEl);
114 -
115 - // Finally, we insert the newly constructed instance at an appropriate location
116 - // in the Document.
117 - if (prior) {
118 - prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
119 - } else {
120 - const head = thisDocument.head;
121 - head.insertBefore(resourceEl, head.firstChild);
106 + if (avoidInsert) {
107 + // We have a link that is already in the document. We don't want to fall through to the insert path
108 + continue;
109 + }
110 + } else {
111 + // <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.
112 + resourceEl = styleTagsToHoist[i++];
113 + if (!resourceEl) {
114 + // we are done with all style tags
115 + break;
116 }
117 +
118 + precedence = resourceEl.getAttribute('data-precedence');
119 + resourceEl.removeAttribute('media');
120 + }
121 +
122 + // resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting
123 + const prior = precedences.get(precedence) || lastResource;
124 + if (prior === lastResource) {
125 + lastResource = resourceEl;
126 }
127 + precedences.set(precedence, resourceEl);
128
125 - // If we are a <link> we will have a loadingState and we can use this
126 - // combined with matchMedia to decide if we need to await this dependency
127 - // loading. <style> tags won't have a loadingState so they are never awaited
128 - loadingState = resourceEl['_p'];
129 - media = resourceEl.getAttribute('media');
130 - if (
131 - loadingState &&
132 - loadingState['s'] !== 'l' &&
133 - (!media || window['matchMedia'](media).matches)
134 - ) {
135 - dependencies.push(loadingState);
129 + // Finally, we insert the newly constructed instance at an appropriate location
130 + // in the Document.
131 + if (prior) {
132 + prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
133 + } else {
134 + const head = thisDocument.head;
135 + head.insertBefore(resourceEl, head.firstChild);
136 }
137 }
138
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+1 -1
@@ -6,6 +6,6 @@ export const clientRenderBoundary =
6 export const completeBoundary =
7 '$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()}};';
8 export const completeBoundaryWithStyles =
9 - '$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"))};';
9 + '$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"))};';
10 export const completeSegment =
11 '$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)};';
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js
+75 -75
@@ -20,77 +20,72 @@ export {clientRenderBoundary, completeBoundary, completeSegment};
20 export function completeBoundaryWithStyles(
21 suspenseBoundaryID,
22 contentID,
23 - styles,
23 + stylesheetDescriptors,
24 ) {
25 const completeBoundaryImpl = window['$RC'];
26 const resourceMap = window['$RM'];
27
28 - const stylesToHoist = new Map();
28 const precedences = new Map();
29 const thisDocument = document;
30 let lastResource, node;
31
33 - let nodes = thisDocument.querySelectorAll('template[data-precedence]');
34 - for (let i = 0; (node = nodes[i++]); ) {
35 - let child = node.content.firstChild;
36 - for (; child; child = child.nextSibling) {
37 - stylesToHoist.set(child.getAttribute('data-href'), child);
38 - }
39 - node.parentNode.removeChild(node);
40 - }
41 -
42 - // Seed the precedence list with existing resources
43 - nodes = thisDocument.querySelectorAll(
32 + // Seed the precedence list with existing resources and collect hoistable style tags
33 + const nodes = thisDocument.querySelectorAll(
34 'link[data-precedence],style[data-precedence]',
35 );
36 + const styleTagsToHoist = [];
37 for (let i = 0; (node = nodes[i++]); ) {
47 - // We populate the resourceMap from found nodes so we can incorporate any
48 - // resources the client runtime adds when the two runtimes are running concurrently
49 - resourceMap.set(
50 - node.getAttribute(node.nodeName === 'STYLE' ? 'data-href' : 'href'),
51 - node,
52 - );
53 - precedences.set(node.dataset['precedence'], (lastResource = node));
38 + if (node.getAttribute('media') === 'not all') {
39 + styleTagsToHoist.push(node);
40 + } else {
41 + if (node.tagName === 'LINK') {
42 + resourceMap.set(node.getAttribute('href'), node);
43 + }
44 + precedences.set(node.dataset['precedence'], (lastResource = node));
45 + }
46 }
47
48 let i = 0;
49 const dependencies = [];
58 - let style, href, precedence, attr, loadingState, resourceEl, media;
50 + let href, precedence, attr, loadingState, resourceEl, media;
51
52 function setStatus(s) {
53 this['s'] = s;
54 }
55
64 - while ((style = styles[i++])) {
65 - let j = 0;
66 - href = style[j++];
56 + // Sheets Mode
57 + let sheetMode = true;
58 + while (true) {
59 + if (sheetMode) {
60 + // Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for
61 + // dependency if they already existed
62 + const stylesheetDescriptor = stylesheetDescriptors[i++];
63 + if (!stylesheetDescriptor) {
64 + // enter <style> Mode
65 + sheetMode = false;
66 + i = 0;
67 + continue;
68 + }
69
68 - if ((resourceEl = resourceMap.get(href))) {
69 - // We have an already known resource. It could be a <style>, a <link> created
70 - // by this runtime (which will have a loadingState) or a <link> created by
71 - // the client runtime (which will also have a loadingState). We look for a
72 - // loadingState and test whether it is not loaded yet and whether the media matches
73 - // before using it as a dependency. If it is a <style> there will be no loadingState
74 - // and we can avoid tracking it as a dependency because these tags don't load
75 - } else {
76 - // We haven't already processed this href so we need to hoist an element. It will
77 - // either be a <style> that was sent in a <template> and prepped in `stylesToHoist`
78 - // or we will need to create a <link>
79 - if ((resourceEl = stylesToHoist.get(href))) {
80 - // We have a <style> which needs to be hoisted to the correct precedence
81 - // We set it in the resourceMap so we can bail out on future passes
82 - // if this is depended on more than once
83 - precedence = resourceEl.getAttribute('data-precedence');
70 + let avoidInsert = false;
71 + let j = 0;
72 + href = stylesheetDescriptor[j++];
73 +
74 + if ((resourceEl = resourceMap.get(href))) {
75 + // We have an already inserted stylesheet.
76 + loadingState = resourceEl['_p'];
77 + avoidInsert = true;
78 } else {
85 - // If we got this far we are depending on a <link> which is not yet in the document.
79 + // We haven't already processed this href so we need to construct a stylesheet and hoist it
80 // We construct it here and attach a loadingState. We also check whether it matches
81 // media before we include it in the dependency array.
82 resourceEl = thisDocument.createElement('link');
83 resourceEl.href = href;
84 resourceEl.rel = 'stylesheet';
91 - resourceEl.dataset['precedence'] = precedence = style[j++];
92 - while ((attr = style[j++])) {
93 - resourceEl.setAttribute(attr, style[j++]);
85 + resourceEl.dataset['precedence'] = precedence =
86 + stylesheetDescriptor[j++];
87 + while ((attr = stylesheetDescriptor[j++])) {
88 + resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
89 }
90 loadingState = resourceEl['_p'] = new Promise((re, rj) => {
91 resourceEl.onload = re;
@@ -100,42 +95,47 @@ export function completeBoundaryWithStyles(
95 setStatus.bind(loadingState, LOADED),
96 setStatus.bind(loadingState, ERRORED),
97 );
98 + // Save this resource element so we can bailout if it is used again
99 + resourceMap.set(href, resourceEl);
100 }
104 - // Save this resource element so we can bailout if it is used again
105 - resourceMap.set(href, resourceEl);
106 -
107 - // The prior style resource is the last one placed at a given
108 - // precedence or the last resource itself which may be null.
109 - // We grab this value and then update the last resource for this
110 - // precedence to be the inserted element, updating the lastResource
111 - // pointer if needed.
112 - const prior = precedences.get(precedence) || lastResource;
113 - if (prior === lastResource) {
114 - lastResource = resourceEl;
101 + media = resourceEl.getAttribute('media');
102 + if (
103 + loadingState &&
104 + loadingState['s'] !== 'l' &&
105 + (!media || window['matchMedia'](media).matches)
106 + ) {
107 + dependencies.push(loadingState);
108 }
116 - precedences.set(precedence, resourceEl);
117 -
118 - // Finally, we insert the newly constructed instance at an appropriate location
119 - // in the Document.
120 - if (prior) {
121 - prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
122 - } else {
123 - const head = thisDocument.head;
124 - head.insertBefore(resourceEl, head.firstChild);
109 + if (avoidInsert) {
110 + // We have a link that is already in the document. We don't want to fall through to the insert path
111 + continue;
112 + }
113 + } else {
114 + // <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.
115 + resourceEl = styleTagsToHoist[i++];
116 + if (!resourceEl) {
117 + // we are done with all style tags
118 + break;
119 }
120 +
121 + precedence = resourceEl.getAttribute('data-precedence');
122 + resourceEl.removeAttribute('media');
123 + }
124 +
125 + // resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting
126 + const prior = precedences.get(precedence) || lastResource;
127 + if (prior === lastResource) {
128 + lastResource = resourceEl;
129 }
130 + precedences.set(precedence, resourceEl);
131
128 - // If we are a <link> we will have a loadingState and we can use this
129 - // combined with matchMedia to decide if we need to await this dependency
130 - // loading. <style> tags won't have a loadingState so they are never awaited
131 - loadingState = resourceEl['_p'];
132 - media = resourceEl.getAttribute('media');
133 - if (
134 - loadingState &&
135 - loadingState['s'] !== 'l' &&
136 - (!media || window['matchMedia'](media).matches)
137 - ) {
138 - dependencies.push(loadingState);
132 + // Finally, we insert the newly constructed instance at an appropriate location
133 + // in the Document.
134 + if (prior) {
135 + prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
136 + } else {
137 + const head = thisDocument.head;
138 + head.insertBefore(resourceEl, head.firstChild);
139 }
140 }
141
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+267 -18
@@ -1281,8 +1281,8 @@ body {
1281 <html>
1282 <head>
1283 <link rel="stylesheet" href="initial" data-precedence="one" />
1284 + <link rel="stylesheet" href="foo" data-precedence="one" />
1285 <link rel="stylesheet" href="preset" data-precedence="preset" />
1285 - <link rel="preload" href="foo" as="style" />
1286 </head>
1287 <body>
1288 <div>loading foo bar...</div>
@@ -1304,7 +1304,6 @@ body {
1304 <link rel="stylesheet" href="foo" data-precedence="one" />
1305 <link rel="stylesheet" href="preset" data-precedence="preset" />
1306 <link rel="stylesheet" href="bar" data-precedence="default" />
1307 - <link rel="preload" href="foo" as="style" />
1307 </head>
1308 <body>
1309 <div>loading foo bar...</div>
@@ -1329,7 +1328,6 @@ body {
1328 <link rel="stylesheet" href="foo" data-precedence="one" />
1329 <link rel="stylesheet" href="preset" data-precedence="preset" />
1330 <link rel="stylesheet" href="bar" data-precedence="default" />
1332 - <link rel="preload" href="foo" as="style" />
1331 </head>
1332 <body>
1333 <div>loading foo bar...</div>
@@ -1354,7 +1352,6 @@ body {
1352 <link rel="stylesheet" href="foo" data-precedence="one" />
1353 <link rel="stylesheet" href="preset" data-precedence="preset" />
1354 <link rel="stylesheet" href="bar" data-precedence="default" />
1357 - <link rel="preload" href="foo" as="style" />
1355 </head>
1356 <body>
1357 <div>
@@ -1379,7 +1376,6 @@ body {
1376 <link rel="stylesheet" href="foo" data-precedence="one" />
1377 <link rel="stylesheet" href="preset" data-precedence="preset" />
1378 <link rel="stylesheet" href="bar" data-precedence="default" />
1382 - <link rel="preload" href="foo" as="style" />
1379 </head>
1380 <body>
1381 <div>
@@ -1407,7 +1403,6 @@ body {
1403 <link rel="stylesheet" href="preset" data-precedence="preset" />
1404 <link rel="stylesheet" href="bar" data-precedence="default" />
1405 <link rel="stylesheet" href="baz" data-precedence="two" />
1410 - <link rel="preload" href="foo" as="style" />
1406 </head>
1407 <body>
1408 <div>
@@ -1449,7 +1444,6 @@ body {
1444 <link rel="stylesheet" href="preset" data-precedence="preset" />
1445 <link rel="stylesheet" href="bar" data-precedence="default" />
1446 <link rel="stylesheet" href="baz" data-precedence="two" />
1452 - <link rel="preload" href="foo" as="style" />
1447 </head>
1448 <body>
1449 <div>
@@ -1482,7 +1476,6 @@ body {
1476 <link rel="stylesheet" href="preset" data-precedence="preset" />
1477 <link rel="stylesheet" href="bar" data-precedence="default" />
1478 <link rel="stylesheet" href="baz" data-precedence="two" />
1485 - <link rel="preload" href="foo" as="style" />
1479 </head>
1480 <body>
1481 <div>
@@ -3499,6 +3492,7 @@ body {
3492 <div>1</div>,
3493 ]);
3494 });
3495 +
3496 // @gate enableFloat
3497 it('escapes hrefs when selecting matching elements in the document when rendering Resources', async () => {
3498 function App() {
@@ -4157,19 +4151,14 @@ background-color: green;
4151 resolveText('first');
4152 });
4153
4160 - const styleTemplates = document.querySelectorAll(
4161 - 'template[data-precedence]',
4162 - );
4163 - expect(styleTemplates.length).toBe(1);
4164 - expect(getMeaningfulChildren(styleTemplates[0].content)).toEqual(
4165 - <style data-href="foo" data-precedence="default">
4166 - {css}
4167 - </style>,
4168 - );
4154 expect(getMeaningfulChildren(document)).toEqual(
4155 <html>
4156 <head />
4172 - <body />
4157 + <body>
4158 + <style data-href="foo" data-precedence="default" media="not all">
4159 + {css}
4160 + </style>
4161 + </body>
4162 </html>,
4163 );
4164
@@ -4194,6 +4183,266 @@ background-color: green;
4183 </html>,
4184 );
4185 });
4186 +
4187 + it('can hoist styles flushed early even when no other style dependencies are flushed on completion', async () => {
4188 + await actIntoEmptyDocument(() => {
4189 + renderToPipeableStream(
4190 + <html>
4191 + <body>
4192 + <Suspense fallback="loading...">
4193 + <BlockedOn value="first">
4194 + <style href="foo" precedence="default">
4195 + some css
4196 + </style>
4197 + <div>first</div>
4198 + <BlockedOn value="second">
4199 + <div>second</div>
4200 + </BlockedOn>
4201 + </BlockedOn>
4202 + </Suspense>
4203 + </body>
4204 + </html>,
4205 + ).pipe(writable);
4206 + });
4207 + expect(getMeaningfulChildren(document)).toEqual(
4208 + <html>
4209 + <head />
4210 + <body>loading...</body>
4211 + </html>,
4212 + );
4213 +
4214 + // When we resolve first we flush the style tag because it is ready but we aren't yet ready to
4215 + // flush the entire boundary and reveal it.
4216 + await act(() => {
4217 + resolveText('first');
4218 + });
4219 + expect(getMeaningfulChildren(document)).toEqual(
4220 + <html>
4221 + <head />
4222 + <body>
4223 + loading...
4224 + <style data-href="foo" data-precedence="default" media="not all">
4225 + some css
4226 + </style>
4227 + </body>
4228 + </html>,
4229 + );
4230 +
4231 + // When we resolve second we flush the rest of the boundary segments and reveal the boundary. The style tag
4232 + // is hoisted during this reveal process even though no other styles flushed during this tick
4233 + await act(() => {
4234 + resolveText('second');
4235 + });
4236 + expect(getMeaningfulChildren(document)).toEqual(
4237 + <html>
4238 + <head>
4239 + <style data-href="foo" data-precedence="default">
4240 + some css
4241 + </style>
4242 + </head>
4243 + <body>
4244 + <div>first</div>
4245 + <div>second</div>
4246 + </body>
4247 + </html>,
4248 + );
4249 + });
4250 +
4251 + it('can emit multiple style rules into a single style tag for a given precedence', async () => {
4252 + await actIntoEmptyDocument(() => {
4253 + renderToPipeableStream(
4254 + <html>
4255 + <body>
4256 + <style href="1" precedence="default">
4257 + 1
4258 + </style>
4259 + <style href="2" precedence="foo">
4260 + foo2
4261 + </style>
4262 + <style href="3" precedence="default">
4263 + 3
4264 + </style>
4265 + <style href="4" precedence="default">
4266 + 4
4267 + </style>
4268 + <style href="5" precedence="foo">
4269 + foo5
4270 + </style>
4271 + <div>initial</div>
4272 + <Suspense fallback="loading...">
4273 + <BlockedOn value="first">
4274 + <style href="6" precedence="default">
4275 + 6
4276 + </style>
4277 + <style href="7" precedence="foo">
4278 + foo7
4279 + </style>
4280 + <style href="8" precedence="default">
4281 + 8
4282 + </style>
4283 + <style href="9" precedence="default">
4284 + 9
4285 + </style>
4286 + <style href="10" precedence="foo">
4287 + foo10
4288 + </style>
4289 + <div>first</div>
4290 + <BlockedOn value="second">
4291 + <style href="11" precedence="default">
4292 + 11
4293 + </style>
4294 + <style href="12" precedence="foo">
4295 + foo12
4296 + </style>
4297 + <style href="13" precedence="default">
4298 + 13
4299 + </style>
4300 + <style href="14" precedence="default">
4301 + 14
4302 + </style>
4303 + <style href="15" precedence="foo">
4304 + foo15
4305 + </style>
4306 + <div>second</div>
4307 + </BlockedOn>
4308 + </BlockedOn>
4309 + </Suspense>
4310 + </body>
4311 + </html>,
4312 + ).pipe(writable);
4313 + });
4314 + expect(getMeaningfulChildren(document)).toEqual(
4315 + <html>
4316 + <head>
4317 + <style data-href="1 3 4" data-precedence="default">
4318 + 134
4319 + </style>
4320 + <style data-href="2 5" data-precedence="foo">
4321 + foo2foo5
4322 + </style>
4323 + </head>
4324 + <body>
4325 + <div>initial</div>loading...
4326 + </body>
4327 + </html>,
4328 + );
4329 +
4330 + // When we resolve first we flush the style tag because it is ready but we aren't yet ready to
4331 + // flush the entire boundary and reveal it.
4332 + await act(() => {
4333 + resolveText('first');
4334 + });
4335 + await act(() => {
4336 + resolveText('second');
4337 + });
4338 +
4339 + // Some sets of styles were ready before the entire boundary and they got emitted as early as they were
4340 + // ready. The remaining styles were ready when the boundary finished and they got grouped as well
4341 + expect(getMeaningfulChildren(document)).toEqual(
4342 + <html>
4343 + <head>
4344 + <style data-href="1 3 4" data-precedence="default">
4345 + 134
4346 + </style>
4347 + <style data-href="6 8 9" data-precedence="default">
4348 + 689
4349 + </style>
4350 + <style data-href="11 13 14" data-precedence="default">
4351 + 111314
4352 + </style>
4353 + <style data-href="2 5" data-precedence="foo">
4354 + foo2foo5
4355 + </style>
4356 + <style data-href="7 10" data-precedence="foo">
4357 + foo7foo10
4358 + </style>
4359 + <style data-href="12 15" data-precedence="foo">
4360 + foo12foo15
4361 + </style>
4362 + </head>
4363 + <body>
4364 + <div>initial</div>
4365 + <div>first</div>
4366 + <div>second</div>
4367 + </body>
4368 + </html>,
4369 + );
4370 +
4371 + // Client inserted style tags are not grouped together but can hydrate against a grouped set
4372 + ReactDOMClient.hydrateRoot(
4373 + document,
4374 + <html>
4375 + <body>
4376 + <style href="1" precedence="default">
4377 + 1
4378 + </style>
4379 + <style href="2" precedence="foo">
4380 + foo2
4381 + </style>
4382 + <style href="16" precedence="default">
4383 + 16
4384 + </style>
4385 + <style href="17" precedence="default">
4386 + 17
4387 + </style>
4388 + </body>
4389 + </html>,
4390 + );
4391 + expect(Scheduler).toFlushWithoutYielding();
4392 + expect(getMeaningfulChildren(document)).toEqual(
4393 + <html>
4394 + <head>
4395 + <style data-href="1 3 4" data-precedence="default">
4396 + 134
4397 + </style>
4398 + <style data-href="6 8 9" data-precedence="default">
4399 + 689
4400 + </style>
4401 + <style data-href="11 13 14" data-precedence="default">
4402 + 111314
4403 + </style>
4404 + <style data-href="16" data-precedence="default">
4405 + 16
4406 + </style>
4407 + <style data-href="17" data-precedence="default">
4408 + 17
4409 + </style>
4410 + <style data-href="2 5" data-precedence="foo">
4411 + foo2foo5
4412 + </style>
4413 + <style data-href="7 10" data-precedence="foo">
4414 + foo7foo10
4415 + </style>
4416 + <style data-href="12 15" data-precedence="foo">
4417 + foo12foo15
4418 + </style>
4419 + </head>
4420 + <body>
4421 + <div>initial</div>
4422 + <div>first</div>
4423 + <div>second</div>
4424 + </body>
4425 + </html>,
4426 + );
4427 + });
4428 +
4429 + it('warns if you render a <style> with an href with a space on the server', async () => {
4430 + await expect(async () => {
4431 + await actIntoEmptyDocument(() => {
4432 + renderToPipeableStream(
4433 + <html>
4434 + <body>
4435 + <style href="foo bar" precedence="default">
4436 + style
4437 + </style>
4438 + </body>
4439 + </html>,
4440 + ).pipe(writable);
4441 + });
4442 + }).toErrorDev(
4443 + '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".',
4444 + );
4445 + });
4446 });
4447
4448 describe('Script Resources', () => {
packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+1 -5
@@ -339,11 +339,6 @@ export function hoistResources(
339 boundaryResources: BoundaryResources,
340 ) {}
341
342 -export function hoistResourcesToRoot(
343 - resources: Resources,
344 - boundaryResources: BoundaryResources,
345 -) {}
346 -
342 export function prepareToRender(resources: Resources) {}
343 export function cleanupAfterRender(previousDispatcher: mixed) {}
344 export function createResources() {}
@@ -356,6 +351,7 @@ export function setCurrentlyRenderingBoundaryResourcesTarget(
351 export function writeResourcesForBoundary(
352 destination: Destination,
353 boundaryResources: BoundaryResources,
354 + responseState: ResponseState,
355 ): boolean {
356 return true;
357 }
packages/react-server/src/ReactFizzServer.js
+10 -24
@@ -70,7 +70,6 @@ import {
70 writeHoistables,
71 writePostamble,
72 hoistResources,
73 - hoistResourcesToRoot,
73 prepareToRender,
74 cleanupAfterRender,
75 setCurrentlyRenderingBoundaryResourcesTarget,
@@ -594,11 +593,6 @@ function renderSuspenseBoundary(
593 contentRootSegment.textEmbedded,
594 );
595 contentRootSegment.status = COMPLETED;
597 - if (enableFloat) {
598 - if (newBoundary.pendingTasks === 0) {
599 - hoistCompletedBoundaryResources(request, newBoundary);
600 - }
601 - }
596 queueCompletedSegment(newBoundary, contentRootSegment);
597 if (newBoundary.pendingTasks === 0) {
598 // This must have been the last segment we were waiting on. This boundary is now complete.
@@ -652,19 +646,6 @@ function renderSuspenseBoundary(
646 popComponentStackInDEV(task);
647 }
648
655 -function hoistCompletedBoundaryResources(
656 - request: Request,
657 - completedBoundary: SuspenseBoundary,
658 -): void {
659 - if (request.completedRootSegment !== null || request.pendingRootTasks > 0) {
660 - // The Shell has not flushed yet. we can hoist Resources for this boundary
661 - // all the way to the Root.
662 - hoistResourcesToRoot(request.resources, completedBoundary.resources);
663 - }
664 - // We don't hoist if the root already flushed because late resources will be hoisted
665 - // as boundaries flush
666 -}
667 -
649 function renderBackupSuspenseBoundary(
650 request: Request,
651 task: Task,
@@ -1802,9 +1783,6 @@ function finishedTask(
1783 queueCompletedSegment(boundary, segment);
1784 }
1785 }
1805 - if (enableFloat) {
1806 - hoistCompletedBoundaryResources(request, boundary);
1807 - }
1786 if (boundary.parentFlushed) {
1787 // The segment might be part of a segment that didn't flush yet, but if the boundary's
1788 // parent flushed, we need to schedule the boundary to be emitted.
@@ -2177,7 +2155,11 @@ function flushCompletedBoundary(
2155 completedSegments.length = 0;
2156
2157 if (enableFloat) {
2180 - writeResourcesForBoundary(destination, boundary.resources);
2158 + writeResourcesForBoundary(
2159 + destination,
2160 + boundary.resources,
2161 + request.responseState,
2162 + );
2163 }
2164
2165 return writeCompletedBoundaryInstruction(
@@ -2221,7 +2203,11 @@ function flushPartialBoundary(
2203 // if there is no backpressure. Later before we complete the boundary we
2204 // will write resources regardless of backpressure before we emit the
2205 // completion instruction
2224 - return writeResourcesForBoundary(destination, boundary.resources);
2206 + return writeResourcesForBoundary(
2207 + destination,
2208 + boundary.resources,
2209 + request.responseState,
2210 + );
2211 } else {
2212 return true;
2213 }
packages/react-server/src/forks/ReactServerFormatConfig.custom.js
-1
@@ -78,7 +78,6 @@ export const writePreamble = $$$hostConfig.writePreamble;
78 export const writeHoistables = $$$hostConfig.writeHoistables;
79 export const writePostamble = $$$hostConfig.writePostamble;
80 export const hoistResources = $$$hostConfig.hoistResources;
81 -export const hoistResourcesToRoot = $$$hostConfig.hoistResourcesToRoot;
81 export const createResources = $$$hostConfig.createResources;
82 export const createBoundaryResources = $$$hostConfig.createBoundaryResources;
83 export const setCurrentlyRenderingBoundaryResourcesTarget =
packages/shared/ReactFeatureFlags.js
+1 -1
@@ -113,7 +113,7 @@ export const enableUseEffectEventHook = __EXPERIMENTAL__;
113 // Test in www before enabling in open source.
114 // Enables DOM-server to stream its instruction set as data-attributes
115 // (handled with an MutationObserver) instead of inline-scripts
116 -export const enableFizzExternalRuntime = false;
116 +export const enableFizzExternalRuntime = true;
117
118 // -----------------------------------------------------------------------------
119 // Chopping Block