Skip to content
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

chore: use comment nodes for VFragment bookends #3846

Merged
merged 8 commits into from
Nov 8, 2023

Conversation

divmain
Copy link
Contributor

@divmain divmain commented Nov 6, 2023

Details

Prior to this PR, empty text nodes were placed at each end of a VFragment. This allows for efficient insertion and removal of elements relative to the fragment, which may have sibling nodes not belonging to the fragment. These text nodes are essential bookends for fragments like those found in scoped slots or lwc:if.

When rendered on the server, these empty text nodes are rendered using a zero-width space character. This ensures that a text node is actually present after HTML is parsed by the browser. For example,<div /><div/> will be parsed by the browser (post-SSR, pre-hydration) as two <div>s, even if an empty text node was present in between those <div>s in the VDOM.

This introduces two problems:

  1. Zero-width space characters are layout-impacting. During hydration, we have special logic looking for text nodes with a single zero-width space character, to be matched with empty text VNodes. However, as part of that special logic, we replace the zero-width space character with an empty string, in order to match future CSR VDOM expectations. Additionally, an empty text node is not layout-impacting, whereas a single zero-width space character is layout-impacting in inline or inline-block contexts.
  2. VFragments can be nested. Because fragments are flattened into their containing element, this means there can be two or more text nodes adjacent to each other in the DOM, each one containing a zero-width space character. When this is rendered to HTML and subsequently parsed by the browser, the result is a single text node containing multiple zero-width space characters. Consequently, the number & placement of nodes/elements are inconsistent between SSR'd DOM and CSR VDOM, resulting in hydration failures.

The solution introduced here is to use comment nodes in place of empty text nodes. This clutters the Elements panel in the dev console somewhat, with seemingly extraneous comment nodes. However, this fix results in no performance degradation. HTML will be gzipped in transit, so the multiple comment nodes shouldn't impact network transit size. And we need not introduce special handling to the diffing algo that could easily result in a performance hit.

Does this pull request introduce a breaking change?

  • ⚠️ Probably yes, it does include an observable change.

See description of the breaking change in #3649.

Does this pull request introduce an observable change?

  • ⚠️ Yes, it does include an observable change.

Comment nodes will be present in the DOM where they were not before. Additionally, this involves a compiler change that will require an LWC minor-version bump.

GUS work item

W-14408593

@divmain divmain requested a review from a team as a code owner November 6, 2023 22:39
@divmain divmain force-pushed the divmain/nested-static-fragments branch from cbaa660 to 5e31425 Compare November 7, 2023 07:12
@divmain
Copy link
Contributor Author

divmain commented Nov 7, 2023

FYI for anyone wondering: CI is failing due to an unrelated issue. There's an incompatibility between our version of tachometer and the latest Chrome. Will need to be dealt with separately to make CI go green.

Copy link
Contributor

@jye-sf jye-sf left a comment

Choose a reason for hiding this comment

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

Is it confirmed we're releasing this as 5.0.0?

@divmain
Copy link
Contributor Author

divmain commented Nov 8, 2023

I think a v5.0 will be necessary, since there are breaking changes here. I'm not sure there are any downstreams that have consumed v4.0 that will be effected. But it seems like the "correct" thing to do. Since our internal 248 release is still in the hardening phase, we can still use the corresponding API version number (60) and associate it with v5.0.

@jye-sf
Copy link
Contributor

jye-sf commented Nov 8, 2023

I don't like it but I also can't think of an alternative right now.

This might be just an edge case, but I wonder in the future if it might make sense to hold off on releasing the next OSS major release externally until our internal FF, in case we have further bugfixes that need to be released through that release's API version.

  • We already hold the features that need to be released behind api versioning until close to early freeze to avoid jumping multiple major releases in a single internal release.
  • Usually, the issues we find/fix during this time do need to be fixed in that release and not deferred to the next release.

Might be worth discussing at release planning.

Thanks for getting this fix out so quickly!

@divmain divmain merged commit d900aca into master Nov 8, 2023
7 checks passed
@divmain divmain deleted the divmain/nested-static-fragments branch November 8, 2023 00:49
@nolanlawson
Copy link
Collaborator

FWIW, this PR looks good to me. LWC v5.0.0 and API versioning seem like the pragmatic choice.

The only additional information I would love to have is:

  1. Competitive analysis – what do other frameworks do for light DOM slots? How do they solve the SSR issue?
  2. Tachometer benchmark – running this PR against master as a sanity check to see if comments are slower than text nodes.

FWIW, for #1:

  • Vue renders empty text nodes, but in SSR they do <!--[--> for the start bookend and <!--]--> for the end bookend.
  • Svelte renders neither a text node nor a comment, in both CSR and SSR. They simply render whatever's inside the <slot> – nothing else. Not sure how they accomplish this.

@divmain
Copy link
Contributor Author

divmain commented Nov 14, 2023

For #2, I ran a subset of the benchmarks locally as a sanity check. All the tests timed out after 15 minutes, unable to determine a statistically significant change. I might run the full suite over night, at some point, to see if any perf regressions sneaked through.

For #1, I was aware of Vue's use of comment nodes in SSR, but quite unaware of Svelte's complete non-use of bookends. That's interesting, and possibly worth investigating further. This was something of a hot issue, so I didn't dig into alternatives as deeply as I might have otherwise. I'd love to understand what other frameworks are doing, too.

