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

perf: use constructable stylesheets #2460

Merged
merged 4 commits into from
Sep 7, 2021

Conversation

nolanlawson
Copy link
Collaborator

@nolanlawson nolanlawson commented Aug 17, 2021

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
Benchmark Browser Before After Delta
dom-ss-styled-component-create-1k-different chrome 334ms-337ms 335ms-338ms -0.2% - 0.8% (~same)
dom-ss-styled-component-create-1k-different firefox 296ms-298ms 297ms-299ms -0.2% - 0.9% (~same)
dom-ss-styled-component-create-1k-different safari 161ms-163ms 161ms-163ms -0.8% - 0.9% (~same)
dom-ss-styled-component-create-1k-same chrome 105ms-107ms 105ms-107ms -0.6% - 1.0% (~same)
dom-ss-styled-component-create-1k-same firefox 87ms-89ms 87ms-89ms -1.0% - 0.9% (~same)
dom-ss-styled-component-create-1k-same safari 54ms-55ms 54ms-54ms -0.9% - 0.9% (~same)
dom-styled-component-create-1k-different chrome 330ms-332ms 294ms-295ms -11.5% - -10.6% (faster)
dom-styled-component-create-1k-different firefox 182ms-187ms 173ms-177ms -7.0% - -3.6% (faster)
dom-styled-component-create-1k-different safari 150ms-158ms 146ms-149ms -6.8% - -1.7% (faster)
dom-styled-component-create-1k-same chrome 148ms-150ms 101ms-103ms -32.0% - -30.9% (faster)
dom-styled-component-create-1k-same firefox 99ms-103ms 83ms-87ms -18.6% - -13.5% (faster)
dom-styled-component-create-1k-same safari 63ms-64ms 55ms-56ms -13.4% - -11.5% (faster)

(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

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 }>();
Copy link
Collaborator Author

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)'
);
Copy link
Collaborator Author

@nolanlawson nolanlawson Aug 17, 2021

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.

Copy link
Collaborator Author

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', () => {
Copy link
Collaborator Author

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/CSSStyleSheets 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.

@nolanlawson nolanlawson force-pushed the nolan/constructable-stylesheets-redux branch from 29539ee to 2214d32 Compare August 17, 2021 20:55
return null;
} else {
// native shadow or light DOM
} else if (renderer.ssr) {
Copy link
Member

Choose a reason for hiding this comment

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

Side note: It will be more complex to do rehydration if SSR produces a different HTML structure than client-side rendering. @jodarove @divmain

Copy link
Collaborator Author

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).

Copy link
Member

Choose a reason for hiding this comment

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

Sounds reasonable to me.

Comment on lines +141 to +153
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;
}
Copy link
Member

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?

Copy link
Collaborator Author

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).

Copy link
Collaborator Author

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.)

Copy link
Member

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.

Copy link
Member

@pmdartus pmdartus left a 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.


const rootDir = path.join(__dirname, 'src');
const { tmpDir, styledComponents } = generateStyledComponents();
Copy link
Member

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.

Copy link
Collaborator Author

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.

Comment on lines +141 to +153
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;
}
Copy link
Member

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) {
Copy link
Member

Choose a reason for hiding this comment

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

Sounds reasonable to me.

@pmdartus pmdartus requested review from jodarove and divmain August 23, 2021 15:53
@pmdartus
Copy link
Member

I am also adding @jodarove and @divmain to the review to make sure this change can align with rehydration.

@nolanlawson nolanlawson force-pushed the nolan/constructable-stylesheets-redux branch from f7d2055 to 4f1707f Compare August 23, 2021 16:46
@nolanlawson
Copy link
Collaborator Author

@jodarove @divmain Do you foresee any problems with this PR and rehydration? My assumption was:

  1. SSR'd content would use regular <style>s (until declarative constructable stylesheets ship)
  2. During hydration, the client will remove the <style> and replace it with a constructable stylesheet (or keep it, I dunno, depends on perf/FOUC implications).

Comment on lines +169 to +176
for (let i = 0; i < stylesheets.length; i++) {
if (isGlobal) {
renderer.insertGlobalStylesheet(stylesheets[i]);
} else {
// local level
renderer.insertStylesheet(stylesheets[i], root!.cmpRoot);
}
}
Copy link
Contributor

@jodarove jodarove Sep 1, 2021

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 ?:

Suggested change
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);
}
}

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

Screen Shot 2021-09-01 at 10 10 54 AM

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.

3 participants