Skip to content

Commit 2214d32

Browse files
committed
perf: use constructable stylesheets
1 parent 8ddcfc4 commit 2214d32

File tree

19 files changed

+315
-10
lines changed

19 files changed

+315
-10
lines changed

packages/@lwc/engine-core/src/framework/renderer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface Renderer<N = HostNode, E = HostElement> {
4646
getElementsByClassName(element: E, names: string): HTMLCollection;
4747
isConnected(node: N): boolean;
4848
insertGlobalStylesheet(content: string): void;
49+
insertStylesheet(content: string, target: N): void;
4950
assertInstanceOfHTMLElement?(elm: any, msg: string): void;
5051
defineCustomElement(
5152
name: string,

packages/@lwc/engine-core/src/framework/stylesheet.ts

+27-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: MIT
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
66
*/
7-
import { isArray, isUndefined, ArrayJoin, ArrayPush } from '@lwc/shared';
7+
import { isArray, isUndefined, ArrayJoin, ArrayPush, isNull } from '@lwc/shared';
88

99
import * as api from './api';
1010
import { VNode } from '../3rdparty/snabbdom/types';
@@ -138,16 +138,39 @@ export function getStylesheetsContent(vm: VM, template: Template): string[] {
138138
return content;
139139
}
140140

141+
function getNearestNativeShadowComponent(vm: VM): VM | null {
142+
let owner: VM | null = vm;
143+
while (!isNull(owner)) {
144+
if (owner.renderMode === RenderMode.Shadow && owner.shadowMode === ShadowMode.Native) {
145+
return owner;
146+
}
147+
owner = owner.owner;
148+
}
149+
return owner;
150+
}
151+
141152
export function createStylesheet(vm: VM, stylesheets: string[]): VNode | null {
142153
const { renderer, renderMode, shadowMode } = vm;
143154
if (renderMode === RenderMode.Shadow && shadowMode === ShadowMode.Synthetic) {
144155
for (let i = 0; i < stylesheets.length; i++) {
145156
renderer.insertGlobalStylesheet(stylesheets[i]);
146157
}
147-
return null;
148-
} else {
149-
// native shadow or light DOM
158+
} else if (renderer.ssr) {
159+
// native shadow or light DOM, SSR
150160
const combinedStylesheetContent = ArrayJoin.call(stylesheets, '\n');
151161
return createInlineStyleVNode(combinedStylesheetContent);
162+
} else {
163+
// native shadow or light DOM, DOM renderer
164+
const root = getNearestNativeShadowComponent(vm);
165+
const isGlobal = isNull(root);
166+
for (let i = 0; i < stylesheets.length; i++) {
167+
if (isGlobal) {
168+
renderer.insertGlobalStylesheet(stylesheets[i]);
169+
} else {
170+
// local level
171+
renderer.insertStylesheet(stylesheets[i], root!.cmpRoot);
172+
}
173+
}
152174
}
175+
return null;
153176
}

packages/@lwc/engine-dom/src/renderer.ts

+54
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
create,
1111
hasOwnProperty,
1212
htmlPropertyToAttribute,
13+
isFunction,
1314
isUndefined,
1415
KEY__SHADOW_TOKEN,
1516
setPrototypeOf,
@@ -29,6 +30,10 @@ if (process.env.NODE_ENV === 'development') {
2930
}
3031

3132
const globalStylesheetsParentElement: Element = document.head || document.body || document;
33+
const supportsConstructableStyleSheets = isFunction((CSSStyleSheet.prototype as any).replaceSync);
34+
const styleElements: { [content: string]: HTMLStyleElement } = create(null);
35+
const styleSheets: { [content: string]: CSSStyleSheet } = create(null);
36+
const nodesToStyleSheets = new WeakMap<Node, { [content: string]: true }>();
3237

3338
let getCustomElement, defineCustomElement, HTMLElementConstructor;
3439

@@ -54,6 +59,46 @@ function isCustomElementRegistryAvailable() {
5459
}
5560
}
5661

62+
function insertConstructableStyleSheet(content: string, target: Node) {
63+
// It's important for CSSStyleSheets to be unique based on their content, so that
64+
// `shadowRoot.adoptedStyleSheets.includes(sheet)` works.
65+
let styleSheet = styleSheets[content];
66+
if (isUndefined(styleSheet)) {
67+
styleSheet = new CSSStyleSheet();
68+
(styleSheet as any).replaceSync(content);
69+
styleSheets[content] = styleSheet;
70+
}
71+
if (!(target as any).adoptedStyleSheets.includes(styleSheet)) {
72+
(target as any).adoptedStyleSheets = [...(target as any).adoptedStyleSheets, styleSheet];
73+
}
74+
}
75+
76+
function insertStyleElement(content: string, target: Node) {
77+
// Avoid inserting duplicate `<style>`s
78+
let sheets = nodesToStyleSheets.get(target);
79+
if (isUndefined(sheets)) {
80+
sheets = create(null);
81+
nodesToStyleSheets.set(target, sheets!);
82+
}
83+
if (sheets![content]) {
84+
return;
85+
}
86+
sheets![content] = true;
87+
88+
// This `<style>` may be repeated multiple times in the DOM, so cache it. It's a bit
89+
// faster to call `cloneNode()` on an existing node than to recreate it every time.
90+
let elm = styleElements[content];
91+
if (isUndefined(elm)) {
92+
elm = document.createElement('style');
93+
elm.type = 'text/css';
94+
elm.textContent = content;
95+
styleElements[content] = elm;
96+
} else {
97+
elm = elm.cloneNode(true) as HTMLStyleElement;
98+
}
99+
target.appendChild(elm);
100+
}
101+
57102
if (isCustomElementRegistryAvailable()) {
58103
getCustomElement = customElements.get.bind(customElements);
59104
defineCustomElement = customElements.define.bind(customElements);
@@ -244,6 +289,15 @@ export const renderer: Renderer<Node, Element> = {
244289
globalStylesheetsParentElement.appendChild(elm);
245290
},
246291

292+
insertStylesheet(content: string, target: Node): void {
293+
if (supportsConstructableStyleSheets) {
294+
insertConstructableStyleSheet(content, target);
295+
} else {
296+
// Fall back to <style> element
297+
insertStyleElement(content, target);
298+
}
299+
},
300+
247301
assertInstanceOfHTMLElement(elm: any, msg: string) {
248302
assert.invariant(elm instanceof HTMLElement, msg);
249303
},

packages/@lwc/engine-server/src/renderer.ts

+5
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,11 @@ export const renderer: Renderer<HostNode, HostElement> = {
299299
// synthetic shadow.
300300
},
301301

302+
insertStylesheet() {
303+
// Noop on SSR (for now). This need to be reevaluated whenever we will implement support for
304+
// synthetic shadow.
305+
},
306+
302307
addEventListener() {
303308
// Noop on SSR.
304309
},

packages/integration-karma/test/light-dom/multiple-templates/index.spec.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ describe('multiple templates', () => {
2323
element.next();
2424
return Promise.resolve().then(() => {
2525
expect(element.querySelector('div').textContent).toEqual('b');
26-
expect(getComputedStyle(element.querySelector('div')).color).toEqual('rgb(0, 0, 0)');
26+
expect(getComputedStyle(element.querySelector('div')).color).toEqual(
27+
'rgb(233, 150, 122)'
28+
);
2729
expect(getComputedStyle(element.querySelector('div')).marginLeft).toEqual('10px');
2830
// element should not be dirty after template change
2931
expect(element.querySelector('div').hasAttribute('foo')).toEqual(false);

packages/integration-karma/test/rendering/elements-are-not-recycled/index.spec.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,14 @@ if (process.env.NATIVE_SHADOW) {
101101
document.body.appendChild(elm);
102102

103103
return Promise.resolve().then(() => {
104-
const styles = Array.from(elm.shadowRoot.querySelectorAll('x-simple')).map((xSimple) =>
105-
xSimple.shadowRoot.querySelector('style')
104+
const styles = Array.from(elm.shadowRoot.querySelectorAll('x-simple')).map(
105+
(xSimple) => {
106+
// if constructable stylesheets are supported, return that rather than <style> tags
107+
return (
108+
xSimple.shadowRoot.adoptedStyleSheets ||
109+
xSimple.shadowRoot.querySelector('style')
110+
);
111+
}
106112
);
107113

108114
expect(styles[0]).toBeTruthy();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createElement } from 'lwc';
2+
import Multi from 'x/multi';
3+
4+
if (process.env.NATIVE_SHADOW) {
5+
describe('Shadow DOM styling - multiple shadow DOM components', () => {
6+
it('Does not duplicate styles if template is re-rendered', () => {
7+
const element = createElement('x-multi', { is: Multi });
8+
9+
const getNumStyleSheets = () => {
10+
if (element.shadowRoot.adoptedStyleSheets) {
11+
return element.shadowRoot.adoptedStyleSheets.length;
12+
} else {
13+
return element.shadowRoot.querySelectorAll('style').length;
14+
}
15+
};
16+
17+
document.body.appendChild(element);
18+
return Promise.resolve()
19+
.then(() => {
20+
expect(getComputedStyle(element.shadowRoot.querySelector('div')).color).toEqual(
21+
'rgb(0, 0, 255)'
22+
);
23+
expect(getNumStyleSheets()).toEqual(1);
24+
element.next();
25+
})
26+
.then(() => {
27+
expect(getComputedStyle(element.shadowRoot.querySelector('div')).color).toEqual(
28+
'rgb(255, 0, 0)'
29+
);
30+
expect(getNumStyleSheets()).toEqual(2);
31+
element.next();
32+
})
33+
.then(() => {
34+
expect(getComputedStyle(element.shadowRoot.querySelector('div')).color).toEqual(
35+
'rgb(0, 0, 255)'
36+
);
37+
expect(getNumStyleSheets()).toEqual(2);
38+
});
39+
});
40+
});
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.blue {
2+
color: blue;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<div class="blue">a</div>
3+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.red {
2+
color: red;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<div class="red">b</div>
3+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { LightningElement, api } from 'lwc';
2+
import A from './a.html';
3+
import B from './b.html';
4+
5+
export default class Multi extends LightningElement {
6+
current = A;
7+
8+
@api
9+
next() {
10+
this.current = this.current === A ? B : A;
11+
}
12+
13+
render() {
14+
return this.current;
15+
}
16+
}

packages/perf-benchmarks-components/rollup.config.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@ import path from 'path';
99
import glob from 'glob';
1010
import lwc from '@lwc/rollup-plugin';
1111
import replace from '@rollup/plugin-replace';
12+
import { generateStyledComponents } from './scripts/generate-styled-components';
1213

13-
const rootDir = path.join(__dirname, 'src');
14+
const { tmpDir, styledComponents } = generateStyledComponents();
1415

1516
function createConfig(componentFile, engineType) {
17+
const rootDir = componentFile.includes(tmpDir)
18+
? path.join(tmpDir, 'src')
19+
: path.join(__dirname, 'src');
1620
const lwcImportModule = engineType === 'server' ? '@lwc/engine-server' : '@lwc/engine-dom';
1721
return {
1822
input: componentFile,
@@ -34,7 +38,7 @@ function createConfig(componentFile, engineType) {
3438
}),
3539
],
3640
output: {
37-
file: componentFile.replace('/src/', `/dist/${engineType}/`),
41+
file: componentFile.replace(tmpDir, __dirname).replace('/src/', `/dist/${engineType}/`),
3842
format: 'esm',
3943
},
4044
// These packages need to be external so that perf-benchmarks can potentially swap them out
@@ -43,7 +47,7 @@ function createConfig(componentFile, engineType) {
4347
};
4448
}
4549

46-
const components = glob.sync(path.join(__dirname, 'src/**/*.js'));
50+
const components = [...glob.sync(path.join(__dirname, 'src/**/*.js')), ...styledComponents];
4751

4852
const config = ['server', 'dom']
4953
.map((engineType) => components.map((component) => createConfig(component, engineType)))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2018, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
8+
import path from 'path';
9+
import fs from 'fs';
10+
import os from 'os';
11+
import mkdirp from 'mkdirp';
12+
13+
const NUM_COMPONENTS = 1000;
14+
15+
// Generates some components with individual CSS for each one.
16+
// We could use @rollup/plugin-virtual for this, but @lwc/rollup-plugin deliberately
17+
// filters virtual modules, so it's simpler to just write to a temp dir.
18+
export function generateStyledComponents() {
19+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lwc-'));
20+
21+
const components = Array(NUM_COMPONENTS)
22+
.fill()
23+
.map((_, i) =>
24+
path.join(tmpDir, `src/benchmark/styledComponent${i}/styledComponent${i}.js`)
25+
);
26+
27+
components.forEach((jsFilename, i) => {
28+
const cssFilename = jsFilename.replace('.js', '.css');
29+
const htmlFilename = jsFilename.replace('.js', '.html');
30+
31+
const js =
32+
'import { LightningElement } from "lwc"; export default class extends LightningElement {}';
33+
const css = `div { color: ${i.toString(16).padStart(6, '0')}}`;
34+
const html = '<template><div></div></template>';
35+
36+
mkdirp.sync(path.dirname(jsFilename));
37+
fs.writeFileSync(jsFilename, js, 'utf-8');
38+
fs.writeFileSync(cssFilename, css, 'utf-8');
39+
fs.writeFileSync(htmlFilename, html, 'utf-8');
40+
});
41+
42+
const oneComponentFilename = path.join(tmpDir, 'src/benchmark/styledComponent.js');
43+
const oneComponent = `export { default } from ${JSON.stringify(components[0])};`;
44+
fs.writeFileSync(oneComponentFilename, oneComponent, 'utf-8');
45+
46+
const allComponentsFilename = path.join(tmpDir, 'src/benchmark/styledComponents.js');
47+
const allComponents = `
48+
${components.map((mod, i) => `import cmp${i} from ${JSON.stringify(mod)}`).join(';')};
49+
const modules = [${components.map((_, i) => `cmp${i}`).join(',')}];
50+
export default modules;`;
51+
fs.writeFileSync(allComponentsFilename, allComponents, 'utf-8');
52+
53+
return {
54+
styledComponents: [oneComponentFilename, allComponentsFilename],
55+
tmpDir,
56+
};
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright (c) 2018, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
8+
import components from 'perf-benchmarks-components/dist/dom/benchmark/styledComponents.js';
9+
import { styledComponentBenchmark } from '../../../utils/styledComponentBenchmark';
10+
11+
// Create 1k components with different CSS in each component
12+
styledComponentBenchmark(`ss-benchmark-styled-component/create/1k/different`, components);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright (c) 2018, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
8+
import StyledComponent from 'perf-benchmarks-components/dist/dom/benchmark/styledComponent.js';
9+
import { styledComponentBenchmark } from '../../../utils/styledComponentBenchmark';
10+
11+
// Create 1k components with the same CSS in each component
12+
styledComponentBenchmark(`ss-benchmark-styled-component/create/1k/same`, StyledComponent);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright (c) 2018, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
8+
import components from 'perf-benchmarks-components/dist/dom/benchmark/styledComponents.js';
9+
import { styledComponentBenchmark } from '../../../utils/styledComponentBenchmark';
10+
11+
// Create 1k components with different CSS in each component
12+
styledComponentBenchmark(`benchmark-styled-component/create/1k/different`, components);

0 commit comments

Comments
 (0)