-
Notifications
You must be signed in to change notification settings - Fork 396
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
fix(engine-dom): remove stylesheets when unused #2797
Conversation
Two thoughts on this PR:
|
4aab717
to
c354b27
Compare
This reverts commit 1b83ab6.
5573b54
to
79ecf74
Compare
|
||
function insertStyleElement(content: string, target: ShadowRoot | Document) { | ||
const elm = createStyleElement(content); | ||
const targetAnchorPoint = isDocument(target) ? target.head : target; |
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.
Previously we were doing:
document.head || document.body || document
But I don't see the point of that. If it's the document, it has a head. And if it has neither a head nor a body, the browser won't let you append directly to the document anyway.
adoptedStyleSheets.push(styleSheet); | ||
} else { | ||
target.adoptedStyleSheets = [...adoptedStyleSheets, styleSheet]; | ||
} |
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.
Previously we were not using constructable stylesheets for the global document
. The main reason was performance (there are a lot of stylesheets on the document
compared to a single shadow root). But now that we have mutable adopted stylesheets shipping in Chrome 99, there shouldn't be a perf penalty for appending to the adoptedStyleSheets
array.
/nucleus test |
As it turns out, this is a really difficult feature to implement. And it causes a more severe perf impact than I expected: Considering the perf impact, the extra complexity, the potential for backwards compatibility breakages, and the potential for unexpected breakages if our bookkeeping is buggy (e.g. styles get unrendered when they shouldn't), it's debatable whether we should even implement this. |
if (document.adoptedStyleSheets) { | ||
document.adoptedStyleSheets = []; | ||
} | ||
}); |
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.
It would be great to fix this, but it only happens in dev mode, and hot-swapping is not even used on platform.
expect(getComputedStyle(element.shadowRoot.querySelector('.green')).color).toEqual( | ||
'rgb(0, 0, 0)' | ||
); | ||
expect(getComputedStyle(element).marginLeft).toEqual('0px'); |
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.
This is a good example of the behavior becoming more consistent with this PR. This is very subtle, because what's actually happening here is that, in synthetic, the stylesheets are also not removed from the DOM. But the reason the color is "correct" in synthetic mode is that the scope attributes are not applied to the DOM nodes (due to the stylesheets
array being empty).
In any case, this PR makes the behavior consistent.
} | ||
} else if (ssr || isHydrating()) { | ||
} else if (ssr || vm.hydrated) { |
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.
It turns out that what we care about is if the vm was hydrated in the first place, not whether it's currently hydrating.
throw new Error('Unexpected element left in the <head> by a test: ' + child.outerHTML); | ||
} | ||
}); | ||
var bodyChildren = getBodyChildren(); |
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.
We actually don't want the test to clear the <head>
after every test, because that might hide bugs in the stylesheet count bookkeeping. Changing this code helped me find some bugs in how I was handling hydration, as well as the hot-swapping issue.
I can't believe I got this working, so I guess this PR is ready for review now. This should probably have a feature flag, but we need to fix #2811 first. |
Let me play the devil's advocate and challenge the necessity of this change. This change doesn't impact the native shadow DOM components. For native shadow DOM components, styles are either injected in the shadow tree via a This leaves us with synthetic shadow DOM components and light DOM components. We all agree that synthetic shadow DOM isn't something we want to deprecate. So I am not much worried about the impact of leaving the stylesheet in place for synthetic shadow DOM components. IMO to evaluate the necessity for this change we should answer the following question: "Should stylesheets injected by light DOM components be cleanup upon removal?". Note that this should only impact light DOM components that are injected inside the main document tree. Light DOM components injected within a shadow tree (light DOM components wrapped within shadow DOM component) aren't impacted as the stylesheet will be injected in the closest ancestor shadow root. Going back to the original issue #2466 (comment), @nolanlawson you added an interesting comment:
While I understand why you are referring to the CSS-in-JS, I think a better comparison would be with Svelte and Vue component styling. Those frameworks have built-in styling support which is more relatable to LWC light DOM component styling. Upon inspection, none of those frameworks do clean up injected styles after unmounting a component tree. I haven't heard either strong feedback from developers using those frameworks complaining about the lack of style clean-up capability. If the main concern here is the potential style clashing cross light DOM components, my answer would be to prefer scoped styles over global styles for light DOM components. As a side note, there are a couple of interesting refactors in this PR, that we might want to land regardless of if we roll out style clean up or not. |
The most important case this PR solves is multi-template components, which does affect native shadow DOM. If a component switches from template A to template B, and then back to template A, then B's styles will still win because they are listed last in the DOM. What prompted me to write this PR is someone pointing out that our own documentation is incorrect on this (#2793 (comment)). Probably this worked at some point in the past (when native shadow injected inline
It does affect light DOM components inside of shadow roots. If the shadow root is never removed from the DOM, then the styles are never cleaned up. So this may happen if a large omnipresent container component is never removed. Any light DOM components inside of that shadow root will just continually append style sheets and never remove them.
Scoped styles actually have exactly the same problem with multi-template components. Scoped styles use the same scoping token for all templates owned by a given component. So if that component switches templates, the two style sheets will still conflict with each other.
I didn't find anything for Vue, but I did find some discussion in Svelte and certain CSS-in-JS libraries about this. It seems some developers would like this functionality:
For LWC, I think an argument could be made that we should only remove styles for multi-template components. This case feels clearly "wrong" to me, whereas removing styles on component unmount feels a bit like an implementation detail, or a memory leak optimization. (E.g. for dynamically-created components, we don't want to keep adding This PR could be refactored to only handle multi-template components. But I haven't thought much about how we would implement it, or how we would handle corner cases. (E.g. simply reordering the style sheets is not sufficient for the case where template A has no styles, but template B does.) |
@pmdartus OK, after Dev Sync, I realized I was wrong about two things:
So maybe our message to developers should be: "If you want multi-template components to work as expected when switching back and forth, then use scoped styles." Ditto for light DOM components, as you said. |
Closing in favor of #2827 |
Details
Fixes #2466
This PR causes
<style>
s andadoptedStyleSheets
to be removed from the DOM when the style is no longer needed. This happens in two cases:render()
function switches from one template to anotherNote that multiple components/templates may depend on the same
<style>
/adoptedStyleSheet
(e.g. multiple instances of the same component), so we have to do some bookkeeping to check that no components/templates are still relying on the style.Upsides of this PR
<head>
(potential memory leak)Downsides of this PR
Does this pull request introduce a breaking change?
Does this pull request introduce an observable change?
Styles are unrendered when dynamically swapped out, rather than remaining in the DOM. For instance, if a component has a
render()
function, and it switches from template A to template B, here is the behavior:SSR'ed/hydrated components are a bit different, because they use inline
<style>
s rather than shared style sheets.GUS work item
W-9789883