Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0e3058a
fix(engine-dom): remove stylesheets when unused
nolanlawson Apr 18, 2022
d276c7e
fix: remove unnecessary change
nolanlawson Apr 19, 2022
944a55d
test: add more tests
nolanlawson Apr 19, 2022
d6bcd59
test: add hydration test, fix unrendering styles
nolanlawson Apr 26, 2022
88f7edd
test: more tests
nolanlawson Apr 27, 2022
49953e7
test: re-enable tests
nolanlawson Apr 27, 2022
7f9afd9
refactor: refactor
nolanlawson Apr 27, 2022
8b5feab
refactor: refactor
nolanlawson Apr 27, 2022
14b97f8
test: fix test in ie11
nolanlawson Apr 27, 2022
d2561cc
test: fix ie11 again
nolanlawson Apr 27, 2022
a442aa5
fix: headers
nolanlawson Apr 27, 2022
6bbd4af
Revert "test: fix ie11 again"
nolanlawson Apr 27, 2022
435b290
test: actually fix ie11, rename api to avoid ambiguity
nolanlawson Apr 27, 2022
b5fa193
fix: add comment
nolanlawson Apr 27, 2022
4a012c1
refactor: refactor part one
nolanlawson Apr 28, 2022
29424ff
refactor: refactor part 2
nolanlawson Apr 28, 2022
53f68e9
refactor: refactor part 3
nolanlawson Apr 28, 2022
d69c65c
refactor: refactor part 4
nolanlawson Apr 28, 2022
79ecf74
fix: fix comment
nolanlawson Apr 28, 2022
6e1e70b
fix: fix comment
nolanlawson Apr 28, 2022
8d918b3
fix: work around ie11 bug
nolanlawson Apr 28, 2022
dab7e56
refactor: refactor
nolanlawson Apr 28, 2022
5a5e97b
fix: fix for race condition
nolanlawson Apr 28, 2022
f6479ea
fix: another undefined case
nolanlawson Apr 28, 2022
9c6d33e
fix: remove redundant typescript types
nolanlawson Apr 28, 2022
202be37
fix: add feature flag to disable style removal
nolanlawson Apr 28, 2022
0ee12bf
fix: switch to Maps instead of objects
nolanlawson Apr 28, 2022
c5abb57
test: fix test
nolanlawson Apr 28, 2022
44a6132
test: fix test
nolanlawson Apr 28, 2022
0bfb8b6
fix: remove flag for now
nolanlawson Apr 28, 2022
4882534
test: remove <head> cleanup from tests
nolanlawson Apr 29, 2022
1a026c3
Revert "fix: remove flag for now"
nolanlawson Apr 29, 2022
25f005f
fix: try to fix hydration and style removal
nolanlawson Apr 29, 2022
e78ad66
Revert "Revert "fix: remove flag for now""
nolanlawson Apr 29, 2022
52a5309
fix: remove unnecessary test util
nolanlawson Apr 29, 2022
eb677d4
refactor: make code DRYer
nolanlawson Apr 29, 2022
f6ad82f
refactor: refactor test
nolanlawson Apr 29, 2022
88bbb3c
refactor: refactor test
nolanlawson Apr 29, 2022
dbb1870
chore: empty commit to poke CI
nolanlawson Apr 29, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@lwc/engine-core/src/framework/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ function hydrateCustomElement(elm: Node, vnode: VCustomElement): Node | null {
mode,
owner,
tagName: sel,
hydrated: true,
});

