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
[Fizz] Improve text separator byte efficiency #24630
Changes from 3 commits
96fad9f
6393401
a9e72bf
d0660fe
3030aec
3f61057
f7c4b86
19c76fa
fc76c7f
277beb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -279,6 +279,7 @@ function encodeHTMLTextNode(text: string): string { | |
} | ||
|
||
const textSeparator = stringToPrecomputedChunk('<!-- -->'); | ||
let lastPushedText = false; | ||
|
||
export function pushTextInstance( | ||
target: Array<Chunk | PrecomputedChunk>, | ||
|
@@ -289,8 +290,72 @@ export function pushTextInstance( | |
// Empty text doesn't have a DOM node representation and the hydration is aware of this. | ||
return; | ||
} | ||
// TODO: Avoid adding a text separator in common cases. | ||
target.push(stringToChunk(encodeHTMLTextNode(text)), textSeparator); | ||
// We use a null charater to wrap every text chunk. we use this to boundary check whether we need | ||
// to insert a Node textSeparator like <!-- --> when flushing segments between sets of chunks. | ||
if (lastPushedText) { | ||
target.push(textSeparator); | ||
} | ||
lastPushedText = true; | ||
target.push(stringToChunk(encodeHTMLTextNode(text))); | ||
} | ||
|
||
// The Text Embedding flags allow the format config to know when to include or exclude | ||
// text separators between parent and child segments. We track the leading edge and | ||
// trailing edges separately however due to the fact that peeking ahead is usually | ||
// not possible we discern between knowing we need a separator at the leading edge and | ||
// assuming we might need one at the trailing edge. In the future we could get more | ||
// advanced with the tracking and drop the trailing edge in some cases when we know a segment | ||
// is followed by a Text or non-Text Node explicitly vs followed by another Segment | ||
export opaque type TextEmbedding = number; | ||
|
||
const NO_TEXT_EMBED = /* */ 0b0000; | ||
|
||
const LEADING_SEPARATOR_NEEDED = /* */ 0b0011; | ||
const LEADING_TEXT_EMBED_KNOWN = /* */ 0b0001; | ||
// const LEADING_TEXT_EMBED_POSSIBLE = /* */ 0b0010; | ||
|
||
const TRAILING_SEPARATOR_NEEDED = /* */ 0b1100; | ||
// const TRAILING_TEXT_EMBED_KNOWN = /* */ 0b0100; | ||
const TRAILING_TEXT_EMBED_POSSIBLE = /* */ 0b1000; | ||
|
||
// Suspense boundaries always emit a comment node at the leading and trailing edge and thus need | ||
// no additional separators. we also use this for the root segment even though it isn't a Boundary | ||
// because it cannot have pre or post text and thus matches the same embedding semantics or Boundaries | ||
export function textEmbeddingForBoundarySegment(): TextEmbedding { | ||
lastPushedText = false; | ||
return NO_TEXT_EMBED; | ||
} | ||
|
||
// Segments that are not the boundary segment can be truly embedded in Text. we determine the leading edge | ||
// based on what was last pushed. We cannot know the trailing edge so we conservatively assume we are embedded there | ||
export function textEmbeddingForSegment(): TextEmbedding { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While this whole feature is DOM specific and ideally we wouldn't punish other environments - it's also really DOM that's our main (and currently only target). I wouldn't be too hurt if this feature was just built into ReactFizzServer instead. Including the lastPushedText part. Since otherwise we leak a lot of concepts and have to make up flags etc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I had this thought as I kept adding more functions to the format config. glad you see it the same way and makes sense. I'll move things over so we can clean up the interface There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you able to just use the segment.textEmbedding flag as the running tally for lastPushedText? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not totally, for boundaries, we need to track lastPushedText internally but we never need to push a separator at the end b/c we always have the suspense comment node. I suppose if I could know I was in a segment that was a boundary or the root I could use that as an alternative way to discriminating those cases and actually for delayed segments we don't want to emit a final separator but they aren't boundaries so I need a way to know if it is delayed or not when I get to the final push but I can just track that all as another property on the Segment |
||
let embedding = TRAILING_TEXT_EMBED_POSSIBLE; | ||
embedding |= lastPushedText ? LEADING_TEXT_EMBED_KNOWN : NO_TEXT_EMBED; | ||
|
||
lastPushedText = false; | ||
return embedding; | ||
} | ||
|
||
// If a Segment is going to be flushed later than it's parent text separators arent needed | ||
// because the DOM patch will leavee the adjacent text as separate nodes | ||
export function textEmbeddingForDelayedSegment(): TextEmbedding { | ||
return NO_TEXT_EMBED; | ||
} | ||
|
||
// Called when a segment is about to be rendered by a Task | ||
export function prepareForSegment(textEmbedding: TextEmbedding) { | ||
lastPushedText = textEmbedding & LEADING_SEPARATOR_NEEDED; | ||
} | ||
|
||
// Called when a segment is exhaustively rendered | ||
export function finalizeForSegment( | ||
target: Array<Chunk | PrecomputedChunk>, | ||
textEmbedding: TextEmbedding, | ||
) { | ||
if (lastPushedText && textEmbedding & TRAILING_SEPARATOR_NEEDED) { | ||
target.push(textSeparator); | ||
} | ||
lastPushedText = false; | ||
} | ||
|
||
const styleNameCache: Map<string, PrecomputedChunk> = new Map(); | ||
|
@@ -1324,6 +1389,7 @@ export function pushStartInstance( | |
responseState: ResponseState, | ||
formatContext: FormatContext, | ||
): ReactNodeList { | ||
lastPushedText = false; | ||
if (__DEV__) { | ||
validateARIAProperties(type, props); | ||
validateInputProperties(type, props); | ||
|
@@ -1436,6 +1502,7 @@ export function pushEndInstance( | |
type: string, | ||
props: Object, | ||
): void { | ||
lastPushedText = false; | ||
switch (type) { | ||
// Omitted close tags | ||
// TODO: Instead of repeating this switch we could try to pass a flag from above. | ||
|
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 can use
ResponseState
for this instead of module scope. It's a little safer because you can technically callrenderToString
from withinrenderToString
I think.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.
good call