[Fiber] SuspenseList with "hidden" tail row should "catch" suspense - #35042

Merged
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden
Nov 5, 2025
Merged

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense#35042
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden

Conversation

@sebmarkbage

@sebmarkbagesebmarkbage commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

Normally if you suspend in a SuspenseList row above a Suspense boundary in that row, it'll suspend the parent. Which can itself delay the commit or resuspend a parent boundary. That's because SuspenseList mostly just coordinates the state of the inner boundaries and isn't a boundary itself.

However, for tail "hidden" and "collapsed" this is not quite the case because the rows themselves can avoid being rendered.

In the case of "collapsed" we require at least one Suspense boundary above to have successfully rendered before committing the list because the idea of this mode is that you should at least always show some indicator that things are still loading. Since we'd never try the next one after that at all, this just works. Expect there was an unrelated bug that meant that "suspend with delay" on a Retry didn't suspend the commit. This caused a scenario were it'd allow a commit proceed when it shouldn't. So I fixed that too. The counter intuitive thing here is that we won't actually show a previous completed row if the loading state of the next row is still loading.

For tail "hidden" it's a little different because we don't actually require any loading indicator at all to be shown while it's loading. If we attempt a row and it suspends, we can just hide it (and the rest) and move to commit. Therefore this implements a path where if all the rest of the tail are new mounts (we wouldn't be required to unmount any existing boundaries) then we can treat the SuspenseList boundary itself as "catching" the suspense. This is more coherent semantics since any future row that we didn't attempt also wouldn't resuspend the parent.

This allows simple cases like <SuspenseList>{list}</SuspenseList> to stream in each row without any indicator and no need for Suspense boundaries.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Nov 4, 2025
@react-sizebot

Copy link
Copy Markdown

Comparing: d000261...7d82cbc

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB+0.05%1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB+0.05%1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
facebook-www/ReactDOM-prod.classic.js+0.10%690.02 kB690.71 kB+0.06%121.46 kB121.53 kB
facebook-www/ReactDOM-prod.modern.js+0.10%680.45 kB681.13 kB+0.06%119.84 kB119.92 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.41 kB327.06 kB+0.34%56.67 kB56.86 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.49 kB327.13 kB+0.34%56.69 kB56.88 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.66 kB327.31 kB+0.34%56.73 kB56.92 kB
oss-stable-semver/react-art/cjs/react-art.production.js+0.50%330.13 kB331.77 kB+0.34%55.96 kB56.15 kB
oss-stable/react-art/cjs/react-art.production.js+0.50%330.20 kB331.85 kB+0.34%55.98 kB56.17 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js+0.47%345.38 kB347.01 kB+0.36%60.06 kB60.28 kB
oss-experimental/react-art/cjs/react-art.production.js+0.46%354.88 kB356.51 kB+0.33%59.92 kB60.12 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-profiling.js+0.45%370.17 kB371.84 kB+0.31%63.43 kB63.62 kB
facebook-www/ReactART-prod.modern.js+0.43%380.04 kB381.67 kB+0.31%63.75 kB63.94 kB
facebook-www/ReactART-prod.classic.js+0.42%389.88 kB391.51 kB+0.34%65.36 kB65.59 kB
react-native/implementations/ReactFabric-prod.js+0.41%377.57 kB379.14 kB+0.23%65.58 kB65.73 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.js+0.39%447.54 kB449.30 kB+0.30%71.59 kB71.80 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.js+0.39%447.57 kB449.32 kB+0.30%71.61 kB71.83 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js+0.36%485.61 kB487.35 kB+0.31%77.27 kB77.51 kB
facebook-www/ReactReconciler-prod.modern.js+0.34%503.84 kB505.58 kB+0.26%80.14 kB80.34 kB
facebook-www/ReactReconciler-prod.classic.js+0.34%514.01 kB515.74 kB+0.26%81.73 kB81.94 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.22 kB519.98 kB+0.28%81.09 kB81.31 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.25 kB520.00 kB+0.28%81.12 kB81.34 kB
facebook-www/ReactTestRenderer-dev.modern.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
facebook-www/ReactTestRenderer-dev.classic.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js+0.31%561.01 kB562.75 kB+0.27%87.36 kB87.59 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js+0.31%616.01 kB617.89 kB+0.23%98.63 kB98.86 kB
react-native/implementations/ReactFabric-profiling.js+0.29%451.73 kB453.06 kB+0.17%76.16 kB76.29 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.02 kB666.90 kB+0.21%105.14 kB105.36 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.04 kB666.92 kB+0.21%105.14 kB105.36 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.09 kB666.97 kB+0.21%105.16 kB105.39 kB
oss-stable-semver/react-dom/cjs/react-dom-client.production.js+0.27%606.10 kB607.76 kB+0.21%107.36 kB107.59 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-stable-semver/react-art/cjs/react-art.development.js+0.27%696.69 kB698.54 kB+0.20%109.67 kB109.88 kB
oss-stable/react-art/cjs/react-art.development.js+0.27%696.76 kB698.61 kB+0.20%109.69 kB109.91 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js+0.26%800.23 kB802.32 kB+0.19%125.30 kB125.53 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js+0.26%800.26 kB802.35 kB+0.19%125.32 kB125.56 kB
oss-experimental/react-art/cjs/react-art.development.js+0.25%727.52 kB729.37 kB+0.17%114.33 kB114.53 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js+0.25%854.64 kB856.74 kB+0.18%133.11 kB133.35 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.23 kB681.89 kB+0.18%118.08 kB118.30 kB
oss-stable/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.35 kB682.01 kB+0.18%118.12 kB118.33 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js+0.24%679.61 kB681.25 kB+0.19%120.81 kB121.04 kB
facebook-www/ReactART-dev.modern.js+0.24%771.86 kB773.71 kB+0.16%120.20 kB120.40 kB
facebook-www/ReactReconciler-dev.modern.js+0.24%879.24 kB881.33 kB+0.18%135.93 kB136.18 kB
facebook-www/ReactART-dev.classic.js+0.24%781.39 kB783.24 kB+0.16%121.93 kB122.13 kB
facebook-www/ReactReconciler-dev.classic.js+0.24%888.48 kB890.57 kB+0.17%137.70 kB137.94 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js+0.22%743.73 kB745.37 kB+0.17%128.55 kB128.77 kB

