From 1898df2fad79bda5c058ad9c0d7abeeab1a784af Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 7 Sep 2021 14:31:31 -0700 Subject: [PATCH] perf: use constructable stylesheets (#2460) * perf: use constructable stylesheets * fix: update packages/perf-benchmarks-components/scripts/generate-styled-components.js Co-authored-by: Pierre-Marie Dartus * fix: respond to PR comments * fix: fix formatting Co-authored-by: Pierre-Marie Dartus --- .../engine-core/src/framework/renderer.ts | 1 + .../engine-core/src/framework/stylesheet.ts | 34 ++++++++++-- packages/@lwc/engine-dom/src/renderer.ts | 54 ++++++++++++++++++ packages/@lwc/engine-server/src/renderer.ts | 5 ++ .../multiple-templates/index.spec.js | 4 +- .../elements-are-not-recycled/index.spec.js | 10 +++- .../multiple-templates/index.spec.js | 41 ++++++++++++++ .../multiple-templates/x/multi/a.css | 3 + .../multiple-templates/x/multi/a.html | 3 + .../multiple-templates/x/multi/b.css | 3 + .../multiple-templates/x/multi/b.html | 3 + .../multiple-templates/x/multi/multi.js | 16 ++++++ .../rollup.config.js | 10 +++- .../scripts/generate-styled-components.js | 55 +++++++++++++++++++ ...component-create-1k-different.benchmark.js | 12 ++++ ...yled-component-create-1k-same.benchmark.js | 12 ++++ ...component-create-1k-different.benchmark.js | 12 ++++ ...yled-component-create-1k-same.benchmark.js | 12 ++++ .../src/utils/styledComponentBenchmark.js | 35 ++++++++++++ 19 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 packages/integration-karma/test/shadow-dom/multiple-templates/index.spec.js create mode 100644 packages/integration-karma/test/shadow-dom/multiple-templates/x/multi/a.css create mode 100644 packages/integration-karma/test/shadow-dom/multiple-templates/x/multi/a.html create mode 100644 packages/integration-karma/test/shadow-dom/multiple-templates/x/multi/b.css create mode 100644 packages/integration-karma/test/shadow-dom/multiple-templates/x/multi/b.html create mode 100644 packages/integration-karma/test/shadow-dom/multiple-templates/x/multi/multi.js create mode 100644 packages/perf-benchmarks-components/scripts/generate-styled-components.js create mode 100644 packages/perf-benchmarks/src/__benchmarks__/engine-dom/benchmark-styled-component/ss-styled-component-create-1k-different.benchmark.js create mode 100644 packages/perf-benchmarks/src/__benchmarks__/engine-dom/benchmark-styled-component/ss-styled-component-create-1k-same.benchmark.js create mode 100644 packages/perf-benchmarks/src/__benchmarks__/engine-dom/benchmark-styled-component/styled-component-create-1k-different.benchmark.js create mode 100644 packages/perf-benchmarks/src/__benchmarks__/engine-dom/benchmark-styled-component/styled-component-create-1k-same.benchmark.js create mode 100644 packages/perf-benchmarks/src/utils/styledComponentBenchmark.js diff --git a/packages/@lwc/engine-core/src/framework/renderer.ts b/packages/@lwc/engine-core/src/framework/renderer.ts index cd4d7400a8..9f24462494 100644 --- a/packages/@lwc/engine-core/src/framework/renderer.ts +++ b/packages/@lwc/engine-core/src/framework/renderer.ts @@ -46,6 +46,7 @@ export interface Renderer { getElementsByClassName(element: E, names: string): HTMLCollection; isConnected(node: N): boolean; insertGlobalStylesheet(content: string): void; + insertStylesheet(content: string, target: N): void; assertInstanceOfHTMLElement?(elm: any, msg: string): void; defineCustomElement( name: string, diff --git a/packages/@lwc/engine-core/src/framework/stylesheet.ts b/packages/@lwc/engine-core/src/framework/stylesheet.ts index 3e6d7ca68a..1c8af5028f 100644 --- a/packages/@lwc/engine-core/src/framework/stylesheet.ts +++ b/packages/@lwc/engine-core/src/framework/stylesheet.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { isArray, isUndefined, ArrayJoin, ArrayPush } from '@lwc/shared'; +import { isArray, isUndefined, ArrayJoin, ArrayPush, isNull } from '@lwc/shared'; import * as api from './api'; import { VNode } from '../3rdparty/snabbdom/types'; @@ -138,16 +138,42 @@ export function getStylesheetsContent(vm: VM, template: Template): string[] { return content; } +// It might be worth caching this to avoid doing the lookup repeatedly, but +// perf testing has not shown it to be a huge improvement yet: +// https://github.com/salesforce/lwc/pull/2460#discussion_r691208892 +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; +} + export function createStylesheet(vm: VM, stylesheets: string[]): VNode | null { const { renderer, renderMode, shadowMode } = vm; if (renderMode === RenderMode.Shadow && shadowMode === ShadowMode.Synthetic) { for (let i = 0; i < stylesheets.length; i++) { renderer.insertGlobalStylesheet(stylesheets[i]); } - return null; - } else { - // native shadow or light DOM + } else if (renderer.ssr) { + // native shadow or light DOM, SSR const combinedStylesheetContent = ArrayJoin.call(stylesheets, '\n'); return createInlineStyleVNode(combinedStylesheetContent); + } else { + // native shadow or light DOM, DOM renderer + const root = getNearestNativeShadowComponent(vm); + const isGlobal = isNull(root); + for (let i = 0; i < stylesheets.length; i++) { + if (isGlobal) { + renderer.insertGlobalStylesheet(stylesheets[i]); + } else { + // local level + renderer.insertStylesheet(stylesheets[i], root!.cmpRoot); + } + } } + return null; } diff --git a/packages/@lwc/engine-dom/src/renderer.ts b/packages/@lwc/engine-dom/src/renderer.ts index f3af21af1e..05229cfe14 100644 --- a/packages/@lwc/engine-dom/src/renderer.ts +++ b/packages/@lwc/engine-dom/src/renderer.ts @@ -10,6 +10,7 @@ import { create, hasOwnProperty, htmlPropertyToAttribute, + isFunction, isUndefined, KEY__SHADOW_TOKEN, setPrototypeOf, @@ -29,6 +30,10 @@ if (process.env.NODE_ENV === 'development') { } const globalStylesheetsParentElement: Element = document.head || document.body || document; +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(); let getCustomElement, defineCustomElement, HTMLElementConstructor; @@ -54,6 +59,46 @@ function isCustomElementRegistryAvailable() { } } +function insertConstructableStyleSheet(content: string, target: Node) { + // It's important for CSSStyleSheets to be unique based on their content, so that + // `shadowRoot.adoptedStyleSheets.includes(sheet)` works. + let styleSheet = styleSheets[content]; + if (isUndefined(styleSheet)) { + styleSheet = new CSSStyleSheet(); + (styleSheet as any).replaceSync(content); + styleSheets[content] = styleSheet; + } + if (!(target as any).adoptedStyleSheets.includes(styleSheet)) { + (target as any).adoptedStyleSheets = [...(target as any).adoptedStyleSheets, styleSheet]; + } +} + +function insertStyleElement(content: string, target: Node) { + // Avoid inserting duplicate `