mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[Fizz] Outline a Suspense Boundary if it has Suspensey CSS or Images (#34552)
We should favor outlining a boundary if it contains Suspensey CSS or
Suspensey Images since then we can load that content separately and not
block the main content. This also allows us to animate the reveal.
For example this should be able to animate the reveal even though the
actual HTML content isn't large in this case it's worth outlining so
that the JS runtime can choose to animate this reveal.
```js
<ViewTransition>
<Suspense>
<img src="..." />
</Suspense>
</ViewTransition>
```
For Suspensey Images, in Fizz, we currently only implement the suspensey
semantics when a View Transition is running. Therefore the outlining
only applies if it appears inside a Suspense boundary which might
animate. Otherwise there's no point in outlining. It is also only if the
Suspense boundary itself might animate its appear and not just any
ViewTransition. So the effect is very conservative.
For CSS it applies even without ViewTransition though, since it can help
unblock the main content faster.
This commit is contained in:
parent
ac2c1a5a58
commit
6eb5d67e9c
|
|
@ -238,8 +238,8 @@ export default function Page({url, navigate}) {
|
||||||
<Suspend />
|
<Suspend />
|
||||||
</div>
|
</div>
|
||||||
</ViewTransition>
|
</ViewTransition>
|
||||||
|
{show ? <Component /> : null}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
{show ? <Component /> : null}
|
|
||||||
</div>
|
</div>
|
||||||
</ViewTransition>
|
</ViewTransition>
|
||||||
</SwipeRecognizer>
|
</SwipeRecognizer>
|
||||||
|
|
|
||||||
|
|
@ -782,13 +782,14 @@ const HTML_COLGROUP_MODE = 9;
|
||||||
|
|
||||||
type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
||||||
|
|
||||||
const NO_SCOPE = /* */ 0b000000;
|
const NO_SCOPE = /* */ 0b0000000;
|
||||||
const NOSCRIPT_SCOPE = /* */ 0b000001;
|
const NOSCRIPT_SCOPE = /* */ 0b0000001;
|
||||||
const PICTURE_SCOPE = /* */ 0b000010;
|
const PICTURE_SCOPE = /* */ 0b0000010;
|
||||||
const FALLBACK_SCOPE = /* */ 0b000100;
|
const FALLBACK_SCOPE = /* */ 0b0000100;
|
||||||
const EXIT_SCOPE = /* */ 0b001000; // A direct Instance below a Suspense fallback is the only thing that can "exit"
|
const EXIT_SCOPE = /* */ 0b0001000; // A direct Instance below a Suspense fallback is the only thing that can "exit"
|
||||||
const ENTER_SCOPE = /* */ 0b010000; // A direct Instance below Suspense content is the only thing that can "enter"
|
const ENTER_SCOPE = /* */ 0b0010000; // A direct Instance below Suspense content is the only thing that can "enter"
|
||||||
const UPDATE_SCOPE = /* */ 0b100000; // Inside a scope that applies "update" ViewTransitions if anything mutates here.
|
const UPDATE_SCOPE = /* */ 0b0100000; // Inside a scope that applies "update" ViewTransitions if anything mutates here.
|
||||||
|
const APPEARING_SCOPE = /* */ 0b1000000; // Below Suspense content subtree which might appear in an "enter" animation or "shared" animation.
|
||||||
|
|
||||||
// Everything not listed here are tracked for the whole subtree as opposed to just
|
// Everything not listed here are tracked for the whole subtree as opposed to just
|
||||||
// until the next Instance.
|
// until the next Instance.
|
||||||
|
|
@ -987,11 +988,20 @@ export function getSuspenseContentFormatContext(
|
||||||
resumableState: ResumableState,
|
resumableState: ResumableState,
|
||||||
parentContext: FormatContext,
|
parentContext: FormatContext,
|
||||||
): FormatContext {
|
): FormatContext {
|
||||||
|
const viewTransition = getSuspenseViewTransition(
|
||||||
|
parentContext.viewTransition,
|
||||||
|
);
|
||||||
|
let subtreeScope = parentContext.tagScope | ENTER_SCOPE;
|
||||||
|
if (viewTransition !== null && viewTransition.share !== 'none') {
|
||||||
|
// If we have a ViewTransition wrapping Suspense then the appearing animation
|
||||||
|
// will be applied just like an "enter" below. Mark it as animating.
|
||||||
|
subtreeScope |= APPEARING_SCOPE;
|
||||||
|
}
|
||||||
return createFormatContext(
|
return createFormatContext(
|
||||||
parentContext.insertionMode,
|
parentContext.insertionMode,
|
||||||
parentContext.selectedValue,
|
parentContext.selectedValue,
|
||||||
parentContext.tagScope | ENTER_SCOPE,
|
subtreeScope,
|
||||||
getSuspenseViewTransition(parentContext.viewTransition),
|
viewTransition,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1063,6 +1073,9 @@ export function getViewTransitionFormatContext(
|
||||||
} else {
|
} else {
|
||||||
subtreeScope &= ~UPDATE_SCOPE;
|
subtreeScope &= ~UPDATE_SCOPE;
|
||||||
}
|
}
|
||||||
|
if (enter !== 'none') {
|
||||||
|
subtreeScope |= APPEARING_SCOPE;
|
||||||
|
}
|
||||||
return createFormatContext(
|
return createFormatContext(
|
||||||
parentContext.insertionMode,
|
parentContext.insertionMode,
|
||||||
parentContext.selectedValue,
|
parentContext.selectedValue,
|
||||||
|
|
@ -3289,6 +3302,7 @@ function pushImg(
|
||||||
props: Object,
|
props: Object,
|
||||||
resumableState: ResumableState,
|
resumableState: ResumableState,
|
||||||
renderState: RenderState,
|
renderState: RenderState,
|
||||||
|
hoistableState: null | HoistableState,
|
||||||
formatContext: FormatContext,
|
formatContext: FormatContext,
|
||||||
): null {
|
): null {
|
||||||
const pictureOrNoScriptTagInScope =
|
const pictureOrNoScriptTagInScope =
|
||||||
|
|
@ -3321,6 +3335,19 @@ function pushImg(
|
||||||
) {
|
) {
|
||||||
// We have a suspensey image and ought to preload it to optimize the loading of display blocking
|
// We have a suspensey image and ought to preload it to optimize the loading of display blocking
|
||||||
// resumableState.
|
// resumableState.
|
||||||
|
|
||||||
|
if (hoistableState !== null) {
|
||||||
|
// Mark this boundary's state as having suspensey images.
|
||||||
|
// Only do that if we have a ViewTransition that might trigger a parent Suspense boundary
|
||||||
|
// to animate its appearing. Since that's the only case we'd actually apply suspensey images
|
||||||
|
// for SSR reveals.
|
||||||
|
const isInSuspenseWithEnterViewTransition =
|
||||||
|
formatContext.tagScope & APPEARING_SCOPE;
|
||||||
|
if (isInSuspenseWithEnterViewTransition) {
|
||||||
|
hoistableState.suspenseyImages = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sizes = typeof props.sizes === 'string' ? props.sizes : undefined;
|
const sizes = typeof props.sizes === 'string' ? props.sizes : undefined;
|
||||||
const key = getImageResourceKey(src, srcSet, sizes);
|
const key = getImageResourceKey(src, srcSet, sizes);
|
||||||
|
|
||||||
|
|
@ -4255,7 +4282,14 @@ export function pushStartInstance(
|
||||||
return pushStartPreformattedElement(target, props, type, formatContext);
|
return pushStartPreformattedElement(target, props, type, formatContext);
|
||||||
}
|
}
|
||||||
case 'img': {
|
case 'img': {
|
||||||
return pushImg(target, props, resumableState, renderState, formatContext);
|
return pushImg(
|
||||||
|
target,
|
||||||
|
props,
|
||||||
|
resumableState,
|
||||||
|
renderState,
|
||||||
|
hoistableState,
|
||||||
|
formatContext,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Omitted close tags
|
// Omitted close tags
|
||||||
case 'base':
|
case 'base':
|
||||||
|
|
@ -6125,6 +6159,7 @@ type StylesheetResource = {
|
||||||
export type HoistableState = {
|
export type HoistableState = {
|
||||||
styles: Set<StyleQueue>,
|
styles: Set<StyleQueue>,
|
||||||
stylesheets: Set<StylesheetResource>,
|
stylesheets: Set<StylesheetResource>,
|
||||||
|
suspenseyImages: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StyleQueue = {
|
export type StyleQueue = {
|
||||||
|
|
@ -6138,6 +6173,7 @@ export function createHoistableState(): HoistableState {
|
||||||
return {
|
return {
|
||||||
styles: new Set(),
|
styles: new Set(),
|
||||||
stylesheets: new Set(),
|
stylesheets: new Set(),
|
||||||
|
suspenseyImages: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6995,6 +7031,18 @@ export function hoistHoistables(
|
||||||
): void {
|
): void {
|
||||||
childState.styles.forEach(hoistStyleQueueDependency, parentState);
|
childState.styles.forEach(hoistStyleQueueDependency, parentState);
|
||||||
childState.stylesheets.forEach(hoistStylesheetDependency, parentState);
|
childState.stylesheets.forEach(hoistStylesheetDependency, parentState);
|
||||||
|
if (childState.suspenseyImages) {
|
||||||
|
// If the child has suspensey images, the parent now does too if it's inlined.
|
||||||
|
// Similarly, if a SuspenseList row has a suspensey image then effectively
|
||||||
|
// the next row should be blocked on it as well since the next row can't show
|
||||||
|
// earlier. In practice, since the child will be outlined this transferring
|
||||||
|
// may never matter but is conceptually correct.
|
||||||
|
parentState.suspenseyImages = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasSuspenseyContent(hoistableState: HoistableState): boolean {
|
||||||
|
return hoistableState.stylesheets.size > 0 || hoistableState.suspenseyImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is called at various times depending on whether we are rendering
|
// This function is called at various times depending on whether we are rendering
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
import type {
|
import type {
|
||||||
RenderState as BaseRenderState,
|
RenderState as BaseRenderState,
|
||||||
ResumableState,
|
ResumableState,
|
||||||
|
HoistableState,
|
||||||
StyleQueue,
|
StyleQueue,
|
||||||
Resource,
|
Resource,
|
||||||
HeadersDescriptor,
|
HeadersDescriptor,
|
||||||
|
|
@ -325,5 +326,10 @@ export function writePreambleStart(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasSuspenseyContent(hoistableState: HoistableState): boolean {
|
||||||
|
// Never outline.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export type TransitionStatus = FormStatus;
|
export type TransitionStatus = FormStatus;
|
||||||
export const NotPendingTransition: TransitionStatus = NotPending;
|
export const NotPendingTransition: TransitionStatus = NotPending;
|
||||||
|
|
|
||||||
|
|
@ -242,5 +242,10 @@ export function writeCompletedRoot(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasSuspenseyContent(hoistableState: HoistableState): boolean {
|
||||||
|
// Never outline.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export type TransitionStatus = FormStatus;
|
export type TransitionStatus = FormStatus;
|
||||||
export const NotPendingTransition: TransitionStatus = NotPending;
|
export const NotPendingTransition: TransitionStatus = NotPending;
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,9 @@ const ReactNoopServer = ReactFizzServer({
|
||||||
writeHoistablesForBoundary() {},
|
writeHoistablesForBoundary() {},
|
||||||
writePostamble() {},
|
writePostamble() {},
|
||||||
hoistHoistables(parent: HoistableState, child: HoistableState) {},
|
hoistHoistables(parent: HoistableState, child: HoistableState) {},
|
||||||
|
hasSuspenseyContent(hoistableState: HoistableState): boolean {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
createHoistableState(): HoistableState {
|
createHoistableState(): HoistableState {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
15
packages/react-server/src/ReactFizzServer.js
vendored
15
packages/react-server/src/ReactFizzServer.js
vendored
|
|
@ -99,6 +99,7 @@ import {
|
||||||
hoistPreambleState,
|
hoistPreambleState,
|
||||||
isPreambleReady,
|
isPreambleReady,
|
||||||
isPreambleContext,
|
isPreambleContext,
|
||||||
|
hasSuspenseyContent,
|
||||||
} from './ReactFizzConfig';
|
} from './ReactFizzConfig';
|
||||||
import {
|
import {
|
||||||
constructClassInstance,
|
constructClassInstance,
|
||||||
|
|
@ -461,7 +462,7 @@ function isEligibleForOutlining(
|
||||||
// The larger this limit is, the more we can save on preparing fallbacks in case we end up
|
// The larger this limit is, the more we can save on preparing fallbacks in case we end up
|
||||||
// outlining.
|
// outlining.
|
||||||
return (
|
return (
|
||||||
boundary.byteSize > 500 &&
|
(boundary.byteSize > 500 || hasSuspenseyContent(boundary.contentState)) &&
|
||||||
// For boundaries that can possibly contribute to the preamble we don't want to outline
|
// For boundaries that can possibly contribute to the preamble we don't want to outline
|
||||||
// them regardless of their size since the fallbacks should only be emitted if we've
|
// them regardless of their size since the fallbacks should only be emitted if we've
|
||||||
// errored the boundary.
|
// errored the boundary.
|
||||||
|
|
@ -5748,8 +5749,13 @@ function flushSegment(
|
||||||
|
|
||||||
return writeEndPendingSuspenseBoundary(destination, request.renderState);
|
return writeEndPendingSuspenseBoundary(destination, request.renderState);
|
||||||
} else if (
|
} else if (
|
||||||
|
// We don't outline when we're emitting partially completed boundaries optimistically
|
||||||
|
// because it doesn't make sense to outline something if its parent is going to be
|
||||||
|
// blocked on something later in the stream anyway.
|
||||||
|
!flushingPartialBoundaries &&
|
||||||
isEligibleForOutlining(request, boundary) &&
|
isEligibleForOutlining(request, boundary) &&
|
||||||
flushedByteSize + boundary.byteSize > request.progressiveChunkSize
|
(flushedByteSize + boundary.byteSize > request.progressiveChunkSize ||
|
||||||
|
hasSuspenseyContent(boundary.contentState))
|
||||||
) {
|
) {
|
||||||
// Inlining this boundary would make the current sequence being written too large
|
// Inlining this boundary would make the current sequence being written too large
|
||||||
// and block the parent for too long. Instead, it will be emitted separately so that we
|
// and block the parent for too long. Instead, it will be emitted separately so that we
|
||||||
|
|
@ -5980,6 +5986,8 @@ function flushPartiallyCompletedSegment(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let flushingPartialBoundaries = false;
|
||||||
|
|
||||||
function flushCompletedQueues(
|
function flushCompletedQueues(
|
||||||
request: Request,
|
request: Request,
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
|
|
@ -6095,6 +6103,7 @@ function flushCompletedQueues(
|
||||||
|
|
||||||
// Next we emit any segments of any boundaries that are partially complete
|
// Next we emit any segments of any boundaries that are partially complete
|
||||||
// but not deeply complete.
|
// but not deeply complete.
|
||||||
|
flushingPartialBoundaries = true;
|
||||||
const partialBoundaries = request.partialBoundaries;
|
const partialBoundaries = request.partialBoundaries;
|
||||||
for (i = 0; i < partialBoundaries.length; i++) {
|
for (i = 0; i < partialBoundaries.length; i++) {
|
||||||
const boundary = partialBoundaries[i];
|
const boundary = partialBoundaries[i];
|
||||||
|
|
@ -6106,6 +6115,7 @@ function flushCompletedQueues(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
partialBoundaries.splice(0, i);
|
partialBoundaries.splice(0, i);
|
||||||
|
flushingPartialBoundaries = false;
|
||||||
|
|
||||||
// Next we check the completed boundaries again. This may have had
|
// Next we check the completed boundaries again. This may have had
|
||||||
// boundaries added to it in case they were too larged to be inlined.
|
// boundaries added to it in case they were too larged to be inlined.
|
||||||
|
|
@ -6123,6 +6133,7 @@ function flushCompletedQueues(
|
||||||
}
|
}
|
||||||
largeBoundaries.splice(0, i);
|
largeBoundaries.splice(0, i);
|
||||||
} finally {
|
} finally {
|
||||||
|
flushingPartialBoundaries = false;
|
||||||
if (
|
if (
|
||||||
request.allPendingTasks === 0 &&
|
request.allPendingTasks === 0 &&
|
||||||
request.clientRenderedBoundaries.length === 0 &&
|
request.clientRenderedBoundaries.length === 0 &&
|
||||||
|
|
|
||||||
|
|
@ -104,4 +104,5 @@ export const writeHoistablesForBoundary = $$$config.writeHoistablesForBoundary;
|
||||||
export const writePostamble = $$$config.writePostamble;
|
export const writePostamble = $$$config.writePostamble;
|
||||||
export const hoistHoistables = $$$config.hoistHoistables;
|
export const hoistHoistables = $$$config.hoistHoistables;
|
||||||
export const createHoistableState = $$$config.createHoistableState;
|
export const createHoistableState = $$$config.createHoistableState;
|
||||||
|
export const hasSuspenseyContent = $$$config.hasSuspenseyContent;
|
||||||
export const emitEarlyPreloads = $$$config.emitEarlyPreloads;
|
export const emitEarlyPreloads = $$$config.emitEarlyPreloads;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user