Fix Suspense SSR content hidden for no-JS users (#35460) - #424
Conversation
Removed size-based outlining condition that caused large Suspense
boundaries to show fallback with a JS instruction to swap content.
Without JS, the content never appeared.
This change preserves defer={true} and suspensey content outlining,
but forces large content to inline, ensuring visibility for all users.
Greptile SummaryThis PR adds a regression test (
Confidence Score: 2/5
Important Files Changed
Prompt To Fix All With AIThis is a comment left during a code review.
Path: packages/react-dom/src/__tests__/ReactDOMFizzServerNoScript-test.js
Line: 55-56
Comment:
**Misleading assertion comment for no-JS visibility**
The comment "No-JS user sees it" is inaccurate. `toContain(largeContent)` passes in **both** the inlined and outlined cases — when a boundary is outlined, the actual content is still written to the stream (as a completed boundary that's swapped in via JavaScript). So this assertion does not distinguish between the two paths and does not prove that no-JS users can see the content.
The true proof of no-JS visibility is the second assertion on line 60 (`not.toContain('Loading...')`). Consider updating the comment to avoid suggesting this assertion is sufficient on its own:
```suggestion
// Verify large content is present somewhere in the stream
expect(streamedContent).toContain(largeContent);
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 0197581 |
| /** | ||
| * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| * @emails react-core | ||
| * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment | ||
| */ | ||
|
|
||
| 'use strict'; | ||
|
|
||
| let React; | ||
| let ReactDOMFizzServer; | ||
| let Suspense; | ||
|
|
||
| describe('ReactDOMFizzServerNoScript', () => { | ||
| beforeEach(() => { | ||
| jest.resetModules(); | ||
| React = require('react'); | ||
| Suspense = React.Suspense; | ||
| ReactDOMFizzServer = require('react-dom/server'); | ||
| }); | ||
|
|
||
| it('renders large content inline for no-JS support (size-based outlining disabled)', async () => { | ||
| // Generate content larger than progressiveChunkSize (100) | ||
| const largeContent = 'A'.repeat(2000); | ||
|
|
||
| function App() { | ||
| return ( | ||
| <Suspense fallback="Loading..."> | ||
| <div>{largeContent}</div> | ||
| </Suspense> | ||
| ); | ||
| } | ||
|
|
||
| let streamedContent = ''; | ||
| const writable = new (require('stream').Writable)({ | ||
| write(chunk, encoding, callback) { | ||
| streamedContent += chunk.toString(); | ||
| callback(); | ||
| }, | ||
| }); | ||
|
|
||
| await new Promise(resolve => { | ||
| const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, { | ||
| progressiveChunkSize: 100, | ||
| onAllReady() { | ||
| pipe(writable); | ||
| resolve(); | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| // Verify content is present in the initial stream (No-JS user sees it) | ||
| expect(streamedContent).toContain(largeContent); | ||
|
|
||
| // Verify fallback is NOT present (because it was inlined, so no pending state) | ||
| // Note: If it WAS outlined, we would see "Loading..." | ||
| expect(streamedContent).not.toContain('Loading...'); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Missing source changes — test has no corresponding fix
This PR adds a regression test that asserts large Suspense content is inlined rather than outlined, but the actual source change to packages/react-server/src/ReactFizzServer.js is missing from the PR diff. The merge commit (fb79b1e) resolved the merge in a way that dropped all changes to ReactFizzServer.js, while the original commit (29bdf10) did modify it to remove the size-based outlining condition at line 5595:
(flushedByteSize + boundary.byteSize > request.progressiveChunkSize ||
hasSuspenseyContent(boundary.contentState, flushingShell) ||
boundary.defer)
Without removing that condition, this test should fail because:
- With
progressiveChunkSize: 100and content of 2000 bytes, the boundary will be outlined (emitted as pending with fallback) - The outlined content is still flushed later in the same pass via
flushCompletedQueues, sostreamedContentwill contain both the large content AND"Loading..." - The assertion on line 60 (
expect(streamedContent).not.toContain('Loading...')) would fail
The ReactFizzServer.js changes need to be re-applied for this test (and the actual fix) to work.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-dom/src/__tests__/ReactDOMFizzServerNoScript-test.js
Line: 1-62
Comment:
**Missing source changes — test has no corresponding fix**
This PR adds a regression test that asserts large Suspense content is inlined rather than outlined, but the actual source change to `packages/react-server/src/ReactFizzServer.js` is **missing from the PR diff**. The merge commit (`fb79b1e`) resolved the merge in a way that dropped all changes to `ReactFizzServer.js`, while the original commit (`29bdf10`) did modify it to remove the size-based outlining condition at line 5595:
```
(flushedByteSize + boundary.byteSize > request.progressiveChunkSize ||
hasSuspenseyContent(boundary.contentState, flushingShell) ||
boundary.defer)
```
Without removing that condition, this test should **fail** because:
1. With `progressiveChunkSize: 100` and content of 2000 bytes, the boundary will be **outlined** (emitted as pending with fallback)
2. The outlined content is still flushed later in the same pass via `flushCompletedQueues`, so `streamedContent` will contain both the large content AND `"Loading..."`
3. The assertion on line 60 (`expect(streamedContent).not.toContain('Loading...')`) would fail
The `ReactFizzServer.js` changes need to be re-applied for this test (and the actual fix) to work.
How can I resolve this? If you propose a fix, please make it concise.| // Verify content is present in the initial stream (No-JS user sees it) | ||
| expect(streamedContent).toContain(largeContent); |
There was a problem hiding this comment.
Misleading assertion comment for no-JS visibility
The comment "No-JS user sees it" is inaccurate. toContain(largeContent) passes in both the inlined and outlined cases — when a boundary is outlined, the actual content is still written to the stream (as a completed boundary that's swapped in via JavaScript). So this assertion does not distinguish between the two paths and does not prove that no-JS users can see the content.
The true proof of no-JS visibility is the second assertion on line 60 (not.toContain('Loading...')). Consider updating the comment to avoid suggesting this assertion is sufficient on its own:
| // Verify content is present in the initial stream (No-JS user sees it) | |
| expect(streamedContent).toContain(largeContent); | |
| // Verify large content is present somewhere in the stream | |
| expect(streamedContent).toContain(largeContent); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-dom/src/__tests__/ReactDOMFizzServerNoScript-test.js
Line: 55-56
Comment:
**Misleading assertion comment for no-JS visibility**
The comment "No-JS user sees it" is inaccurate. `toContain(largeContent)` passes in **both** the inlined and outlined cases — when a boundary is outlined, the actual content is still written to the stream (as a completed boundary that's swapped in via JavaScript). So this assertion does not distinguish between the two paths and does not prove that no-JS users can see the content.
The true proof of no-JS visibility is the second assertion on line 60 (`not.toContain('Loading...')`). Consider updating the comment to avoid suggesting this assertion is sufficient on its own:
```suggestion
// Verify large content is present somewhere in the stream
expect(streamedContent).toContain(largeContent);
```
How can I resolve this? If you propose a fix, please make it concise.
Mirror of facebook/react#35628
Original author: maxathy
Summary
Removed size-based outlining condition that caused large Suspense boundaries to show fallback with a JS instruction to swap content. Without JS, the content never appeared.
This change preserves defer={true} and suspensey content outlining, but forces large content to inline, ensuring visibility for all users.
How did you test this change?
Added new regression test: Created
ReactDOMFizzServerNoScript-test.js
that verifies large Suspense content (>2000 bytes) appears in the initial HTML stream when using a small progressiveChunkSize (100). This confirms no-JS users will see the content instead of the fallback.