vnode.elm = elm;
Expand Down
3 changes: 1 addition & 2 deletions packages/@lwc/engine-core/src/framework/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@ export {
setGetProperty,
setHTMLElement,
setInsert,
setInsertGlobalStylesheet,
setInsertStylesheet,
setIsConnected,
setIsHydrating,
setIsNativeShadowDefined,
Expand All @@ -97,4 +95,5 @@ export {
setSetText,
setSsr,
setAddEventListener,
setToggleStyleSheet,
} from '../renderer';
41 changes: 20 additions & 21 deletions packages/@lwc/engine-core/src/framework/stylesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,7 @@
*/
import { ArrayJoin, ArrayPush, isArray, isNull, isUndefined, KEY__SCOPED_CSS } from '@lwc/shared';

import {
getClassList,
removeAttribute,
setAttribute,
insertGlobalStylesheet,
ssr,
isHydrating,
insertStylesheet,
} from '../renderer';
import { getClassList, removeAttribute, setAttribute, ssr, toggleStyleSheet } from '../renderer';

import api from './api';
import { RenderMode, ShadowMode, VM } from './vm';
Expand Down Expand Up @@ -203,32 +195,39 @@ function getNearestNativeShadowComponent(vm: VM): VM | null {
return owner;
}

export function createStylesheet(vm: VM, stylesheets: string[]): VNode | null {
function createOrRemoveStylesheet(vm: VM, stylesheets: string[], insert: false): null;
function createOrRemoveStylesheet(vm: VM, stylesheets: string[], insert: true): VNode | null;
function createOrRemoveStylesheet(vm: VM, stylesheets: string[], insert: boolean) {
const { renderMode, shadowMode } = vm;
if (renderMode === RenderMode.Shadow && shadowMode === ShadowMode.Synthetic) {
for (let i = 0; i < stylesheets.length; i++) {
insertGlobalStylesheet(stylesheets[i]);
toggleStyleSheet(stylesheets[i], insert);
}
} else if (ssr || isHydrating()) {
} else if (ssr || vm.hydrated) {
Copy link
Contributor Author

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.

// Note: We need to ensure that during hydration, the stylesheets method is the same as those in ssr.
// This works in the client, because the stylesheets are created, and cached in the VM
// the first time the VM renders.

// native shadow or light DOM, SSR
const combinedStylesheetContent = ArrayJoin.call(stylesheets, '\n');
return createInlineStyleVNode(combinedStylesheetContent);
if (insert) {
const combinedStylesheetContent = ArrayJoin.call(stylesheets, '\n');
return createInlineStyleVNode(combinedStylesheetContent);
}
// If it's being removed, we don't need to do anything. The vdom diffing will take care of it.
} 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) {
insertGlobalStylesheet(stylesheets[i]);
} else {
// local level
insertStylesheet(stylesheets[i], root!.shadowRoot!);
}
toggleStyleSheet(stylesheets[i], insert, root?.shadowRoot ?? undefined);
}
}
return null;
}

export function createStylesheet(vm: VM, stylesheets: string[]): VNode | null {
return createOrRemoveStylesheet(vm, stylesheets, true);
}

export function removeStylesheet(vm: VM, stylesheets: string[]) {
createOrRemoveStylesheet(vm, stylesheets, false);
}
25 changes: 24 additions & 1 deletion packages/@lwc/engine-core/src/framework/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { connectWireAdapters, disconnectWireAdapters, installWireAdapters } from
import { AccessorReactiveObserver } from './decorators/api';
import { removeActiveVM } from './hot-swaps';
import { VNodes, VCustomElement, VNode, VNodeType } from './vnodes';
import { getStylesheetsContent, removeStylesheet } from './stylesheet';

import type { HostNode, HostElement } from '../renderer';

