[Fizz] Delay detachment of completed boundaries until reveal (#33511)
Sebastian "Sebbie" Silbermann committed
Jun 11, 2025 at 21:24 UTC
a00ca6f6b51e46a0ccec54a2231bfe7a1ed9ae1d
5 files changed
+50
-4
fixtures/view-transition/src/components/App.js
+1
@@ -4,6 +4,7 @@ import React, {
4
useEffect,
5
useState,
6
unstable_addTransitionType as addTransitionType,
7
+ use,
8
} from 'react';
9
10
import Chrome from './Chrome';
fixtures/view-transition/src/components/NestedReveal.js
new
+36
@@ -0,0 +1,36 @@
1
+import React, {Suspense, use} from 'react';
2
+
3
+async function sleep(ms) {
4
+ return new Promise(resolve => setTimeout(resolve, ms));
5
+}
6
+
7
+function Use({useable}) {
8
+ use(useable);
9
+ return null;
10
+}
11
+
12
+let delay1;
13
+let delay2;
14
+
15
+export default function NestedReveal({}) {
16
+ if (!delay1) {
17
+ delay1 = sleep(100);
18
+ // Needs to happen before the throttled reveal of delay 1
19
+ delay2 = sleep(200);
20
+ }
21
+
22
+ return (
23
+ <div className="swipe-recognizer">
24
+ Shell
25
+ <Suspense fallback="Loading level 1">
26
+ <div>Level 1</div>
27
+ <Use useable={delay1} />
28
+
29
+ <Suspense fallback="Loading level 2">
30
+ <div>Level 2</div>
31
+ <Use useable={delay2} />
32
+ </Suspense>
33
+ </Suspense>
34
+ </div>
35
+ );
36
+}
fixtures/view-transition/src/components/Page.js
+2
@@ -18,6 +18,7 @@ import SwipeRecognizer from './SwipeRecognizer';
18
import './Page.css';
19
20
import transitions from './Transitions.module.css';
21
+import NestedReveal from './NestedReveal';
22
23
async function sleep(ms) {
24
return new Promise(resolve => setTimeout(resolve, ms));
@@ -241,6 +242,7 @@ export default function Page({url, navigate}) {
242
</div>
243
</ViewTransition>
244
</SwipeRecognizer>
245
+ <NestedReveal />
246
</div>
247
);
248
}
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+1
-1
@@ -6,7 +6,7 @@ export const markShellTime =
6
export const clientRenderBoundary =
7
'$RX=function(b,c,d,e,f){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),f&&(a.cstck=f),b._reactRetry&&b._reactRetry())};';
8
export const completeBoundary =
9
- '$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],h=b[a+1],e=c.parentNode;if(e){var f=c.previousSibling,g=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===g)break;else g--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||g++}d=c.nextSibling;e.removeChild(c);c=d}while(c);for(;h.firstChild;)e.insertBefore(h.firstChild,c);f.data="$";f._reactRetry&&f._reactRetry()}}b.length=0};$RC=function(b,a){if(a=document.getElementById(a))if(a.parentNode.removeChild(a),b=document.getElementById(b))b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))};';
9
+ '$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&g._reactRetry()}}b.length=0};$RC=function(b,a){if(a=document.getElementById(a))(b=document.getElementById(b))?(b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))):a.parentNode.removeChild(a)};';
10
export const completeBoundaryUpgradeToViewTransitions =
11
'$RV=function(A,g){function k(a,b){var e=a.getAttribute(b);e&&(b=a.style,l.push(a,b.viewTransitionName,b.viewTransitionClass),"auto"!==e&&(b.viewTransitionClass=e),(a=a.getAttribute("vt-name"))||(a="_T_"+K++ +"_"),b.viewTransitionName=a,B=!0)}var B=!1,K=0,l=[];try{var f=document.__reactViewTransition;if(f){f.finished.finally($RV.bind(null,g));return}var m=new Map;for(f=1;f<g.length;f+=2)for(var h=g[f].querySelectorAll("[vt-share]"),d=0;d<h.length;d++){var c=h[d];m.set(c.getAttribute("vt-name"),c)}var u=[];for(h=0;h<g.length;h+=2){var C=g[h],x=C.parentNode;if(x){var v=x.getBoundingClientRect();if(v.left||v.top||v.width||v.height){c=C;for(f=0;c;){if(8===c.nodeType){var r=c.data;if("/$"===r)if(0===f)break;else f--;else"$"!==r&&"$?"!==r&&"$~"!==r&&"$!"!==r||f++}else if(1===c.nodeType){d=c;var D=d.getAttribute("vt-name"),y=m.get(D);k(d,y?"vt-share":"vt-exit");y&&(k(y,"vt-share"),m.set(D,null));var E=d.querySelectorAll("[vt-share]");for(d=0;d<E.length;d++){var F=E[d],G=F.getAttribute("vt-name"),\nH=m.get(G);H&&(k(F,"vt-share"),k(H,"vt-share"),m.set(G,null))}}c=c.nextSibling}for(var I=g[h+1],t=I.firstElementChild;t;)null!==m.get(t.getAttribute("vt-name"))&&k(t,"vt-enter"),t=t.nextElementSibling;c=x;do for(var n=c.firstElementChild;n;){var J=n.getAttribute("vt-update");J&&"none"!==J&&!l.includes(n)&&k(n,"vt-update");n=n.nextElementSibling}while((c=c.parentNode)&&1===c.nodeType&&"none"!==c.getAttribute("vt-update"));u.push.apply(u,I.querySelectorAll(\'img[src]:not([loading="lazy"])\'))}}}if(B){var z=\ndocument.__reactViewTransition=document.startViewTransition({update:function(){A(g);for(var a=[document.documentElement.clientHeight,document.fonts.ready],b={},e=0;e<u.length;b={g:b.g},e++)if(b.g=u[e],!b.g.complete){var p=b.g.getBoundingClientRect();0<p.bottom&&0<p.right&&p.top<window.innerHeight&&p.left<window.innerWidth&&(p=new Promise(function(w){return function(q){w.g.addEventListener("load",q);w.g.addEventListener("error",q)}}(b)),a.push(p))}return Promise.race([Promise.all(a),new Promise(function(w){var q=\nperformance.now();setTimeout(w,2300>q&&2E3<q?2300-q:500)})])},types:[]});z.ready.finally(function(){for(var a=l.length-3;0<=a;a-=3){var b=l[a],e=b.style;e.viewTransitionName=l[a+1];e.viewTransitionClass=l[a+1];""===b.getAttribute("style")&&b.removeAttribute("style")}});z.finished.finally(function(){document.__reactViewTransition===z&&(document.__reactViewTransition=null)});$RB=[];return}}catch(a){}A(g)}.bind(null,$RV);';
12
export const completeBoundaryWithStyles =
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
+10
-3
@@ -34,6 +34,10 @@ export function revealCompletedBoundaries(batch) {
34
for (let i = 0; i < batch.length; i += 2) {
35
const suspenseIdNode = batch[i];
36
const contentNode = batch[i + 1];
37
+ // We can detach the content now.
38
+ // Completions of boundaries within this contentNode will now find the boundary
39
+ // in its designated place.
40
+ contentNode.parentNode.removeChild(contentNode);
41
42
// Clear all the existing children. This is complicated because
43
// there can be embedded Suspense boundaries in the fallback.
@@ -385,13 +389,16 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
389
// the segment. Regardless we can ignore this case.
390
return;
391
}
388
- // We'll detach the content node so that regardless of what happens next we don't leave in the tree.
389
- // This might also help by not causing recalcing each time we move a child from here to the target.
390
- contentNodeOuter.parentNode.removeChild(contentNodeOuter);
392
393
// Find the fallback's first element.
394
const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);
395
if (!suspenseIdNodeOuter) {
396
+ // We'll never reveal this boundary so we can remove its content immediately.
397
+ // Otherwise we'll leave it in until we reveal it.
398
+ // This is important in case this specific boundary contains other boundaries
399
+ // that may get completed before we reveal this one.
400
+ contentNodeOuter.parentNode.removeChild(contentNodeOuter);
401
+
402
// The user must have already navigated away from this tree.
403
// E.g. because the parent was hydrated. That's fine there's nothing to do
404
// but we have to make sure that we already deleted the container node.