Generated by 🚫 dangerJS against 7d82cbc

}
case RootSuspendedWithDelay: {
if (!includesOnlyTransitions(lanes)) {
if (!includesOnlyTransitions(lanes) && !includesOnlyRetries(lanes)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case that fixes "collapsed" mode. Because otherwise when we render a case that would lead to an undesirable state (a previous row unsuspends which now suspends the next row) to actually commit today.

@sebmarkbage
sebmarkbage merged commit 986323f into react:mainNov 5, 2025
246 checks passed
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@sebmarkbage@react-sizebot@acdlite
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense - #35042

Merged
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden
Nov 5, 2025
Merged

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense#35042
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden

Conversation

@sebmarkbage

@sebmarkbagesebmarkbage commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

Normally if you suspend in a SuspenseList row above a Suspense boundary in that row, it'll suspend the parent. Which can itself delay the commit or resuspend a parent boundary. That's because SuspenseList mostly just coordinates the state of the inner boundaries and isn't a boundary itself.

However, for tail "hidden" and "collapsed" this is not quite the case because the rows themselves can avoid being rendered.

In the case of "collapsed" we require at least one Suspense boundary above to have successfully rendered before committing the list because the idea of this mode is that you should at least always show some indicator that things are still loading. Since we'd never try the next one after that at all, this just works. Expect there was an unrelated bug that meant that "suspend with delay" on a Retry didn't suspend the commit. This caused a scenario were it'd allow a commit proceed when it shouldn't. So I fixed that too. The counter intuitive thing here is that we won't actually show a previous completed row if the loading state of the next row is still loading.

For tail "hidden" it's a little different because we don't actually require any loading indicator at all to be shown while it's loading. If we attempt a row and it suspends, we can just hide it (and the rest) and move to commit. Therefore this implements a path where if all the rest of the tail are new mounts (we wouldn't be required to unmount any existing boundaries) then we can treat the SuspenseList boundary itself as "catching" the suspense. This is more coherent semantics since any future row that we didn't attempt also wouldn't resuspend the parent.

This allows simple cases like <SuspenseList>{list}</SuspenseList> to stream in each row without any indicator and no need for Suspense boundaries.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Nov 4, 2025
@react-sizebot

Copy link
Copy Markdown

Comparing: d000261...7d82cbc

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB+0.05%1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB+0.05%1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
facebook-www/ReactDOM-prod.classic.js+0.10%690.02 kB690.71 kB+0.06%121.46 kB121.53 kB
facebook-www/ReactDOM-prod.modern.js+0.10%680.45 kB681.13 kB+0.06%119.84 kB119.92 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.41 kB327.06 kB+0.34%56.67 kB56.86 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.49 kB327.13 kB+0.34%56.69 kB56.88 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.66 kB327.31 kB+0.34%56.73 kB56.92 kB
oss-stable-semver/react-art/cjs/react-art.production.js+0.50%330.13 kB331.77 kB+0.34%55.96 kB56.15 kB
oss-stable/react-art/cjs/react-art.production.js+0.50%330.20 kB331.85 kB+0.34%55.98 kB56.17 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js+0.47%345.38 kB347.01 kB+0.36%60.06 kB60.28 kB
oss-experimental/react-art/cjs/react-art.production.js+0.46%354.88 kB356.51 kB+0.33%59.92 kB60.12 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-profiling.js+0.45%370.17 kB371.84 kB+0.31%63.43 kB63.62 kB
facebook-www/ReactART-prod.modern.js+0.43%380.04 kB381.67 kB+0.31%63.75 kB63.94 kB
facebook-www/ReactART-prod.classic.js+0.42%389.88 kB391.51 kB+0.34%65.36 kB65.59 kB
react-native/implementations/ReactFabric-prod.js+0.41%377.57 kB379.14 kB+0.23%65.58 kB65.73 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.js+0.39%447.54 kB449.30 kB+0.30%71.59 kB71.80 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.js+0.39%447.57 kB449.32 kB+0.30%71.61 kB71.83 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js+0.36%485.61 kB487.35 kB+0.31%77.27 kB77.51 kB
facebook-www/ReactReconciler-prod.modern.js+0.34%503.84 kB505.58 kB+0.26%80.14 kB80.34 kB
facebook-www/ReactReconciler-prod.classic.js+0.34%514.01 kB515.74 kB+0.26%81.73 kB81.94 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.22 kB519.98 kB+0.28%81.09 kB81.31 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.25 kB520.00 kB+0.28%81.12 kB81.34 kB
facebook-www/ReactTestRenderer-dev.modern.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
facebook-www/ReactTestRenderer-dev.classic.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js+0.31%561.01 kB562.75 kB+0.27%87.36 kB87.59 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js+0.31%616.01 kB617.89 kB+0.23%98.63 kB98.86 kB
react-native/implementations/ReactFabric-profiling.js+0.29%451.73 kB453.06 kB+0.17%76.16 kB76.29 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.02 kB666.90 kB+0.21%105.14 kB105.36 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.04 kB666.92 kB+0.21%105.14 kB105.36 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.09 kB666.97 kB+0.21%105.16 kB105.39 kB
oss-stable-semver/react-dom/cjs/react-dom-client.production.js+0.27%606.10 kB607.76 kB+0.21%107.36 kB107.59 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-stable-semver/react-art/cjs/react-art.development.js+0.27%696.69 kB698.54 kB+0.20%109.67 kB109.88 kB
oss-stable/react-art/cjs/react-art.development.js+0.27%696.76 kB698.61 kB+0.20%109.69 kB109.91 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js+0.26%800.23 kB802.32 kB+0.19%125.30 kB125.53 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js+0.26%800.26 kB802.35 kB+0.19%125.32 kB125.56 kB
oss-experimental/react-art/cjs/react-art.development.js+0.25%727.52 kB729.37 kB+0.17%114.33 kB114.53 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js+0.25%854.64 kB856.74 kB+0.18%133.11 kB133.35 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.23 kB681.89 kB+0.18%118.08 kB118.30 kB
oss-stable/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.35 kB682.01 kB+0.18%118.12 kB118.33 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js+0.24%679.61 kB681.25 kB+0.19%120.81 kB121.04 kB
facebook-www/ReactART-dev.modern.js+0.24%771.86 kB773.71 kB+0.16%120.20 kB120.40 kB
facebook-www/ReactReconciler-dev.modern.js+0.24%879.24 kB881.33 kB+0.18%135.93 kB136.18 kB
facebook-www/ReactART-dev.classic.js+0.24%781.39 kB783.24 kB+0.16%121.93 kB122.13 kB
facebook-www/ReactReconciler-dev.classic.js+0.24%888.48 kB890.57 kB+0.17%137.70 kB137.94 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js+0.22%743.73 kB745.37 kB+0.17%128.55 kB128.77 kB

Generated by 🚫 dangerJS against 7d82cbc

}
case RootSuspendedWithDelay: {
if (!includesOnlyTransitions(lanes)) {
if (!includesOnlyTransitions(lanes) && !includesOnlyRetries(lanes)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case that fixes "collapsed" mode. Because otherwise when we render a case that would lead to an undesirable state (a previous row unsuspends which now suspends the next row) to actually commit today.

@sebmarkbage
sebmarkbage merged commit 986323f into react:mainNov 5, 2025
246 checks passed
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@sebmarkbage@react-sizebot@acdlite
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense - #35042

Merged
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden
Nov 5, 2025
Merged

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense#35042
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden

Conversation

@sebmarkbage

@sebmarkbagesebmarkbage commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

Normally if you suspend in a SuspenseList row above a Suspense boundary in that row, it'll suspend the parent. Which can itself delay the commit or resuspend a parent boundary. That's because SuspenseList mostly just coordinates the state of the inner boundaries and isn't a boundary itself.

However, for tail "hidden" and "collapsed" this is not quite the case because the rows themselves can avoid being rendered.

In the case of "collapsed" we require at least one Suspense boundary above to have successfully rendered before committing the list because the idea of this mode is that you should at least always show some indicator that things are still loading. Since we'd never try the next one after that at all, this just works. Expect there was an unrelated bug that meant that "suspend with delay" on a Retry didn't suspend the commit. This caused a scenario were it'd allow a commit proceed when it shouldn't. So I fixed that too. The counter intuitive thing here is that we won't actually show a previous completed row if the loading state of the next row is still loading.

For tail "hidden" it's a little different because we don't actually require any loading indicator at all to be shown while it's loading. If we attempt a row and it suspends, we can just hide it (and the rest) and move to commit. Therefore this implements a path where if all the rest of the tail are new mounts (we wouldn't be required to unmount any existing boundaries) then we can treat the SuspenseList boundary itself as "catching" the suspense. This is more coherent semantics since any future row that we didn't attempt also wouldn't resuspend the parent.

This allows simple cases like <SuspenseList>{list}</SuspenseList> to stream in each row without any indicator and no need for Suspense boundaries.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Nov 4, 2025
@react-sizebot

Copy link
Copy Markdown

Comparing: d000261...7d82cbc

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB+0.05%1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB+0.05%1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
facebook-www/ReactDOM-prod.classic.js+0.10%690.02 kB690.71 kB+0.06%121.46 kB121.53 kB
facebook-www/ReactDOM-prod.modern.js+0.10%680.45 kB681.13 kB+0.06%119.84 kB119.92 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.41 kB327.06 kB+0.34%56.67 kB56.86 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.49 kB327.13 kB+0.34%56.69 kB56.88 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.66 kB327.31 kB+0.34%56.73 kB56.92 kB
oss-stable-semver/react-art/cjs/react-art.production.js+0.50%330.13 kB331.77 kB+0.34%55.96 kB56.15 kB
oss-stable/react-art/cjs/react-art.production.js+0.50%330.20 kB331.85 kB+0.34%55.98 kB56.17 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js+0.47%345.38 kB347.01 kB+0.36%60.06 kB60.28 kB
oss-experimental/react-art/cjs/react-art.production.js+0.46%354.88 kB356.51 kB+0.33%59.92 kB60.12 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-profiling.js+0.45%370.17 kB371.84 kB+0.31%63.43 kB63.62 kB
facebook-www/ReactART-prod.modern.js+0.43%380.04 kB381.67 kB+0.31%63.75 kB63.94 kB
facebook-www/ReactART-prod.classic.js+0.42%389.88 kB391.51 kB+0.34%65.36 kB65.59 kB
react-native/implementations/ReactFabric-prod.js+0.41%377.57 kB379.14 kB+0.23%65.58 kB65.73 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.js+0.39%447.54 kB449.30 kB+0.30%71.59 kB71.80 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.js+0.39%447.57 kB449.32 kB+0.30%71.61 kB71.83 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js+0.36%485.61 kB487.35 kB+0.31%77.27 kB77.51 kB
facebook-www/ReactReconciler-prod.modern.js+0.34%503.84 kB505.58 kB+0.26%80.14 kB80.34 kB
facebook-www/ReactReconciler-prod.classic.js+0.34%514.01 kB515.74 kB+0.26%81.73 kB81.94 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.22 kB519.98 kB+0.28%81.09 kB81.31 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.25 kB520.00 kB+0.28%81.12 kB81.34 kB
facebook-www/ReactTestRenderer-dev.modern.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
facebook-www/ReactTestRenderer-dev.classic.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js+0.31%561.01 kB562.75 kB+0.27%87.36 kB87.59 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js+0.31%616.01 kB617.89 kB+0.23%98.63 kB98.86 kB
react-native/implementations/ReactFabric-profiling.js+0.29%451.73 kB453.06 kB+0.17%76.16 kB76.29 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.02 kB666.90 kB+0.21%105.14 kB105.36 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.04 kB666.92 kB+0.21%105.14 kB105.36 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.09 kB666.97 kB+0.21%105.16 kB105.39 kB
oss-stable-semver/react-dom/cjs/react-dom-client.production.js+0.27%606.10 kB607.76 kB+0.21%107.36 kB107.59 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-stable-semver/react-art/cjs/react-art.development.js+0.27%696.69 kB698.54 kB+0.20%109.67 kB109.88 kB
oss-stable/react-art/cjs/react-art.development.js+0.27%696.76 kB698.61 kB+0.20%109.69 kB109.91 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js+0.26%800.23 kB802.32 kB+0.19%125.30 kB125.53 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js+0.26%800.26 kB802.35 kB+0.19%125.32 kB125.56 kB
oss-experimental/react-art/cjs/react-art.development.js+0.25%727.52 kB729.37 kB+0.17%114.33 kB114.53 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js+0.25%854.64 kB856.74 kB+0.18%133.11 kB133.35 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.23 kB681.89 kB+0.18%118.08 kB118.30 kB
oss-stable/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.35 kB682.01 kB+0.18%118.12 kB118.33 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js+0.24%679.61 kB681.25 kB+0.19%120.81 kB121.04 kB
facebook-www/ReactART-dev.modern.js+0.24%771.86 kB773.71 kB+0.16%120.20 kB120.40 kB
facebook-www/ReactReconciler-dev.modern.js+0.24%879.24 kB881.33 kB+0.18%135.93 kB136.18 kB
facebook-www/ReactART-dev.classic.js+0.24%781.39 kB783.24 kB+0.16%121.93 kB122.13 kB
facebook-www/ReactReconciler-dev.classic.js+0.24%888.48 kB890.57 kB+0.17%137.70 kB137.94 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js+0.22%743.73 kB745.37 kB+0.17%128.55 kB128.77 kB

Generated by 🚫 dangerJS against 7d82cbc

}
case RootSuspendedWithDelay: {
if (!includesOnlyTransitions(lanes)) {
if (!includesOnlyTransitions(lanes) && !includesOnlyRetries(lanes)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case that fixes "collapsed" mode. Because otherwise when we render a case that would lead to an undesirable state (a previous row unsuspends which now suspends the next row) to actually commit today.

@sebmarkbage
sebmarkbage merged commit 986323f into react:mainNov 5, 2025
246 checks passed
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@sebmarkbage@react-sizebot@acdlite
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense - #35042

Merged
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden
Nov 5, 2025
Merged

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense#35042
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden

Conversation

@sebmarkbage

@sebmarkbagesebmarkbage commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

Normally if you suspend in a SuspenseList row above a Suspense boundary in that row, it'll suspend the parent. Which can itself delay the commit or resuspend a parent boundary. That's because SuspenseList mostly just coordinates the state of the inner boundaries and isn't a boundary itself.

However, for tail "hidden" and "collapsed" this is not quite the case because the rows themselves can avoid being rendered.

In the case of "collapsed" we require at least one Suspense boundary above to have successfully rendered before committing the list because the idea of this mode is that you should at least always show some indicator that things are still loading. Since we'd never try the next one after that at all, this just works. Expect there was an unrelated bug that meant that "suspend with delay" on a Retry didn't suspend the commit. This caused a scenario were it'd allow a commit proceed when it shouldn't. So I fixed that too. The counter intuitive thing here is that we won't actually show a previous completed row if the loading state of the next row is still loading.

For tail "hidden" it's a little different because we don't actually require any loading indicator at all to be shown while it's loading. If we attempt a row and it suspends, we can just hide it (and the rest) and move to commit. Therefore this implements a path where if all the rest of the tail are new mounts (we wouldn't be required to unmount any existing boundaries) then we can treat the SuspenseList boundary itself as "catching" the suspense. This is more coherent semantics since any future row that we didn't attempt also wouldn't resuspend the parent.

This allows simple cases like <SuspenseList>{list}</SuspenseList> to stream in each row without any indicator and no need for Suspense boundaries.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Nov 4, 2025
@react-sizebot

Copy link
Copy Markdown

Comparing: d000261...7d82cbc

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB+0.05%1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB+0.05%1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
facebook-www/ReactDOM-prod.classic.js+0.10%690.02 kB690.71 kB+0.06%121.46 kB121.53 kB
facebook-www/ReactDOM-prod.modern.js+0.10%680.45 kB681.13 kB+0.06%119.84 kB119.92 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.41 kB327.06 kB+0.34%56.67 kB56.86 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.49 kB327.13 kB+0.34%56.69 kB56.88 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.66 kB327.31 kB+0.34%56.73 kB56.92 kB
oss-stable-semver/react-art/cjs/react-art.production.js+0.50%330.13 kB331.77 kB+0.34%55.96 kB56.15 kB
oss-stable/react-art/cjs/react-art.production.js+0.50%330.20 kB331.85 kB+0.34%55.98 kB56.17 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js+0.47%345.38 kB347.01 kB+0.36%60.06 kB60.28 kB
oss-experimental/react-art/cjs/react-art.production.js+0.46%354.88 kB356.51 kB+0.33%59.92 kB60.12 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-profiling.js+0.45%370.17 kB371.84 kB+0.31%63.43 kB63.62 kB
facebook-www/ReactART-prod.modern.js+0.43%380.04 kB381.67 kB+0.31%63.75 kB63.94 kB
facebook-www/ReactART-prod.classic.js+0.42%389.88 kB391.51 kB+0.34%65.36 kB65.59 kB
react-native/implementations/ReactFabric-prod.js+0.41%377.57 kB379.14 kB+0.23%65.58 kB65.73 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.js+0.39%447.54 kB449.30 kB+0.30%71.59 kB71.80 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.js+0.39%447.57 kB449.32 kB+0.30%71.61 kB71.83 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js+0.36%485.61 kB487.35 kB+0.31%77.27 kB77.51 kB
facebook-www/ReactReconciler-prod.modern.js+0.34%503.84 kB505.58 kB+0.26%80.14 kB80.34 kB
facebook-www/ReactReconciler-prod.classic.js+0.34%514.01 kB515.74 kB+0.26%81.73 kB81.94 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.22 kB519.98 kB+0.28%81.09 kB81.31 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.25 kB520.00 kB+0.28%81.12 kB81.34 kB
facebook-www/ReactTestRenderer-dev.modern.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
facebook-www/ReactTestRenderer-dev.classic.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js+0.31%561.01 kB562.75 kB+0.27%87.36 kB87.59 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js+0.31%616.01 kB617.89 kB+0.23%98.63 kB98.86 kB
react-native/implementations/ReactFabric-profiling.js+0.29%451.73 kB453.06 kB+0.17%76.16 kB76.29 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.02 kB666.90 kB+0.21%105.14 kB105.36 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.04 kB666.92 kB+0.21%105.14 kB105.36 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.09 kB666.97 kB+0.21%105.16 kB105.39 kB
oss-stable-semver/react-dom/cjs/react-dom-client.production.js+0.27%606.10 kB607.76 kB+0.21%107.36 kB107.59 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-stable-semver/react-art/cjs/react-art.development.js+0.27%696.69 kB698.54 kB+0.20%109.67 kB109.88 kB
oss-stable/react-art/cjs/react-art.development.js+0.27%696.76 kB698.61 kB+0.20%109.69 kB109.91 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js+0.26%800.23 kB802.32 kB+0.19%125.30 kB125.53 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js+0.26%800.26 kB802.35 kB+0.19%125.32 kB125.56 kB
oss-experimental/react-art/cjs/react-art.development.js+0.25%727.52 kB729.37 kB+0.17%114.33 kB114.53 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js+0.25%854.64 kB856.74 kB+0.18%133.11 kB133.35 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.23 kB681.89 kB+0.18%118.08 kB118.30 kB
oss-stable/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.35 kB682.01 kB+0.18%118.12 kB118.33 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js+0.24%679.61 kB681.25 kB+0.19%120.81 kB121.04 kB
facebook-www/ReactART-dev.modern.js+0.24%771.86 kB773.71 kB+0.16%120.20 kB120.40 kB
facebook-www/ReactReconciler-dev.modern.js+0.24%879.24 kB881.33 kB+0.18%135.93 kB136.18 kB
facebook-www/ReactART-dev.classic.js+0.24%781.39 kB783.24 kB+0.16%121.93 kB122.13 kB
facebook-www/ReactReconciler-dev.classic.js+0.24%888.48 kB890.57 kB+0.17%137.70 kB137.94 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js+0.22%743.73 kB745.37 kB+0.17%128.55 kB128.77 kB

Generated by 🚫 dangerJS against 7d82cbc

}
case RootSuspendedWithDelay: {
if (!includesOnlyTransitions(lanes)) {
if (!includesOnlyTransitions(lanes) && !includesOnlyRetries(lanes)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case that fixes "collapsed" mode. Because otherwise when we render a case that would lead to an undesirable state (a previous row unsuspends which now suspends the next row) to actually commit today.

@sebmarkbage
sebmarkbage merged commit 986323f into react:mainNov 5, 2025
246 checks passed
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@sebmarkbage@react-sizebot@acdlite
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense - #35042

Merged
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden
Nov 5, 2025
Merged

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense#35042
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden

Conversation

@sebmarkbage

@sebmarkbagesebmarkbage commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

Normally if you suspend in a SuspenseList row above a Suspense boundary in that row, it'll suspend the parent. Which can itself delay the commit or resuspend a parent boundary. That's because SuspenseList mostly just coordinates the state of the inner boundaries and isn't a boundary itself.

However, for tail "hidden" and "collapsed" this is not quite the case because the rows themselves can avoid being rendered.

In the case of "collapsed" we require at least one Suspense boundary above to have successfully rendered before committing the list because the idea of this mode is that you should at least always show some indicator that things are still loading. Since we'd never try the next one after that at all, this just works. Expect there was an unrelated bug that meant that "suspend with delay" on a Retry didn't suspend the commit. This caused a scenario were it'd allow a commit proceed when it shouldn't. So I fixed that too. The counter intuitive thing here is that we won't actually show a previous completed row if the loading state of the next row is still loading.

For tail "hidden" it's a little different because we don't actually require any loading indicator at all to be shown while it's loading. If we attempt a row and it suspends, we can just hide it (and the rest) and move to commit. Therefore this implements a path where if all the rest of the tail are new mounts (we wouldn't be required to unmount any existing boundaries) then we can treat the SuspenseList boundary itself as "catching" the suspense. This is more coherent semantics since any future row that we didn't attempt also wouldn't resuspend the parent.

This allows simple cases like <SuspenseList>{list}</SuspenseList> to stream in each row without any indicator and no need for Suspense boundaries.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Nov 4, 2025
@react-sizebot

Copy link
Copy Markdown

Comparing: d000261...7d82cbc

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB+0.05%1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB+0.05%1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
facebook-www/ReactDOM-prod.classic.js+0.10%690.02 kB690.71 kB+0.06%121.46 kB121.53 kB
facebook-www/ReactDOM-prod.modern.js+0.10%680.45 kB681.13 kB+0.06%119.84 kB119.92 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.41 kB327.06 kB+0.34%56.67 kB56.86 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.49 kB327.13 kB+0.34%56.69 kB56.88 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.66 kB327.31 kB+0.34%56.73 kB56.92 kB
oss-stable-semver/react-art/cjs/react-art.production.js+0.50%330.13 kB331.77 kB+0.34%55.96 kB56.15 kB
oss-stable/react-art/cjs/react-art.production.js+0.50%330.20 kB331.85 kB+0.34%55.98 kB56.17 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js+0.47%345.38 kB347.01 kB+0.36%60.06 kB60.28 kB
oss-experimental/react-art/cjs/react-art.production.js+0.46%354.88 kB356.51 kB+0.33%59.92 kB60.12 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-profiling.js+0.45%370.17 kB371.84 kB+0.31%63.43 kB63.62 kB
facebook-www/ReactART-prod.modern.js+0.43%380.04 kB381.67 kB+0.31%63.75 kB63.94 kB
facebook-www/ReactART-prod.classic.js+0.42%389.88 kB391.51 kB+0.34%65.36 kB65.59 kB
react-native/implementations/ReactFabric-prod.js+0.41%377.57 kB379.14 kB+0.23%65.58 kB65.73 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.js+0.39%447.54 kB449.30 kB+0.30%71.59 kB71.80 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.js+0.39%447.57 kB449.32 kB+0.30%71.61 kB71.83 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js+0.36%485.61 kB487.35 kB+0.31%77.27 kB77.51 kB
facebook-www/ReactReconciler-prod.modern.js+0.34%503.84 kB505.58 kB+0.26%80.14 kB80.34 kB
facebook-www/ReactReconciler-prod.classic.js+0.34%514.01 kB515.74 kB+0.26%81.73 kB81.94 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.22 kB519.98 kB+0.28%81.09 kB81.31 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.25 kB520.00 kB+0.28%81.12 kB81.34 kB
facebook-www/ReactTestRenderer-dev.modern.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
facebook-www/ReactTestRenderer-dev.classic.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js+0.31%561.01 kB562.75 kB+0.27%87.36 kB87.59 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js+0.31%616.01 kB617.89 kB+0.23%98.63 kB98.86 kB
react-native/implementations/ReactFabric-profiling.js+0.29%451.73 kB453.06 kB+0.17%76.16 kB76.29 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.02 kB666.90 kB+0.21%105.14 kB105.36 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.04 kB666.92 kB+0.21%105.14 kB105.36 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.09 kB666.97 kB+0.21%105.16 kB105.39 kB
oss-stable-semver/react-dom/cjs/react-dom-client.production.js+0.27%606.10 kB607.76 kB+0.21%107.36 kB107.59 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-stable-semver/react-art/cjs/react-art.development.js+0.27%696.69 kB698.54 kB+0.20%109.67 kB109.88 kB
oss-stable/react-art/cjs/react-art.development.js+0.27%696.76 kB698.61 kB+0.20%109.69 kB109.91 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js+0.26%800.23 kB802.32 kB+0.19%125.30 kB125.53 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js+0.26%800.26 kB802.35 kB+0.19%125.32 kB125.56 kB
oss-experimental/react-art/cjs/react-art.development.js+0.25%727.52 kB729.37 kB+0.17%114.33 kB114.53 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js+0.25%854.64 kB856.74 kB+0.18%133.11 kB133.35 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.23 kB681.89 kB+0.18%118.08 kB118.30 kB
oss-stable/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.35 kB682.01 kB+0.18%118.12 kB118.33 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js+0.24%679.61 kB681.25 kB+0.19%120.81 kB121.04 kB
facebook-www/ReactART-dev.modern.js+0.24%771.86 kB773.71 kB+0.16%120.20 kB120.40 kB
facebook-www/ReactReconciler-dev.modern.js+0.24%879.24 kB881.33 kB+0.18%135.93 kB136.18 kB
facebook-www/ReactART-dev.classic.js+0.24%781.39 kB783.24 kB+0.16%121.93 kB122.13 kB
facebook-www/ReactReconciler-dev.classic.js+0.24%888.48 kB890.57 kB+0.17%137.70 kB137.94 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js+0.22%743.73 kB745.37 kB+0.17%128.55 kB128.77 kB

Generated by 🚫 dangerJS against 7d82cbc

}
case RootSuspendedWithDelay: {
if (!includesOnlyTransitions(lanes)) {
if (!includesOnlyTransitions(lanes) && !includesOnlyRetries(lanes)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case that fixes "collapsed" mode. Because otherwise when we render a case that would lead to an undesirable state (a previous row unsuspends which now suspends the next row) to actually commit today.

@sebmarkbage
sebmarkbage merged commit 986323f into react:mainNov 5, 2025
246 checks passed
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@sebmarkbage@react-sizebot@acdlite
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense - #35042

Merged
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden
Nov 5, 2025
Merged

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense#35042
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden

Conversation

@sebmarkbage

@sebmarkbagesebmarkbage commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

Normally if you suspend in a SuspenseList row above a Suspense boundary in that row, it'll suspend the parent. Which can itself delay the commit or resuspend a parent boundary. That's because SuspenseList mostly just coordinates the state of the inner boundaries and isn't a boundary itself.

However, for tail "hidden" and "collapsed" this is not quite the case because the rows themselves can avoid being rendered.

In the case of "collapsed" we require at least one Suspense boundary above to have successfully rendered before committing the list because the idea of this mode is that you should at least always show some indicator that things are still loading. Since we'd never try the next one after that at all, this just works. Expect there was an unrelated bug that meant that "suspend with delay" on a Retry didn't suspend the commit. This caused a scenario were it'd allow a commit proceed when it shouldn't. So I fixed that too. The counter intuitive thing here is that we won't actually show a previous completed row if the loading state of the next row is still loading.

For tail "hidden" it's a little different because we don't actually require any loading indicator at all to be shown while it's loading. If we attempt a row and it suspends, we can just hide it (and the rest) and move to commit. Therefore this implements a path where if all the rest of the tail are new mounts (we wouldn't be required to unmount any existing boundaries) then we can treat the SuspenseList boundary itself as "catching" the suspense. This is more coherent semantics since any future row that we didn't attempt also wouldn't resuspend the parent.

This allows simple cases like <SuspenseList>{list}</SuspenseList> to stream in each row without any indicator and no need for Suspense boundaries.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Nov 4, 2025
@react-sizebot

Copy link
Copy Markdown

Comparing: d000261...7d82cbc

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB+0.05%1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB+0.05%1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
facebook-www/ReactDOM-prod.classic.js+0.10%690.02 kB690.71 kB+0.06%121.46 kB121.53 kB
facebook-www/ReactDOM-prod.modern.js+0.10%680.45 kB681.13 kB+0.06%119.84 kB119.92 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.41 kB327.06 kB+0.34%56.67 kB56.86 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.49 kB327.13 kB+0.34%56.69 kB56.88 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.66 kB327.31 kB+0.34%56.73 kB56.92 kB
oss-stable-semver/react-art/cjs/react-art.production.js+0.50%330.13 kB331.77 kB+0.34%55.96 kB56.15 kB
oss-stable/react-art/cjs/react-art.production.js+0.50%330.20 kB331.85 kB+0.34%55.98 kB56.17 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js+0.47%345.38 kB347.01 kB+0.36%60.06 kB60.28 kB
oss-experimental/react-art/cjs/react-art.production.js+0.46%354.88 kB356.51 kB+0.33%59.92 kB60.12 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-profiling.js+0.45%370.17 kB371.84 kB+0.31%63.43 kB63.62 kB
facebook-www/ReactART-prod.modern.js+0.43%380.04 kB381.67 kB+0.31%63.75 kB63.94 kB
facebook-www/ReactART-prod.classic.js+0.42%389.88 kB391.51 kB+0.34%65.36 kB65.59 kB
react-native/implementations/ReactFabric-prod.js+0.41%377.57 kB379.14 kB+0.23%65.58 kB65.73 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.js+0.39%447.54 kB449.30 kB+0.30%71.59 kB71.80 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.js+0.39%447.57 kB449.32 kB+0.30%71.61 kB71.83 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js+0.36%485.61 kB487.35 kB+0.31%77.27 kB77.51 kB
facebook-www/ReactReconciler-prod.modern.js+0.34%503.84 kB505.58 kB+0.26%80.14 kB80.34 kB
facebook-www/ReactReconciler-prod.classic.js+0.34%514.01 kB515.74 kB+0.26%81.73 kB81.94 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.22 kB519.98 kB+0.28%81.09 kB81.31 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.25 kB520.00 kB+0.28%81.12 kB81.34 kB
facebook-www/ReactTestRenderer-dev.modern.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
facebook-www/ReactTestRenderer-dev.classic.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js+0.31%561.01 kB562.75 kB+0.27%87.36 kB87.59 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js+0.31%616.01 kB617.89 kB+0.23%98.63 kB98.86 kB
react-native/implementations/ReactFabric-profiling.js+0.29%451.73 kB453.06 kB+0.17%76.16 kB76.29 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.02 kB666.90 kB+0.21%105.14 kB105.36 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.04 kB666.92 kB+0.21%105.14 kB105.36 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.09 kB666.97 kB+0.21%105.16 kB105.39 kB
oss-stable-semver/react-dom/cjs/react-dom-client.production.js+0.27%606.10 kB607.76 kB+0.21%107.36 kB107.59 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-stable-semver/react-art/cjs/react-art.development.js+0.27%696.69 kB698.54 kB+0.20%109.67 kB109.88 kB
oss-stable/react-art/cjs/react-art.development.js+0.27%696.76 kB698.61 kB+0.20%109.69 kB109.91 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js+0.26%800.23 kB802.32 kB+0.19%125.30 kB125.53 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js+0.26%800.26 kB802.35 kB+0.19%125.32 kB125.56 kB
oss-experimental/react-art/cjs/react-art.development.js+0.25%727.52 kB729.37 kB+0.17%114.33 kB114.53 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js+0.25%854.64 kB856.74 kB+0.18%133.11 kB133.35 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.23 kB681.89 kB+0.18%118.08 kB118.30 kB
oss-stable/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.35 kB682.01 kB+0.18%118.12 kB118.33 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js+0.24%679.61 kB681.25 kB+0.19%120.81 kB121.04 kB
facebook-www/ReactART-dev.modern.js+0.24%771.86 kB773.71 kB+0.16%120.20 kB120.40 kB
facebook-www/ReactReconciler-dev.modern.js+0.24%879.24 kB881.33 kB+0.18%135.93 kB136.18 kB
facebook-www/ReactART-dev.classic.js+0.24%781.39 kB783.24 kB+0.16%121.93 kB122.13 kB
facebook-www/ReactReconciler-dev.classic.js+0.24%888.48 kB890.57 kB+0.17%137.70 kB137.94 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js+0.22%743.73 kB745.37 kB+0.17%128.55 kB128.77 kB

Generated by 🚫 dangerJS against 7d82cbc

}
case RootSuspendedWithDelay: {
if (!includesOnlyTransitions(lanes)) {
if (!includesOnlyTransitions(lanes) && !includesOnlyRetries(lanes)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case that fixes "collapsed" mode. Because otherwise when we render a case that would lead to an undesirable state (a previous row unsuspends which now suspends the next row) to actually commit today.

@sebmarkbage
sebmarkbage merged commit 986323f into react:mainNov 5, 2025
246 checks passed
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@sebmarkbage@react-sizebot@acdlite
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense - #35042

Merged
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden
Nov 5, 2025
Merged

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense#35042
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden

Conversation

@sebmarkbage

@sebmarkbagesebmarkbage commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

Normally if you suspend in a SuspenseList row above a Suspense boundary in that row, it'll suspend the parent. Which can itself delay the commit or resuspend a parent boundary. That's because SuspenseList mostly just coordinates the state of the inner boundaries and isn't a boundary itself.

However, for tail "hidden" and "collapsed" this is not quite the case because the rows themselves can avoid being rendered.

In the case of "collapsed" we require at least one Suspense boundary above to have successfully rendered before committing the list because the idea of this mode is that you should at least always show some indicator that things are still loading. Since we'd never try the next one after that at all, this just works. Expect there was an unrelated bug that meant that "suspend with delay" on a Retry didn't suspend the commit. This caused a scenario were it'd allow a commit proceed when it shouldn't. So I fixed that too. The counter intuitive thing here is that we won't actually show a previous completed row if the loading state of the next row is still loading.

For tail "hidden" it's a little different because we don't actually require any loading indicator at all to be shown while it's loading. If we attempt a row and it suspends, we can just hide it (and the rest) and move to commit. Therefore this implements a path where if all the rest of the tail are new mounts (we wouldn't be required to unmount any existing boundaries) then we can treat the SuspenseList boundary itself as "catching" the suspense. This is more coherent semantics since any future row that we didn't attempt also wouldn't resuspend the parent.

This allows simple cases like <SuspenseList>{list}</SuspenseList> to stream in each row without any indicator and no need for Suspense boundaries.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Nov 4, 2025
@react-sizebot

Copy link
Copy Markdown

Comparing: d000261...7d82cbc

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB+0.05%1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB+0.05%1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
facebook-www/ReactDOM-prod.classic.js+0.10%690.02 kB690.71 kB+0.06%121.46 kB121.53 kB
facebook-www/ReactDOM-prod.modern.js+0.10%680.45 kB681.13 kB+0.06%119.84 kB119.92 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.41 kB327.06 kB+0.34%56.67 kB56.86 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.49 kB327.13 kB+0.34%56.69 kB56.88 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.66 kB327.31 kB+0.34%56.73 kB56.92 kB
oss-stable-semver/react-art/cjs/react-art.production.js+0.50%330.13 kB331.77 kB+0.34%55.96 kB56.15 kB
oss-stable/react-art/cjs/react-art.production.js+0.50%330.20 kB331.85 kB+0.34%55.98 kB56.17 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js+0.47%345.38 kB347.01 kB+0.36%60.06 kB60.28 kB
oss-experimental/react-art/cjs/react-art.production.js+0.46%354.88 kB356.51 kB+0.33%59.92 kB60.12 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-profiling.js+0.45%370.17 kB371.84 kB+0.31%63.43 kB63.62 kB
facebook-www/ReactART-prod.modern.js+0.43%380.04 kB381.67 kB+0.31%63.75 kB63.94 kB
facebook-www/ReactART-prod.classic.js+0.42%389.88 kB391.51 kB+0.34%65.36 kB65.59 kB
react-native/implementations/ReactFabric-prod.js+0.41%377.57 kB379.14 kB+0.23%65.58 kB65.73 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.js+0.39%447.54 kB449.30 kB+0.30%71.59 kB71.80 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.js+0.39%447.57 kB449.32 kB+0.30%71.61 kB71.83 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js+0.36%485.61 kB487.35 kB+0.31%77.27 kB77.51 kB
facebook-www/ReactReconciler-prod.modern.js+0.34%503.84 kB505.58 kB+0.26%80.14 kB80.34 kB
facebook-www/ReactReconciler-prod.classic.js+0.34%514.01 kB515.74 kB+0.26%81.73 kB81.94 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.22 kB519.98 kB+0.28%81.09 kB81.31 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.25 kB520.00 kB+0.28%81.12 kB81.34 kB
facebook-www/ReactTestRenderer-dev.modern.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
facebook-www/ReactTestRenderer-dev.classic.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js+0.31%561.01 kB562.75 kB+0.27%87.36 kB87.59 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js+0.31%616.01 kB617.89 kB+0.23%98.63 kB98.86 kB
react-native/implementations/ReactFabric-profiling.js+0.29%451.73 kB453.06 kB+0.17%76.16 kB76.29 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.02 kB666.90 kB+0.21%105.14 kB105.36 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.04 kB666.92 kB+0.21%105.14 kB105.36 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.09 kB666.97 kB+0.21%105.16 kB105.39 kB
oss-stable-semver/react-dom/cjs/react-dom-client.production.js+0.27%606.10 kB607.76 kB+0.21%107.36 kB107.59 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-stable-semver/react-art/cjs/react-art.development.js+0.27%696.69 kB698.54 kB+0.20%109.67 kB109.88 kB
oss-stable/react-art/cjs/react-art.development.js+0.27%696.76 kB698.61 kB+0.20%109.69 kB109.91 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js+0.26%800.23 kB802.32 kB+0.19%125.30 kB125.53 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js+0.26%800.26 kB802.35 kB+0.19%125.32 kB125.56 kB
oss-experimental/react-art/cjs/react-art.development.js+0.25%727.52 kB729.37 kB+0.17%114.33 kB114.53 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js+0.25%854.64 kB856.74 kB+0.18%133.11 kB133.35 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.23 kB681.89 kB+0.18%118.08 kB118.30 kB
oss-stable/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.35 kB682.01 kB+0.18%118.12 kB118.33 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js+0.24%679.61 kB681.25 kB+0.19%120.81 kB121.04 kB
facebook-www/ReactART-dev.modern.js+0.24%771.86 kB773.71 kB+0.16%120.20 kB120.40 kB
facebook-www/ReactReconciler-dev.modern.js+0.24%879.24 kB881.33 kB+0.18%135.93 kB136.18 kB
facebook-www/ReactART-dev.classic.js+0.24%781.39 kB783.24 kB+0.16%121.93 kB122.13 kB
facebook-www/ReactReconciler-dev.classic.js+0.24%888.48 kB890.57 kB+0.17%137.70 kB137.94 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js+0.22%743.73 kB745.37 kB+0.17%128.55 kB128.77 kB

Generated by 🚫 dangerJS against 7d82cbc

}
case RootSuspendedWithDelay: {
if (!includesOnlyTransitions(lanes)) {
if (!includesOnlyTransitions(lanes) && !includesOnlyRetries(lanes)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case that fixes "collapsed" mode. Because otherwise when we render a case that would lead to an undesirable state (a previous row unsuspends which now suspends the next row) to actually commit today.

@sebmarkbage
sebmarkbage merged commit 986323f into react:mainNov 5, 2025
246 checks passed
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@sebmarkbage@react-sizebot@acdlite
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense - #35042

Merged
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden
Nov 5, 2025
Merged

[Fiber] SuspenseList with "hidden" tail row should "catch" suspense#35042
sebmarkbage merged 4 commits into
react:mainfrom
sebmarkbage:suspenselistsuspendhidden

Conversation

@sebmarkbage

@sebmarkbagesebmarkbage commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

Normally if you suspend in a SuspenseList row above a Suspense boundary in that row, it'll suspend the parent. Which can itself delay the commit or resuspend a parent boundary. That's because SuspenseList mostly just coordinates the state of the inner boundaries and isn't a boundary itself.

However, for tail "hidden" and "collapsed" this is not quite the case because the rows themselves can avoid being rendered.

In the case of "collapsed" we require at least one Suspense boundary above to have successfully rendered before committing the list because the idea of this mode is that you should at least always show some indicator that things are still loading. Since we'd never try the next one after that at all, this just works. Expect there was an unrelated bug that meant that "suspend with delay" on a Retry didn't suspend the commit. This caused a scenario were it'd allow a commit proceed when it shouldn't. So I fixed that too. The counter intuitive thing here is that we won't actually show a previous completed row if the loading state of the next row is still loading.

For tail "hidden" it's a little different because we don't actually require any loading indicator at all to be shown while it's loading. If we attempt a row and it suspends, we can just hide it (and the rest) and move to commit. Therefore this implements a path where if all the rest of the tail are new mounts (we wouldn't be required to unmount any existing boundaries) then we can treat the SuspenseList boundary itself as "catching" the suspense. This is more coherent semantics since any future row that we didn't attempt also wouldn't resuspend the parent.

This allows simple cases like <SuspenseList>{list}</SuspenseList> to stream in each row without any indicator and no need for Suspense boundaries.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Nov 4, 2025
@react-sizebot

Copy link
Copy Markdown

Comparing: d000261...7d82cbc

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB+0.05%1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB+0.05%1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
facebook-www/ReactDOM-prod.classic.js+0.10%690.02 kB690.71 kB+0.06%121.46 kB121.53 kB
facebook-www/ReactDOM-prod.modern.js+0.10%680.45 kB681.13 kB+0.06%119.84 kB119.92 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.41 kB327.06 kB+0.34%56.67 kB56.86 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.49 kB327.13 kB+0.34%56.69 kB56.88 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.js+0.51%325.66 kB327.31 kB+0.34%56.73 kB56.92 kB
oss-stable-semver/react-art/cjs/react-art.production.js+0.50%330.13 kB331.77 kB+0.34%55.96 kB56.15 kB
oss-stable/react-art/cjs/react-art.production.js+0.50%330.20 kB331.85 kB+0.34%55.98 kB56.17 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js+0.47%345.38 kB347.01 kB+0.36%60.06 kB60.28 kB
oss-experimental/react-art/cjs/react-art.production.js+0.46%354.88 kB356.51 kB+0.33%59.92 kB60.12 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-profiling.js+0.45%370.17 kB371.84 kB+0.31%63.43 kB63.62 kB
facebook-www/ReactART-prod.modern.js+0.43%380.04 kB381.67 kB+0.31%63.75 kB63.94 kB
facebook-www/ReactART-prod.classic.js+0.42%389.88 kB391.51 kB+0.34%65.36 kB65.59 kB
react-native/implementations/ReactFabric-prod.js+0.41%377.57 kB379.14 kB+0.23%65.58 kB65.73 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.js+0.39%447.54 kB449.30 kB+0.30%71.59 kB71.80 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.js+0.39%447.57 kB449.32 kB+0.30%71.61 kB71.83 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js+0.36%485.61 kB487.35 kB+0.31%77.27 kB77.51 kB
facebook-www/ReactReconciler-prod.modern.js+0.34%503.84 kB505.58 kB+0.26%80.14 kB80.34 kB
facebook-www/ReactReconciler-prod.classic.js+0.34%514.01 kB515.74 kB+0.26%81.73 kB81.94 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.22 kB519.98 kB+0.28%81.09 kB81.31 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.js+0.34%518.25 kB520.00 kB+0.28%81.12 kB81.34 kB
facebook-www/ReactTestRenderer-dev.modern.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
facebook-www/ReactTestRenderer-dev.classic.js+0.31%602.00 kB603.88 kB+0.21%97.14 kB97.34 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js+0.31%561.01 kB562.75 kB+0.27%87.36 kB87.59 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js+0.31%616.01 kB617.89 kB+0.23%98.63 kB98.86 kB
react-native/implementations/ReactFabric-profiling.js+0.29%451.73 kB453.06 kB+0.17%76.16 kB76.29 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.02 kB666.90 kB+0.21%105.14 kB105.36 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.04 kB666.92 kB+0.21%105.14 kB105.36 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js+0.28%665.09 kB666.97 kB+0.21%105.16 kB105.39 kB
oss-stable-semver/react-dom/cjs/react-dom-client.production.js+0.27%606.10 kB607.76 kB+0.21%107.36 kB107.59 kB
oss-stable/react-dom/cjs/react-dom-client.production.js+0.27%606.23 kB607.89 kB+0.21%107.38 kB107.61 kB
oss-stable-semver/react-art/cjs/react-art.development.js+0.27%696.69 kB698.54 kB+0.20%109.67 kB109.88 kB
oss-stable/react-art/cjs/react-art.development.js+0.27%696.76 kB698.61 kB+0.20%109.69 kB109.91 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js+0.26%800.23 kB802.32 kB+0.19%125.30 kB125.53 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js+0.26%800.26 kB802.35 kB+0.19%125.32 kB125.56 kB
oss-experimental/react-art/cjs/react-art.development.js+0.25%727.52 kB729.37 kB+0.17%114.33 kB114.53 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js+0.25%665.20 kB666.84 kB+0.19%117.26 kB117.48 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js+0.25%854.64 kB856.74 kB+0.18%133.11 kB133.35 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.23 kB681.89 kB+0.18%118.08 kB118.30 kB
oss-stable/react-dom/cjs/react-dom-profiling.profiling.js+0.24%680.35 kB682.01 kB+0.18%118.12 kB118.33 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js+0.24%679.61 kB681.25 kB+0.19%120.81 kB121.04 kB
facebook-www/ReactART-dev.modern.js+0.24%771.86 kB773.71 kB+0.16%120.20 kB120.40 kB
facebook-www/ReactReconciler-dev.modern.js+0.24%879.24 kB881.33 kB+0.18%135.93 kB136.18 kB
facebook-www/ReactART-dev.classic.js+0.24%781.39 kB783.24 kB+0.16%121.93 kB122.13 kB
facebook-www/ReactReconciler-dev.classic.js+0.24%888.48 kB890.57 kB+0.17%137.70 kB137.94 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js+0.22%743.73 kB745.37 kB+0.17%128.55 kB128.77 kB

Generated by 🚫 dangerJS against 7d82cbc

}
case RootSuspendedWithDelay: {
if (!includesOnlyTransitions(lanes)) {
if (!includesOnlyTransitions(lanes) && !includesOnlyRetries(lanes)) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case that fixes "collapsed" mode. Because otherwise when we render a case that would lead to an undesirable state (a previous row unsuspends which now suspends the next row) to actually commit today.

@sebmarkbage
sebmarkbage merged commit 986323f into react:mainNov 5, 2025
246 checks passed
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
github-actionsBot pushed a commit that referenced this pull request Nov 5, 2025
…35042)
Normally if you suspend in a SuspenseList row above a Suspense boundary
in that row, it'll suspend the parent. Which can itself delay the commit
or resuspend a parent boundary. That's because SuspenseList mostly just
coordinates the state of the inner boundaries and isn't a boundary
itself.
However, for tail "hidden" and "collapsed" this is not quite the case
because the rows themselves can avoid being rendered.
In the case of "collapsed" we require at least one Suspense boundary
above to have successfully rendered before committing the list because
the idea of this mode is that you should at least always show some
indicator that things are still loading. Since we'd never try the next
one after that at all, this just works. Expect there was an unrelated
bug that meant that "suspend with delay" on a Retry didn't suspend the
commit. This caused a scenario were it'd allow a commit proceed when it
shouldn't. So I fixed that too. The counter intuitive thing here is that
we won't actually show a previous completed row if the loading state of
the next row is still loading.
For tail "hidden" it's a little different because we don't actually
require any loading indicator at all to be shown while it's loading. If
we attempt a row and it suspends, we can just hide it (and the rest) and
move to commit. Therefore this implements a path where if all the rest
of the tail are new mounts (we wouldn't be required to unmount any
existing boundaries) then we can treat the SuspenseList boundary itself
as "catching" the suspense. This is more coherent semantics since any
future row that we didn't attempt also wouldn't resuspend the parent.
This allows simple cases like `<SuspenseList>{list}</SuspenseList>` to
stream in each row without any indicator and no need for Suspense
boundaries.
DiffTrain build for [986323f](986323f)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@sebmarkbage@react-sizebot@acdlite