Expand Down Expand Up @@ -111,6 +112,8 @@ export interface VM<N = HostNode, E = HostElement> {
readonly context: Context;
/** The owner VM or null for root elements. */
readonly owner: VM<N, E> | null;
/** Whether or not the VM was hydrated */
readonly hydrated: boolean;
/** Rendering operations associated with the VM */
renderMode: RenderMode;
shadowMode: ShadowMode;
Expand Down Expand Up @@ -217,6 +220,15 @@ export function appendVM(vm: VM) {
rehydrate(vm);
}

function removeStyles(vm: VM) {
// Remove any styles used by this template when no longer needed
const { cmpTemplate } = vm;
if (!isNull(cmpTemplate)) {
const stylesheets = getStylesheetsContent(vm, cmpTemplate);
removeStylesheet(vm, stylesheets);
}
}

// just in case the component comes back, with this we guarantee re-rendering it
// while preventing any attempt to rehydration until after reinsertion.
function resetComponentStateWhenRemoved(vm: VM) {
Expand All @@ -234,6 +246,7 @@ function resetComponentStateWhenRemoved(vm: VM) {
// Spec: https://dom.spec.whatwg.org/#concept-node-remove (step 14-15)
runChildNodesDisconnectedCallback(vm);
runLightChildNodesDisconnectedCallback(vm);
removeStyles(vm);
}

if (process.env.NODE_ENV !== 'production') {
Expand Down Expand Up @@ -268,9 +281,10 @@ export function createVM<HostNode, HostElement>(
mode: ShadowRootMode;
owner: VM<HostNode, HostElement> | null;
tagName: string;
hydrated?: boolean;
}
): VM {
const { mode, owner, tagName } = options;
const { mode, owner, tagName, hydrated } = options;
const def = getComponentInternalDef(ctor);

const vm: VM = {
Expand All @@ -291,6 +305,7 @@ export function createVM<HostNode, HostElement>(
cmpSlots: create(null),
oar: create(null),
cmpTemplate: null,
hydrated: Boolean(hydrated),

renderMode: def.renderMode,

Expand Down Expand Up @@ -658,6 +673,14 @@ export function resetComponentRoot(vm: VM) {

runChildNodesDisconnectedCallback(vm);
vm.velements = EmptyArray;

// If the component was hydrated, then we shouldn't remove the stylesheets
// because it didn't actually insert any shared stylesheets – hydrated components
// use inline <styles>. Removing the styles here would cause the stylesheet
// count to become negative.
if (!vm.hydrated) {
removeStyles(vm);
}
}

export function scheduleRehydration(vm: VM) {
Expand Down
15 changes: 4 additions & 11 deletions packages/@lwc/engine-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,17 +245,10 @@ export let isConnected: isConnectedFunc;
export function setIsConnected(isConnectedImpl: isConnectedFunc) {
isConnected = isConnectedImpl;
}

type insertGlobalStylesheetFunc = (content: string) => void;
export let insertGlobalStylesheet: insertGlobalStylesheetFunc;
export function setInsertGlobalStylesheet(insertGlobalStylesheetImpl: insertGlobalStylesheetFunc) {
insertGlobalStylesheet = insertGlobalStylesheetImpl;
}

type insertStylesheetFunc = (content: string, target: ShadowRoot) => void;
export let insertStylesheet: insertStylesheetFunc;
export function setInsertStylesheet(insertStylesheetImpl: insertStylesheetFunc) {
insertStylesheet = insertStylesheetImpl;
type toggleStyleSheetFunc = (content: string, insert: boolean, target?: ShadowRoot) => void;
export let toggleStyleSheet: toggleStyleSheetFunc;
export function setToggleStyleSheet(toggleStyleSheetImpl: toggleStyleSheetFunc) {
toggleStyleSheet = toggleStyleSheetImpl;
}

type assertInstanceOfHTMLElementFunc = (elm: any, msg: string) => void;
Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/engine-dom/src/apis/hydrate-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function createVMWithProps(element: Element, Ctor: typeof LightningElement, prop
mode: 'open',
owner: null,
tagName: element.tagName.toLowerCase(),
hydrated: true,
});

for (const [key, value] of Object.entries(props)) {
Expand Down
9 changes: 3 additions & 6 deletions packages/@lwc/engine-dom/src/initializeRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import {
setGetProperty,
setHTMLElement,
setInsert,
setInsertGlobalStylesheet,
setInsertStylesheet,
setIsConnected,
setIsHydrating,
setIsNativeShadowDefined,
Expand All @@ -46,6 +44,7 @@ import {
setSetText,
setSsr,
setAddEventListener,
setToggleStyleSheet,
} from '@lwc/engine-core';

import {
Expand All @@ -71,8 +70,6 @@ import {
getProperty,
HTMLElement,
insert,
insertGlobalStylesheet,
insertStylesheet,
isConnected,
isHydrating,
isNativeShadowDefined,
Expand All @@ -89,6 +86,7 @@ import {
setText,
ssr,
addEventListener,
toggleStyleSheet,
} from './renderer';

setAssertInstanceOfHTMLElement(assertInstanceOfHTMLElement);
Expand All @@ -113,8 +111,6 @@ setGetLastElementChild(getLastElementChild);
setGetProperty(getProperty);
setHTMLElement(HTMLElement);
setInsert(insert);
setInsertGlobalStylesheet(insertGlobalStylesheet);
setInsertStylesheet(insertStylesheet);
setIsConnected(isConnected);
setIsHydrating(isHydrating);
setIsNativeShadowDefined(isNativeShadowDefined);
Expand All @@ -131,3 +127,4 @@ setSetProperty(setProperty);
setSetText(setText);
setSsr(ssr);
setAddEventListener(addEventListener);
setToggleStyleSheet(toggleStyleSheet);
98 changes: 1 addition & 97 deletions packages/@lwc/engine-dom/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,13 @@ import {
hasOwnProperty,
htmlPropertyToAttribute,
globalThis,
isFunction,
isUndefined,
isArray,
KEY__IS_NATIVE_SHADOW_ROOT_DEFINED,
KEY__SHADOW_TOKEN,
setPrototypeOf,
StringToLowerCase,
getOwnPropertyDescriptor,
} from '@lwc/shared';

const globalStylesheets: { [content: string]: true } = create(null);

if (process.env.NODE_ENV === 'development') {
// @ts-ignore
window.__lwcResetGlobalStylesheets = () => {
for (const key of Object.keys(globalStylesheets)) {
delete globalStylesheets[key];
}
};
}

const globalStylesheetsParentElement: Element = document.head || document.body || document;
// This check for constructable stylesheets is similar to Fast's:
// https://github.com/microsoft/fast/blob/d49d1ec/packages/web-components/fast-element/src/dom.ts#L51-L53
// See also: https://github.com/whatwg/webidl/issues/1027#issuecomment-934510070
const supportsConstructableStyleSheets =
isFunction(CSSStyleSheet.prototype.replaceSync) && isArray(document.adoptedStyleSheets);
const supportsMutableAdoptedStyleSheets =
supportsConstructableStyleSheets &&
getOwnPropertyDescriptor(document.adoptedStyleSheets, 'length')!.writable;
const styleElements: { [content: string]: HTMLStyleElement } = create(null);
const styleSheets: { [content: string]: CSSStyleSheet } = create(null);
const shadowRootsToStyleSheets = new WeakMap<ShadowRoot, { [content: string]: true }>();
export { toggleStyleSheet } from './styles';

export let getCustomElement: any;
export let defineCustomElement: any;
Expand Down Expand Up @@ -72,53 +46,6 @@ function isCustomElementRegistryAvailable() {
}
}

function insertConstructableStyleSheet(content: string, target: ShadowRoot) {
// 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.replaceSync(content);
styleSheets[content] = styleSheet;
}
const { adoptedStyleSheets } = target;
if (!adoptedStyleSheets.includes(styleSheet)) {
if (supportsMutableAdoptedStyleSheets) {
// This is only supported in later versions of Chromium:
// https://chromestatus.com/feature/5638996492288000
adoptedStyleSheets.push(styleSheet);
} else {
target.adoptedStyleSheets = [...adoptedStyleSheets, styleSheet];
}
}
}

function insertStyleElement(content: string, target: ShadowRoot) {
// Avoid inserting duplicate `<style>`s
let sheets = shadowRootsToStyleSheets.get(target);
if (isUndefined(sheets)) {
sheets = create(null);
shadowRootsToStyleSheets.set(target, sheets!);
}
if (sheets![content]) {
return;
}
sheets![content] = true;

// This `<style>` may be repeated multiple times in the DOM, so cache it. It's a bit
// faster to call `cloneNode()` on an existing node than to recreate it every time.
let elm = styleElements[content];
if (isUndefined(elm)) {
elm = document.createElement('style');
elm.type = 'text/css';
elm.textContent = content;
styleElements[content] = elm;
} else {
elm = elm.cloneNode(true) as HTMLStyleElement;
}
target.appendChild(elm);
}

if (isCustomElementRegistryAvailable()) {
getCustomElement = customElements.get.bind(customElements);
defineCustomElement = customElements.define.bind(customElements);
Expand Down Expand Up @@ -349,29 +276,6 @@ export function isConnected(node: Node): boolean {
return node.isConnected;
}

export function insertGlobalStylesheet(content: string): void {
if (!isUndefined(globalStylesheets[content])) {
return;
}

globalStylesheets[content] = true;

const elm = document.createElement('style');
elm.type = 'text/css';
elm.textContent = content;

globalStylesheetsParentElement.appendChild(elm);
}

export function insertStylesheet(content: string, target: ShadowRoot): void {
if (supportsConstructableStyleSheets) {
insertConstructableStyleSheet(content, target);
} else {
// Fall back to <style> element
insertStyleElement(content, target);
}
}

export function assertInstanceOfHTMLElement(elm: any, msg: string) {
assert.invariant(elm instanceof HTMLElement, msg);
}
Expand Down
Loading