@samitouri / QOS-React / commits / a00ca6f6b5

[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.