Repository: facebook/react
Pull request: [Fizz] If we haven't painted yet, wait to reveal everything until next paint

Before the first rAF, we don't know if there has been other paints before this and if so when. (We could get from performance observer.) We can assume that it's not earlier than 0 so we used delay up until the throttle time starting from zero but if the first paint is about to happen that can be very soon after.

Instead, this reveals it during the next paint which should let us be able to get into the first paint. If we can trust `rel="expect"` to have done its thing we should schedule our raf before first paint but ofc browsers can cheat and paint earlier if they want to.

If we're wrong, this is at least more batched than doing it synchronously. However it will mean that things might get more flashy than it should be if it would've been throttled. An alternative would be to always throttle first reveal.


Changed files:
filename: fixtures/ssr/src/components/LargeContent.js
patch: @@ -1,8 +1,4 @@
-import React, {
-  Fragment,
-  Suspense,
-  unstable_SuspenseList as SuspenseList,
-} from 'react';
+import React, {Suspense, unstable_SuspenseList as SuspenseList} from 'react';
 
 export default function LargeContent() {
   return (

filename: packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
patch: @@ -6,7 +6,7 @@ export const markShellTime =
 export const clientRenderBoundary =
   '$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())};';
 export const completeBoundary =
-  '$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];null!==e.parentNode&&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&&requestAnimationFrame(g._reactRetry)}}b.length=0};\n$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)};';
+  '$RB=[];$RV=function(a){$RT=performance.now();for(var b=0;b<a.length;b+=2){var c=a[b],e=a[b+1];null!==e.parentNode&&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&&requestAnimationFrame(g._reactRetry)}}a.length=0};\n$RC=function(a,b){if(b=document.getElementById(b))(a=document.getElementById(a))?(a.previousSibling.data="$~",$RB.push(a,b),2===$RB.length&&("number"!==typeof $RT?requestAnimationFrame($RV.bind(null,$RB)):(a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:$RT+300-a)))):b.parentNode.removeChild(b)};';
 export const completeBoundaryUpgradeToViewTransitions =
   '$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);';
 export const completeBoundaryWithStyles =

filename: packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
patch: @@ -420,21 +420,24 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
   if (window['$RB'].length === 2) {
     // This is the first time we've pushed to the batch. We need to schedule a callback
     // to flush the batch. This is delayed by the throttle heuristic.
-    const globalMostRecentFallbackTime =
-      typeof window['$RT'] !== 'number' ? 0 : window['$RT'];
-    const currentTime = performance.now();
-    const msUntilTimeout =
-      // If the throttle would make us miss the target metric, then shorten the throttle.
-      // performance.now()'s zero value is assumed to be the start time of the metric.
-      currentTime < TARGET_VANITY_METRIC &&
-      currentTime > TARGET_VANITY_METRIC - FALLBACK_THROTTLE_MS
-        ? TARGET_VANITY_METRIC - currentTime
-        : // Otherwise it's throttled starting from last commit time.
-          globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - currentTime;
-    // We always schedule the flush in a timer even if it's very low or negative to allow
-    // for multiple completeBoundary calls that are already queued to have a chance to
-    // make the batch.
-    setTimeout(window['$RV'].bind(null, window['$RB']), msUntilTimeout);
+    if (typeof window['$RT'] !== 'number') {
+      // If we haven't had our rAF callback yet, schedule everything for the first paint.
+      requestAnimationFrame(window['$RV'].bind(null, window['$RB']));
+    } else {
+      const currentTime = performance.now();
+      const msUntilTimeout =
+        // If the throttle would make us miss the target metric, then shorten the throttle.
+        // performance.now()'s zero value is assumed to be the start time of the metric.
+        currentTime < TARGET_VANITY_METRIC &&
+        currentTime > TARGET_VANITY_METRIC - FALLBACK_THROTTLE_MS
+          ? TARGET_VANITY_METRIC - currentTime
+          : // Otherwise it's throttled starting from last commit time.
+            window['$RT'] + FALLBACK_THROTTLE_MS - currentTime;
+      // We always schedule the flush in a timer even if it's very low or negative to allow
+      // for multiple completeBoundary calls that are already queued to have a chance to
+      // make the batch.
+      setTimeout(window['$RV'].bind(null, window['$RB']), msUntilTimeout);
+    }
   }
 }
 

filename: packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
patch: @@ -40,6 +40,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
 
     // We need the mocked version of setTimeout inside the document.
     window.setTimeout = setTimeout;
+    window.requestAnimationFrame = setTimeout;
 
     patchMessageChannel();
     serverAct = require('internal-test-utils').serverAct;