-
Notifications
You must be signed in to change notification settings - Fork 48.4k
[Fizz] Emit link rel="expect" to block render before the shell has fully loaded #33016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
if ( | ||
(preamble.htmlChunks || preamble.headChunks) && | ||
bootstrapChunks.length === 0 | ||
) { | ||
// If we rendered the whole document, then we emitted a rel="expect" that needs a | ||
// matching target. If we haven't emitted that yet, we need to include it in this | ||
// script tag. | ||
bootstrapChunks.push(renderState.startInlineScript); | ||
pushCompletedShellIdAttribute(bootstrapChunks, resumableState); | ||
bootstrapChunks.push( | ||
endOfStartTag, | ||
formReplayingRuntimeScript, | ||
endInlineScript, | ||
); | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not following how this doesn't lead to possible duplicate writing of the id.
If we included it as part of the bootstrap scripts and sent the shell and then later we render a form that requires this runtime the bootstrapChunks will be empty and we'll end up emitting it again.
it may just require that we track whether the bootstrap scripts were actually emitted. Or maybe we just do the template trick and never try to use this script as a vehicle for the expected id.
Also, i don't understand how in the above implementation you are deciding whether or not this runtime injection is happening within the shell vs at arbitrary later point. If we knew it was the shell we could then use the bootstrapChunks.length === 0 as the signal that no other thing was about to emit this id but I don't think that actually works here
renderState: RenderState, | ||
): boolean { | ||
const preamble = renderState.preamble; | ||
if (preamble.htmlChunks || preamble.headChunks) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition here isn't symmetrical with the one in writePreambleStart
. Maybe that's ok because the ID is essentially reserved and inert without the associated expect link but if we want to actually be precise we need to know if we are in a mode that never emits the paint blocking link
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean the skipExpect
hack? I override all of writeCompletedRoot in ReactMarkup. That's in line with how the rest of those overrides work.
However, for the preamble, I still need to write most of it and just exclude this one thing. I'm not sure how to layer that override yet. Maybe it becomes a render state config at some point but for now I wanted it to be compiled out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh yeah, and the only other place is in the form replaying runtime but that is not part of markup output either?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
those error if you use functions in forms in markup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but also markup can't outline anything so there's no "completed boundary" instructions written. In fact that whole thing is stubbed out.
…lly loaded (#33016) The semantics of React is that anything outside of Suspense boundaries in a transition doesn't display until it has fully unsuspended. With SSR streaming the intention is to preserve that. We explicitly don't want to support the mode of document streaming normally supported by the browser where it can paint content as tags stream in since that leads to content popping in and thrashing in unpredictable ways. This should instead be modeled explictly by nested Suspense boundaries or something like SuspenseList. After the first shell any nested Suspense boundaries are only revealed, by script, once they're fully streamed in to the next boundary. So this is already the case there. However, for the initial shell we have been at the mercy of browser heuristics for how long it decides to stream before the first paint. Chromium now has [an API explicitly for this use case](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#stabilizing_page_state_to_make_cross-document_transitions_consistent) that lets us model the semantics that we want. This is always important but especially so with MPA View Transitions. After this a simple document looks like this: ```html <!DOCTYPE html> <html> <head> <link rel="expect" href="#«R»" blocking="render"/> </head> <body> <p>hello world</p> <script src="bootstrap.js" id="«R»" async=""></script> ... </body> </html> ``` The `rel="expect"` tag indicates that we want to wait to paint until we have streamed far enough to be able to paint the id `"«R»"` which indicates the shell. Ideally this `id` would be assigned to the root most HTML element in the body. However, this is tricky in our implementation because there can be multiple and we can render them out of order. So instead, we assign the id to the first bootstrap script if there is one since these are always added to the end of the shell. If there isn't a bootstrap script then we emit an empty `<template id="«R»"></template>` instead as a marker. Since we currently put as much as possible in the shell if it's loaded by the time we render, this can have some negative effects for very large documents. We should instead apply the heuristic where very large Suspense boundaries get outlined outside the shell even if they're immediately available. This means that even prerenders can end up with script tags. We only emit the `rel="expect"` if you're rendering a whole document. I.e. if you rendered either a `<html>` or `<head>` tag. If you're rendering a partial document, then we don't really know where the streaming parts are anyway and can't provide such guarantees. This does apply whether you're streaming or not because we still want to block rendering until the end, but in practice any serialized state that needs hydrate should still be embedded after the completion id. DiffTrain build for [143d3e1](143d3e1)
Since the very beginning we have had the `progressiveChunkSize` option but we never actually took advantage of it because we didn't count the bytes that we emitted. This starts counting the bytes by taking a pass over the added chunks each time a segment completes. That allows us to outline a Suspense boundary to stream in late even if it is already loaded by the time that back-pressure flow and in a `prerender`. Meaning it gets inserted with script. The effect can be seen in the fixture where if you have large HTML content that can block initial paint (thanks to [`rel="expect"`](#33016) but also nested Suspense boundaries). Before this fix, the paint would be blocked until the large content loaded. This lets us paint the fallback first in the case that the raw bytes of the content takes a while to download. You can set it to `Infinity` to opt-out. E.g. if you want to ensure there's never any scripts. It's always set to `Infinity` in `renderToHTML` and the legacy `renderToString`. One downside is that if we might choose to outline a boundary, we need to let its fallback complete. We don't currently discount the size of the fallback but really just consider them additive even though in theory the fallback itself could also add significant size or even more than the content. It should maybe really be considered the delta but that would require us to track the size of the fallback separately which is tricky. One problem with the current heuristic is that we just consider the size of the boundary content itself down to the next boundary. If you have a lot of small boundaries adding up, it'll never kick in. I intend to address that in a follow up.
Since the very beginning we have had the `progressiveChunkSize` option but we never actually took advantage of it because we didn't count the bytes that we emitted. This starts counting the bytes by taking a pass over the added chunks each time a segment completes. That allows us to outline a Suspense boundary to stream in late even if it is already loaded by the time that back-pressure flow and in a `prerender`. Meaning it gets inserted with script. The effect can be seen in the fixture where if you have large HTML content that can block initial paint (thanks to [`rel="expect"`](#33016) but also nested Suspense boundaries). Before this fix, the paint would be blocked until the large content loaded. This lets us paint the fallback first in the case that the raw bytes of the content takes a while to download. You can set it to `Infinity` to opt-out. E.g. if you want to ensure there's never any scripts. It's always set to `Infinity` in `renderToHTML` and the legacy `renderToString`. One downside is that if we might choose to outline a boundary, we need to let its fallback complete. We don't currently discount the size of the fallback but really just consider them additive even though in theory the fallback itself could also add significant size or even more than the content. It should maybe really be considered the delta but that would require us to track the size of the fallback separately which is tricky. One problem with the current heuristic is that we just consider the size of the boundary content itself down to the next boundary. If you have a lot of small boundaries adding up, it'll never kick in. I intend to address that in a follow up. DiffTrain build for [8e9a5fc](8e9a5fc)
Since the very beginning we have had the `progressiveChunkSize` option but we never actually took advantage of it because we didn't count the bytes that we emitted. This starts counting the bytes by taking a pass over the added chunks each time a segment completes. That allows us to outline a Suspense boundary to stream in late even if it is already loaded by the time that back-pressure flow and in a `prerender`. Meaning it gets inserted with script. The effect can be seen in the fixture where if you have large HTML content that can block initial paint (thanks to [`rel="expect"`](facebook#33016) but also nested Suspense boundaries). Before this fix, the paint would be blocked until the large content loaded. This lets us paint the fallback first in the case that the raw bytes of the content takes a while to download. You can set it to `Infinity` to opt-out. E.g. if you want to ensure there's never any scripts. It's always set to `Infinity` in `renderToHTML` and the legacy `renderToString`. One downside is that if we might choose to outline a boundary, we need to let its fallback complete. We don't currently discount the size of the fallback but really just consider them additive even though in theory the fallback itself could also add significant size or even more than the content. It should maybe really be considered the delta but that would require us to track the size of the fallback separately which is tricky. One problem with the current heuristic is that we just consider the size of the boundary content itself down to the next boundary. If you have a lot of small boundaries adding up, it'll never kick in. I intend to address that in a follow up. DiffTrain build for [8e9a5fc](facebook@8e9a5fc)
The semantics of React is that anything outside of Suspense boundaries in a transition doesn't display until it has fully unsuspended. With SSR streaming the intention is to preserve that.
We explicitly don't want to support the mode of document streaming normally supported by the browser where it can paint content as tags stream in since that leads to content popping in and thrashing in unpredictable ways. This should instead be modeled explictly by nested Suspense boundaries or something like SuspenseList.
After the first shell any nested Suspense boundaries are only revealed, by script, once they're fully streamed in to the next boundary. So this is already the case there. However, for the initial shell we have been at the mercy of browser heuristics for how long it decides to stream before the first paint.
Chromium now has an API explicitly for this use case that lets us model the semantics that we want. This is always important but especially so with MPA View Transitions.
After this a simple document looks like this:
The
rel="expect"
tag indicates that we want to wait to paint until we have streamed far enough to be able to paint the id"«R»"
which indicates the shell.Ideally this
id
would be assigned to the root most HTML element in the body. However, this is tricky in our implementation because there can be multiple and we can render them out of order.So instead, we assign the id to the first bootstrap script if there is one since these are always added to the end of the shell. If there isn't a bootstrap script then we emit an empty
<template id="«R»"></template>
instead as a marker.Since we currently put as much as possible in the shell if it's loaded by the time we render, this can have some negative effects for very large documents. We should instead apply the heuristic where very large Suspense boundaries get outlined outside the shell even if they're immediately available. This means that even prerenders can end up with script tags.
We only emit the
rel="expect"
if you're rendering a whole document. I.e. if you rendered either a<html>
or<head>
tag. If you're rendering a partial document, then we don't really know where the streaming parts are anyway and can't provide such guarantees. This does apply whether you're streaming or not because we still want to block rendering until the end, but in practice any serialized state that needs hydrate should still be embedded after the completion id.