diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 042d6897c6aae1..26f1a2d0ef0760 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -206,6 +206,11 @@ const SUSPENSE_START_DATA = '$'; const SUSPENSE_END_DATA = '/$'; const SUSPENSE_PENDING_START_DATA = '$?'; const SUSPENSE_FALLBACK_START_DATA = '$!'; +const PREAMBLE_CONTRIBUTION_MARKER = 'P'; +const PREAMBLE_CONTRIBUTION_INDICATOR = '|'; +const HTML_CONTRIBUTION_INDEX = 1; +const BODY_CONTRIBUTION_INDEX = 2; +const HEAD_CONTRIBUTION_INDEX = 3; const FORM_STATE_IS_MATCHING = 'F!'; const FORM_STATE_IS_NOT_MATCHING = 'F'; @@ -986,6 +991,37 @@ export function clearSuspenseBoundary( data === SUSPENSE_FALLBACK_START_DATA ) { depth++; + } else if (data[0] === PREAMBLE_CONTRIBUTION_MARKER) { + const ownerDocument = parentInstance.ownerDocument; + + // If this comment is too short or if this slot is an empty space (the indicator) + // Then this boundary DID contribute the singleton component. It is constructed this way + // to minimize the contribution data size for common contributions such as None (no comment) + // and All (). Less common contributions such as Body only would be () + if ( + (data[HTML_CONTRIBUTION_INDEX] || PREAMBLE_CONTRIBUTION_INDICATOR) === + PREAMBLE_CONTRIBUTION_INDICATOR + ) { + const documentElement: Element = (ownerDocument.documentElement: any); + releaseSingletonInstance(documentElement); + } + if ( + (data[BODY_CONTRIBUTION_INDEX] || PREAMBLE_CONTRIBUTION_INDICATOR) === + PREAMBLE_CONTRIBUTION_INDICATOR + ) { + const body: Element = (ownerDocument.body: any); + releaseSingletonInstance(body); + } + if ( + (data[HEAD_CONTRIBUTION_INDEX] || PREAMBLE_CONTRIBUTION_INDICATOR) === + PREAMBLE_CONTRIBUTION_INDICATOR + ) { + const head: Element = (ownerDocument.head: any); + releaseSingletonInstance(head); + // We need to clear the head because this is the only singleton that can have children that + // were part of this boundary but are not inside this boundary. + clearHead(head); + } } } // $FlowFixMe[incompatible-type] we bail out when we get a null @@ -1499,7 +1535,7 @@ function clearContainerSparingly(container: Node) { case 'STYLE': { continue; } - // Stylesheet tags are retained because tehy may likely come from 3rd party scripts and extensions + // Stylesheet tags are retained because they may likely come from 3rd party scripts and extensions case 'LINK': { if (((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet') { continue; @@ -1511,6 +1547,27 @@ function clearContainerSparingly(container: Node) { return; } +function clearHead(head: Element): void { + let node = head.firstChild; + while (node) { + const nextNode = node.nextSibling; + const nodeName = node.nodeName; + if ( + isMarkedHoistable(node) || + nodeName === 'SCRIPT' || + nodeName === 'STYLE' || + (nodeName === 'LINK' && + ((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet') + ) { + // retain these nodes + } else { + head.removeChild(node); + } + node = nextNode; + } + return; +} + // Making this so we can eventually move all of the instance caching to the commit phase. // Currently this is only used to associate fiber and props to instances for hydrating // HostSingletons. The reason we need it here is we only want to make this binding on commit @@ -1872,7 +1929,20 @@ export function getFirstHydratableChild( export function getFirstHydratableChildWithinContainer( parentContainer: Container, ): null | HydratableInstance { - return getNextHydratable(parentContainer.firstChild); + let parentElement: Element; + switch (parentContainer.nodeType) { + case DOCUMENT_NODE: + parentElement = (parentContainer: any).body; + break; + default: { + if (parentContainer.nodeName === 'HTML') { + parentElement = (parentContainer: any).ownerDocument.body; + } else { + parentElement = (parentContainer: any); + } + } + } + return getNextHydratable(parentElement.firstChild); } export function getFirstHydratableChildWithinSuspenseInstance( @@ -1881,6 +1951,40 @@ export function getFirstHydratableChildWithinSuspenseInstance( return getNextHydratable(parentInstance.nextSibling); } +// If it were possible to have more than one scope singleton in a DOM tree +// we would need to model this as a stack but since you can only have one +// and head is the only singleton that is a scope in DOM we can get away with +// tracking this as a single value. +let previousHydratableOnEnteringScopedSingleton: null | HydratableInstance = + null; + +export function getFirstHydratableChildWithinSingleton( + type: string, + singletonInstance: Instance, + currentHydratableInstance: null | HydratableInstance, +): null | HydratableInstance { + if (isSingletonScope(type)) { + previousHydratableOnEnteringScopedSingleton = currentHydratableInstance; + return getNextHydratable(singletonInstance.firstChild); + } else { + return currentHydratableInstance; + } +} + +export function getNextHydratableSiblingAfterSingleton( + type: string, + currentHydratableInstance: null | HydratableInstance, +): null | HydratableInstance { + if (isSingletonScope(type)) { + const previousHydratableInstance = + previousHydratableOnEnteringScopedSingleton; + previousHydratableOnEnteringScopedSingleton = null; + return previousHydratableInstance; + } else { + return currentHydratableInstance; + } +} + export function describeHydratableInstanceForDevWarnings( instance: HydratableInstance, ): string | {type: string, props: $ReadOnly} { diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 4b8841a06e67b5..4652d68bc0a507 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -684,16 +684,25 @@ export function completeResumableState(resumableState: ResumableState): void { resumableState.bootstrapModules = undefined; } +const NoContribution /* */ = 0b000; +const HTMLContribution /* */ = 0b001; +const BodyContribution /* */ = 0b010; +const HeadContribution /* */ = 0b100; +const TotalContribution = + HTMLContribution | HeadContribution | BodyContribution; + export type PreambleState = { htmlChunks: null | Array, headChunks: null | Array, bodyChunks: null | Array, + contribution: number, }; export function createPreambleState(): PreambleState { return { htmlChunks: null, headChunks: null, bodyChunks: null, + contribution: NoContribution, }; } @@ -3227,7 +3236,7 @@ function pushStartHead( throw new Error(`The ${'``'} tag may only be rendered once.`); } preamble.headChunks = []; - return pushStartGenericElement(preamble.headChunks, props, 'head'); + return pushStartSingletonElement(preamble.headChunks, props, 'head'); } else { // This is deep and is likely just an error. we emit it inline though. // Validation should warn that this tag is the the wrong spot. @@ -3251,7 +3260,7 @@ function pushStartBody( } preamble.bodyChunks = []; - return pushStartGenericElement(preamble.bodyChunks, props, 'body'); + return pushStartSingletonElement(preamble.bodyChunks, props, 'body'); } else { // This is deep and is likely just an error. we emit it inline though. // Validation should warn that this tag is the the wrong spot. @@ -3275,7 +3284,7 @@ function pushStartHtml( } preamble.htmlChunks = [DOCTYPE]; - return pushStartGenericElement(preamble.htmlChunks, props, 'html'); + return pushStartSingletonElement(preamble.htmlChunks, props, 'html'); } else { // This is deep and is likely just an error. we emit it inline though. // Validation should warn that this tag is the the wrong spot. @@ -3416,6 +3425,43 @@ function pushScriptImpl( return null; } +// This is a fork of pushStartGenericElement because we don't ever want to do +// the children as strign optimization on that path when rendering singletons. +// When we eliminate that special path we can delete this fork and unify it again +function pushStartSingletonElement( + target: Array, + props: Object, + tag: string, +): ReactNodeList { + target.push(startChunkForTag(tag)); + + 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; + default: + pushAttribute(target, propKey, propValue); + break; + } + } + } + + target.push(endOfStartTag); + pushInnerHTML(target, innerHTML, children); + return children; +} + function pushStartGenericElement( target: Array, props: Object, @@ -3907,14 +3953,17 @@ export function hoistPreambleState( preambleState: PreambleState, ) { const rootPreamble = renderState.preamble; - if (rootPreamble.htmlChunks === null) { + if (rootPreamble.htmlChunks === null && preambleState.htmlChunks) { rootPreamble.htmlChunks = preambleState.htmlChunks; + preambleState.contribution |= HTMLContribution; } - if (rootPreamble.headChunks === null) { + if (rootPreamble.headChunks === null && preambleState.headChunks) { rootPreamble.headChunks = preambleState.headChunks; + preambleState.contribution |= HeadContribution; } - if (rootPreamble.bodyChunks === null) { + if (rootPreamble.bodyChunks === null && preambleState.bodyChunks) { rootPreamble.bodyChunks = preambleState.bodyChunks; + preambleState.contribution |= BodyContribution; } } @@ -4005,6 +4054,21 @@ const clientRenderedSuspenseBoundaryError1D = const clientRenderedSuspenseBoundaryError2 = stringToPrecomputedChunk('>'); +const boundaryPreambleContributionChunkTotal = + stringToPrecomputedChunk(''); +const boundaryPreambleContributionChunkHTMLOnly = + stringToPrecomputedChunk(''); +const boundaryPreambleContributionChunkBodyOnly = + stringToPrecomputedChunk(''); +const boundaryPreambleContributionChunkHeadOnly = + stringToPrecomputedChunk(''); +const boundaryPreambleContributionChunkHTMLAndBody = + stringToPrecomputedChunk(''); +const boundaryPreambleContributionChunkHTMLAndHead = + stringToPrecomputedChunk(''); +const boundaryPreambleContributionChunkHeadAndBody = + stringToPrecomputedChunk(''); + export function writeStartCompletedSuspenseBoundary( destination: Destination, renderState: RenderState, @@ -4091,7 +4155,11 @@ export function writeStartClientRenderedSuspenseBoundary( export function writeEndCompletedSuspenseBoundary( destination: Destination, renderState: RenderState, + preambleState: null | PreambleState, ): boolean { + if (preambleState) { + writePreambleContribution(destination, preambleState); + } return writeChunkAndReturn(destination, endSuspenseBoundary); } export function writeEndPendingSuspenseBoundary( @@ -4103,9 +4171,41 @@ export function writeEndPendingSuspenseBoundary( export function writeEndClientRenderedSuspenseBoundary( destination: Destination, renderState: RenderState, + preambleState: null | PreambleState, ): boolean { + if (preambleState) { + writePreambleContribution(destination, preambleState); + } return writeChunkAndReturn(destination, endSuspenseBoundary); } +function writePreambleContribution( + destination: Destination, + preambleState: PreambleState, +) { + const contribution = preambleState.contribution; + switch (contribution) { + case TotalContribution: + writeChunk(destination, boundaryPreambleContributionChunkTotal); + break; + case HeadContribution & BodyContribution: + writeChunk(destination, boundaryPreambleContributionChunkHeadAndBody); + break; + case HTMLContribution & HeadContribution: + writeChunk(destination, boundaryPreambleContributionChunkHTMLAndHead); + break; + case HTMLContribution & BodyContribution: + writeChunk(destination, boundaryPreambleContributionChunkHTMLAndBody); + break; + case HeadContribution: + writeChunk(destination, boundaryPreambleContributionChunkHeadOnly); + break; + case BodyContribution: + writeChunk(destination, boundaryPreambleContributionChunkBodyOnly); + break; + case HTMLContribution: + writeChunk(destination, boundaryPreambleContributionChunkHTMLOnly); + } +} const startSegmentHTML = stringToPrecomputedChunk('