-
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
perf: use constructable stylesheets #2460
Conversation
const supportsConstructableStyleSheets = isFunction((CSSStyleSheet.prototype as any).replaceSync); | ||
const styleElements: { [content: string]: HTMLStyleElement } = create(null); | ||
const styleSheets: { [content: string]: CSSStyleSheet } = create(null); | ||
const nodesToStyleSheets = new WeakMap<Node, { [content: string]: true }>(); |
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 styleElements
and styleSheets
are technically a memory leak, but I don't think it's a big deal in practice. With synthetic shadow, we already don't clean up <style>
tags added to the <head>
, so we're already leaking there.
I would also prefer not to maintain a mapping of string
-> CSSStyleSheet
at all, but that would require modifying the style-compiler
, and I made a point of not doing that because I tried that in #2290 and it adds a lot of complexity.
expect(getComputedStyle(element.querySelector('div')).color).toEqual('rgb(0, 0, 0)'); | ||
expect(getComputedStyle(element.querySelector('div')).color).toEqual( | ||
'rgb(233, 150, 122)' | ||
); |
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 the thing that changed in light DOM. Basically the styles from template A remain even when template B is rendered.
I don't think this is a big deal, because it's already how synthetic shadow works – styles at the global <head>
are never cleaned up. Many CSS-in-JS libraries also work this way.
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 opened a separate issue for this; it's a general problem: #2466
|
||
if (process.env.NATIVE_SHADOW) { | ||
describe('Shadow DOM styling - multiple shadow DOM components', () => { | ||
it('Does not duplicate styles if template is re-rendered', () => { |
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 test confirms we don't end up with duplicate <style>
s/CSSStyleSheet
s in the shadow root when we render template A, then B, then A again, etc.
Arguably we could just not care about this, and then the logic would be simpler because we wouldn't have to check for existing styles. I could go either way, but I kind of like that we don't endlessly add <style>
s if (for whatever reason) the component is alternating between A and B repeatedly.
29539ee
to
2214d32
Compare
return null; | ||
} else { | ||
// native shadow or light DOM | ||
} else if (renderer.ssr) { |
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.
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 point, yes. And constructable styles for SSR have not been specced yet: WICG/webcomponents#939
I consider the tradeoff worth it given the massive perf improvement in our benchmark (~30% in dom-styled-component-create-1k-same). Plus in the real-world app I tested, it was such a big difference that it was visually noticeable (~4.3s versus ~2.1s).
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.
Sounds reasonable to me.
function getNearestNativeShadowComponent(vm: VM): VM | null { | ||
let owner: VM | null = vm; | ||
while (!isNull(owner)) { | ||
if (owner.renderMode === RenderMode.Shadow && owner.shadowMode === ShadowMode.Native) { | ||
return owner; | ||
} | ||
owner = owner.owner; | ||
} | ||
return owner; | ||
} |
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.
Would it be better performance-wise to store this on the VM instead of looking this up lazily?
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 worth testing, although we'd have to be careful about when to invalidate the cache (e.g. if the component is moved).
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.
@pmdartus I tried it (nolanlawson@87345ee), but it doesn't seem to be a statistically-significant perf improvement, at least in these benchmarks. I also worry that it introduces footguns (e.g. if we don't invalidate the cache at the right time).
There might be some benchmark that can prove an improvement (e.g. rendering many stylesheets on deeply-nested components inside a native shadow root), but I don't know how realistic this is.
Benchmark results
⠋ Auto-sample 550 (timeout in 0m0s)
┌─────────────┬─────────────────┐
│ Browser │ chrome-headless │
│ │ 92.0.4515.159 │
├─────────────┼─────────────────┤
│ Sample size │ 600 │
└─────────────┴─────────────────┘
┌─────────────────────────────────────────────────┬─────────────┬────────────┬───────────────────┬────────────────────────────────────────────────────┬────────────────────────────────────────────────────┐
│ Benchmark │ Version │ Bytes │ Avg time │ vs dom-styled-component-create-1k-same-this-change │ vs dom-styled-component-create-1k-same-tip-of-tree │
│ │ │ │ │ │ tip-of-tree │
├─────────────────────────────────────────────────┼─────────────┼────────────┼───────────────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────┤
│ dom-styled-component-create-1k-same-this-change │ <none> │ 220.48 KiB │ 52.77ms - 53.43ms │ │ unsure │
│ │ │ │ │ - │ -0% - +1% │
│ │ │ │ │ │ -0.19ms - +0.77ms │
├─────────────────────────────────────────────────┼─────────────┼────────────┼───────────────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────┤
│ dom-styled-component-create-1k-same-tip-of-tree │ tip-of-tree │ 220.18 KiB │ 52.46ms - 53.16ms │ unsure │ │
│ │ │ │ │ -1% - +0% │ - │
│ │ │ │ │ -0.77ms - +0.19ms │ │
└─────────────────────────────────────────────────┴─────────────┴────────────┴───────────────────┴────────────────────────────────────────────────────┴────────────────────────────────────────────────────┘
NOTE Hit 5 minute auto-sample timeout trying to resolve horizon(s)
⠋ Auto-sample 330 (timeout in 0m44s)
┌─────────────┬─────────────────┐
│ Browser │ chrome-headless │
│ │ 92.0.4515.159 │
├─────────────┼─────────────────┤
│ Sample size │ 380 │
└─────────────┴─────────────────┘
┌──────────────────────────────────────────────────────┬─────────────┬────────────┬─────────────────────┬─────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────┐
│ Benchmark │ Version │ Bytes │ Avg time │ vs dom-styled-component-create-1k-different-this-change │ vs dom-styled-component-create-1k-different-tip-of-tree │
│ │ │ │ │ │ tip-of-tree │
├──────────────────────────────────────────────────────┼─────────────┼────────────┼─────────────────────┼─────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ dom-styled-component-create-1k-different-this-change │ <none> │ 977.19 KiB │ 157.52ms - 159.63ms │ │ unsure │
│ │ │ │ │ - │ -1% - +1% │
│ │ │ │ │ │ -1.20ms - +1.57ms │
├──────────────────────────────────────────────────────┼─────────────┼────────────┼─────────────────────┼─────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ dom-styled-component-create-1k-different-tip-of-tree │ tip-of-tree │ 976.89 KiB │ 157.50ms - 159.29ms │ unsure │ │
│ │ │ │ │ -1% - +1% │ - │
│ │ │ │ │ -1.57ms - +1.20ms │ │
└──────────────────────────────────────────────────────┴─────────────┴────────────┴─────────────────────┴─────────────────────────────────────────────────────────┴─────────────────────────────────────────────────────────┘
(Note tip-of-tree is the nolan/constructable-stylesheets-redux
branch in this case.)
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 might be worth adding this as a comment as it is certainly something that I would ask myself when looking at this code in the future.
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.
Regarding the light DOM issue and template replacement, I think it will be even less of an issue once we land scoped styles.
packages/perf-benchmarks-components/scripts/generate-styled-components.js
Outdated
Show resolved
Hide resolved
packages/perf-benchmarks-components/scripts/generate-styled-components.js
Outdated
Show resolved
Hide resolved
|
||
const rootDir = path.join(__dirname, 'src'); | ||
const { tmpDir, styledComponents } = generateStyledComponents(); |
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 is not ideal that we have to add a one-off for a specific test suite.
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.
Agreed, but I figure in the future we may want to use similar functionality for other benchmarks. At that point, we can refactor it to generalize it.
function getNearestNativeShadowComponent(vm: VM): VM | null { | ||
let owner: VM | null = vm; | ||
while (!isNull(owner)) { | ||
if (owner.renderMode === RenderMode.Shadow && owner.shadowMode === ShadowMode.Native) { | ||
return owner; | ||
} | ||
owner = owner.owner; | ||
} | ||
return owner; | ||
} |
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 might be worth adding this as a comment as it is certainly something that I would ask myself when looking at this code in the future.
return null; | ||
} else { | ||
// native shadow or light DOM | ||
} else if (renderer.ssr) { |
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.
Sounds reasonable to me.
…ed-components.js Co-authored-by: Pierre-Marie Dartus <p.dartus@salesforce.com>
f7d2055
to
4f1707f
Compare
@jodarove @divmain Do you foresee any problems with this PR and rehydration? My assumption was:
|
for (let i = 0; i < stylesheets.length; i++) { | ||
if (isGlobal) { | ||
renderer.insertGlobalStylesheet(stylesheets[i]); | ||
} else { | ||
// local level | ||
renderer.insertStylesheet(stylesheets[i], root!.cmpRoot); | ||
} | ||
} |
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 wonder if the js engine optimizes this code, do you know @nolanlawson ?:
for (let i = 0; i < stylesheets.length; i++) { | |
if (isGlobal) { | |
renderer.insertGlobalStylesheet(stylesheets[i]); | |
} else { | |
// local level | |
renderer.insertStylesheet(stylesheets[i], root!.cmpRoot); | |
} | |
} | |
if (isGlobal) { | |
for (let i = 0; i < stylesheets.length; i++) { | |
renderer.insertGlobalStylesheet(stylesheets[i]); | |
} | |
} else { | |
for (let i = 0; i < stylesheets.length; i++) { | |
// local level | |
renderer.insertStylesheet(stylesheets[i], root!.cmpRoot); | |
} | |
} |
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.
@jodarove I admit I don't know. The isGlobal
is a constant, so I would hope the engine optimizes it?
Unfortunately my benchmark wouldn't capture it, because it only has one stylesheet per template.
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.
@jodarove The difference is pretty small (if there even is a difference – I seem to get different results in JSBench if I run multiple times). I'd say let's stick with the more readable version, especially given that most templates will have just one stylesheet.
Details
After looking into the synthetic-to-native shadow migration, it's clear to me that we need a performant solution for including the same stylesheet in multiple components (e.g.
@import 'shared.css'
). And through manual testing, I saw that constructable stylesheets really are a significant perf improvement in Chrome.This PR is a revised version of #2290 that aims to be less invasive to the codebase while still bringing perf improvements. I also added some new Tachometer benchmarks to verify the perf gain.
My goal is to improve native shadow performance without regressing synthetic shadow performance. To achieve this, we're only using constructable stylesheets in the per-component level, not at the global level. (See this doc for why that matters.)
Benchmark results
(The
ss-
ones use synthetic shadow.) Basically, the native shadow ones are massively improved (especially in Chrome), but the synthetic shadow ones are the same.Does this PR introduce breaking changes?
No, it does not introduce breaking changes.
There is a small change in how light DOM components work. Previously, if you switched templates on a light DOM component, then the old
<style>
would be removed from the DOM and replaced with the new<style>
. Now the new<style>
is merely added, while the old<style>
remains. I don't think this will be a big problem in practice.GUS work item
W-9125079