@nolanlawson
Copy link
Collaborator

FWIW, I ran all the benchmarks last night, 200 samples with 15 min timeout. No difference except 1% on the hydration benchmark. So no big deal!

click to see
| Benchmark                                      | Before (low) | Before (high) | Before (avg) | After (low) | After (high) | After (avg) | Delta (low) | Delta (high) | Delta (avg) | Delta perc (low) | Delta perc (high) | Delta perc (avg) |
| ---------------------------------------------- | ------------ | ------------- | ------------ | ----------- | ------------ | ----------- | ----------- | ------------ | ----------- | ---------------- | ----------------- | ---------------- |
| dom-expression                                 | 4.62         | 4.68          | 4.65         | 4.63        | 4.68         | 4.65        | -0.04       | 0.05         | 0.01        | -0.01            | 0.01              | 0.00             |
| dom-light-styled-component-create-10k-same     | 93.29        | 93.88         | 93.58        | 92.84       | 93.44        | 93.14       | -0.87       | -0.02        | -0.44       | -0.01            | -0.00             | -0.00            |
| dom-light-styled-component-create-1k-different | 112.04       | 113.15        | 112.60       | 111.73      | 112.72       | 112.23      | -1.12       | 0.37         | -0.37       | -0.01            | 0.00              | -0.00            |
| dom-ss-mutation-observer-10k                   | 1235.03      | 1249.94       | 1242.48      | 1237.34     | 1252.60      | 1244.97     | -8.18       | 13.16        | 2.49        | -0.01            | 0.01              | 0.00             |
| dom-ss-slot-create-container-5k                | 312.09       | 314.57        | 313.33       | 312.29      | 314.76       | 313.52      | -1.56       | 1.94         | 0.19        | -0.00            | 0.01              | 0.00             |
| dom-ss-slot-update-slotted-content-5k          | 188.64       | 190.55        | 189.59       | 188.51      | 190.32       | 189.41      | -1.50       | 1.14         | -0.18       | -0.01            | 0.01              | -0.00            |
| dom-ss-styled-component-create-10k-same        | 154.16       | 155.35        | 154.75       | 154.23      | 155.53       | 154.88      | -0.75       | 1.01         | 0.13        | -0.00            | 0.01              | 0.00             |
| dom-ss-styled-component-create-1k-different    | 136.34       | 138.01        | 137.18       | 136.59      | 138.12       | 137.36      | -0.96       | 1.32         | 0.18        | -0.01            | 0.01              | 0.00             |
| dom-styled-component-create-10k-same           | 165.11       | 166.47        | 165.79       | 164.47      | 165.76       | 165.12      | -1.61       | 0.26         | -0.68       | -0.01            | 0.00              | -0.00            |
| dom-styled-component-create-1k-different       | 128.15       | 129.11        | 128.63       | 128.47      | 129.73       | 129.10      | -0.33       | 1.26         | 0.47        | -0.00            | 0.01              | 0.00             |
| dom-table-create-10k                           | 114.28       | 115.05        | 114.67       | 114.17      | 114.85       | 114.51      | -0.67       | 0.36         | -0.15       | -0.01            | 0.00              | -0.00            |
| dom-table-hydrate-1k                           | 37.05        | 37.38         | 37.22        | 37.23       | 37.51        | 37.37       | -0.06       | 0.37         | 0.15        | -0.00            | 0.01              | 0.00             |
| dom-tablecmp-append-1k                         | 34.11        | 34.38         | 34.24        | 33.96       | 34.20        | 34.08       | -0.34       | 0.02         | -0.16       | -0.01            | 0.00              | -0.00            |
| dom-tablecmp-create-10k                        | 276.41       | 278.63        | 277.52       | 277.32      | 279.83       | 278.57      | -0.62       | 2.73         | 1.05        | -0.00            | 0.01              | 0.00             |
| dom-tablecmp-create-1k                         | 41.79        | 42.08         | 41.93        | 41.82       | 42.15        | 41.98       | -0.17       | 0.27         | 0.05        | -0.00            | 0.01              | 0.00             |
| dom-tablecmp-hydrate-1k                        | 36.62        | 36.80         | 36.71        | 37.07       | 37.23        | 37.15       | 0.32        | 0.56         | 0.44        | 0.01             | 0.02              | 0.01             |
| dom-trackedcmp-create-10k                      | 240.32       | 242.38        | 241.35       | 240.89      | 243.17       | 242.03      | -0.86       | 2.21         | 0.68        | -0.00            | 0.01              | 0.00             |
| dom-trackedcmp-update-10k                      | 104.06       | 105.12        | 104.59       | 103.81      | 104.90       | 104.36      | -0.99       | 0.52         | -0.24       | -0.01            | 0.00              | -0.00            |
| dom-wireslist-create-10k                       | 233.94       | 235.74        | 234.84       | 233.79      | 236.07       | 234.93      | -1.36       | 1.54         | 0.09        | -0.01            | 0.01              | 0.00             |
| server-table-render-10k                        | 173.02       | 174.64        | 173.83       | 173.25      | 174.98       | 174.11      | -0.90       | 1.46         | 0.28        | -0.01            | 0.01              | 0.00             |
| server-tablecmp-render-10k                     | 168.22       | 169.28        | 168.75       | 168.39      | 169.44       | 168.91      | -0.58       | 0.91         | 0.16        | -0.00            | 0.01              | 0.00             |

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants