Skip to content

Commit c828bfe

Browse files
jodarovepmdartusekashidacaridynolanlawson
authored
perf: special handling for static elements (#2781)
Co-authored-by: Pierre-Marie Dartus <p.dartus@salesforce.com> Co-authored-by: Eugene Kashida <ekashida@gmail.com> Co-authored-by: Caridy Patino <caridy@gmail.com> Co-authored-by: Nolan Lawson <nlawson@salesforce.com>
1 parent cc0cd60 commit c828bfe

File tree

139 files changed

+1533
-1475
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

139 files changed

+1533
-1475
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import {
4141
VComment,
4242
VElementData,
4343
VNodeType,
44+
VStatic,
45+
Key,
4446
} from './vnodes';
4547

4648
const SymbolIterator: typeof Symbol.iterator = Symbol.iterator;
@@ -49,6 +51,18 @@ function addVNodeToChildLWC(vnode: VCustomElement) {
4951
ArrayPush.call(getVMBeingRendered()!.velements, vnode);
5052
}
5153

54+
// [st]atic node
55+
function st(fragment: Element, key: Key): VStatic {
56+
return {
57+
type: VNodeType.Static,
58+
sel: undefined,
59+
key,
60+
elm: undefined,
61+
fragment,
62+
owner: getVMBeingRendered()!,
63+
};
64+
}
65+
5266
// [h]tml node
5367
function h(sel: string, data: VElementData, children: VNodes = EmptyArray): VElement {
5468
const vmBeingRendered = getVMBeingRendered()!;
@@ -546,6 +560,7 @@ const api = ObjectFreeze({
546560
co,
547561
dc,
548562
ti,
563+
st,
549564
gid,
550565
fid,
551566
shc,

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
VComment,
3030
VElement,
3131
VCustomElement,
32+
VStatic,
3233
} from './vnodes';
3334

3435
import { patchProps } from './modules/props';
@@ -81,6 +82,11 @@ function hydrateNode(node: Node, vnode: VNode, renderer: RendererAPI): Node | nu
8182
hydratedNode = hydrateComment(node, vnode, renderer);
8283
break;
8384

85+
case VNodeType.Static:
86+
// VStatic are cacheable and cannot have custom renderer associated to them
87+
hydratedNode = hydrateStaticElement(node, vnode, renderer);
88+
break;
89+
8490
case VNodeType.Element:
8591
hydratedNode = hydrateElement(node, vnode, vnode.data.renderer ?? renderer);
8692
break;
@@ -137,6 +143,16 @@ function hydrateComment(node: Node, vnode: VComment, renderer: RendererAPI): Nod
137143
return node;
138144
}
139145

146+
function hydrateStaticElement(elm: Node, vnode: VStatic, renderer: RendererAPI): Node | null {
147+
if (!areCompatibleNodes(vnode.fragment, elm, vnode, renderer)) {
148+
return handleMismatch(elm, vnode, renderer);
149+
}
150+
151+
vnode.elm = elm;
152+
153+
return elm;
154+
}
155+
140156
function hydrateElement(elm: Node, vnode: VElement, renderer: RendererAPI): Node | null {
141157
if (
142158
!hasCorrectNodeType<Element>(vnode, elm, EnvNodeTypes.ELEMENT, renderer) ||
@@ -481,3 +497,61 @@ function validateStyleAttr(vnode: VBaseElement, elm: Element, renderer: Renderer
481497

482498
return nodesAreCompatible;
483499
}
500+
501+
function areCompatibleNodes(client: Node, ssr: Node, vnode: VNode, renderer: RendererAPI) {
502+
const { getProperty, getAttribute } = renderer;
503+
if (getProperty(client, 'nodeType') === EnvNodeTypes.TEXT) {
504+
if (!hasCorrectNodeType(vnode, ssr, EnvNodeTypes.TEXT, renderer)) {
505+
return false;
506+
}
507+
508+
return getProperty(client, 'nodeValue') === getProperty(ssr, 'nodeValue');
509+
}
510+
511+
if (getProperty(client, 'nodeType') === EnvNodeTypes.COMMENT) {
512+
if (!hasCorrectNodeType(vnode, ssr, EnvNodeTypes.COMMENT, renderer)) {
513+
return false;
514+
}
515+
516+
return getProperty(client, 'nodeValue') === getProperty(ssr, 'nodeValue');
517+
}
518+
519+
if (!hasCorrectNodeType(vnode, ssr, EnvNodeTypes.ELEMENT, renderer)) {
520+
return false;
521+
}
522+
523+
let isCompatibleElements = true;
524+
if (getProperty(client, 'tagName') !== getProperty(ssr, 'tagName')) {
525+
if (process.env.NODE_ENV !== 'production') {
526+
logError(
527+
`Hydration mismatch: expecting element with tag "${getProperty(
528+
client,
529+
'tagName'
530+
).toLowerCase()}" but found "${getProperty(ssr, 'tagName').toLowerCase()}".`,
531+
vnode.owner
532+
);
533+
}
534+
535+
return false;
536+
}
537+
538+
const clientAttrsNames: string[] = getProperty(client, 'getAttributeNames').call(client);
539+
540+
clientAttrsNames.forEach((attrName) => {
541+
if (getAttribute(client, attrName) !== getAttribute(ssr, attrName)) {
542+
logError(
543+
`Mismatch hydrating element <${getProperty(
544+
client,
545+
'tagName'
546+
).toLowerCase()}>: attribute "${attrName}" has different values, expected "${getAttribute(
547+
client,
548+
attrName
549+
)}" but found "${getAttribute(ssr, attrName)}"`,
550+
vnode.owner
551+
);
552+
isCompatibleElements = false;
553+
}
554+
});
555+
556+
return isCompatibleElements;
557+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export {
2626
getAssociatedVMIfPresent,
2727
} from './vm';
2828

29+
export { parseFragment, parseSVGFragment } from './template';
2930
export { hydrateRoot } from './hydration';
3031

3132
// Internal APIs used by compiled code -------------------------------------------------------------

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface RendererAPI {
1818
isHydrating: () => boolean;
1919
insert: (node: N, parent: E, anchor: N | null) => void;
2020
remove: (node: N, parent: E) => void;
21+
cloneNode: (node: N, deep: boolean) => N;
22+
createFragment: (html: string) => N | null;
2123
createElement: (tagName: string, namespace?: string) => E;
2224
createText: (content: string) => N;
2325
createComment: (content: string) => N;

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
keys,
1717
SVG_NAMESPACE,
1818
KEY__SHADOW_RESOLVER,
19+
KEY__SHADOW_STATIC,
1920
} from '@lwc/shared';
2021

2122
import { RendererAPI } from './renderer';
@@ -48,6 +49,7 @@ import {
4849
isVBaseElement,
4950
isSameVnode,
5051
VNodeType,
52+
VStatic,
5153
} from './vnodes';
5254

5355
import { patchAttributes } from './modules/attrs';
@@ -98,6 +100,10 @@ function patch(n1: VNode, n2: VNode, renderer: RendererAPI) {
98100
patchComment(n1 as VComment, n2, renderer);
99101
break;
100102

103+
case VNodeType.Static:
104+
n2.elm = n1.elm;
105+
break;
106+
101107
case VNodeType.Element:
102108
patchElement(n1 as VElement, n2, n2.data.renderer ?? renderer);
103109
break;
@@ -120,6 +126,11 @@ export function mount(node: VNode, parent: ParentNode, renderer: RendererAPI, an
120126
mountComment(node, parent, anchor, renderer);
121127
break;
122128

129+
case VNodeType.Static:
130+
// VStatic cannot have a custom renderer associated to them, using owner's renderer
131+
mountStatic(node, parent, anchor, renderer);
132+
break;
133+
123134
case VNodeType.Element:
124135
// If the vnode data has a renderer override use it, else fallback to owner's renderer
125136
mountElement(node, parent, anchor, node.data.renderer ?? renderer);
@@ -208,6 +219,35 @@ function patchElement(n1: VElement, n2: VElement, renderer: RendererAPI) {
208219
patchChildren(n1.children, n2.children, elm, renderer);
209220
}
210221

222+
function mountStatic(
223+
vnode: VStatic,
224+
parent: ParentNode,
225+
anchor: Node | null,
226+
renderer: RendererAPI
227+
) {
228+
const { owner } = vnode;
229+
const { cloneNode, isSyntheticShadowDefined } = renderer;
230+
const elm = (vnode.elm = cloneNode(vnode.fragment, true));
231+
232+
linkNodeToShadow(elm, owner, renderer);
233+
234+
// Marks this node as Static to propagate the shadow resolver. must happen after elm is assigned to the proper shadow
235+
const { renderMode, shadowMode } = owner;
236+
237+
if (isSyntheticShadowDefined) {
238+
if (shadowMode === ShadowMode.Synthetic || renderMode === RenderMode.Light) {
239+
(elm as any)[KEY__SHADOW_STATIC] = true;
240+
}
241+
}
242+
243+
if (process.env.NODE_ENV !== 'production') {
244+
const isLight = renderMode === RenderMode.Light;
245+
patchElementWithRestrictions(elm, { isPortal: false, isLight });
246+
}
247+
248+
insertNode(elm, parent, anchor, renderer);
249+
}
250+
211251
function mountCustomElement(
212252
vnode: VCustomElement,
213253
parent: ParentNode,

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

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,34 @@ import {
1414
isNull,
1515
isTrue,
1616
isUndefined,
17-
toString,
1817
KEY__SCOPED_CSS,
18+
toString,
1919
} from '@lwc/shared';
2020

2121
import { logError } from '../shared/logger';
2222
import { getComponentTag } from '../shared/format';
23-
2423
import api, { RenderAPI } from './api';
2524
import {
25+
RenderMode,
2626
resetComponentRoot,
2727
runWithBoundaryProtection,
28+
ShadowMode,
2829
SlotSet,
2930
TemplateCache,
3031
VM,
31-
RenderMode,
3232
} from './vm';
3333
import { EmptyArray } from './utils';
3434
import { defaultEmptyTemplate, isTemplateRegistered } from './secure-template';
3535
import {
36-
TemplateStylesheetFactories,
3736
createStylesheet,
3837
getStylesheetsContent,
38+
TemplateStylesheetFactories,
3939
updateStylesheetToken,
4040
} from './stylesheet';
41-
import { logOperationStart, logOperationEnd, OperationId } from './profiler';
41+
import { logOperationEnd, logOperationStart, OperationId } from './profiler';
4242
import { getTemplateOrSwappedTemplate, setActiveVM } from './hot-swaps';
4343
import { VNodes } from './vnodes';
44+
import { RendererAPI } from './renderer';
4445

4546
export interface Template {
4647
(api: RenderAPI, cmp: object, slotSet: SlotSet, cache: TemplateCache): VNodes;
@@ -113,6 +114,82 @@ function validateLightDomTemplate(template: Template, vm: VM) {
113114
}
114115
}
115116

117+
const enum FragmentCache {
118+
HAS_SCOPED_STYLE = 1 << 0,
119+
SHADOW_MODE_SYNTHETIC = 1 << 1,
120+
}
121+
122+
function buildParseFragmentFn(
123+
createFragmentFn: (html: string, renderer: RendererAPI) => Element
124+
): (strings: string[], ...keys: number[]) => () => Element {
125+
return (strings: string[], ...keys: number[]) => {
126+
const cache = create(null);
127+
128+
return function (): Element {
129+
const {
130+
context: { hasScopedStyles, stylesheetToken },
131+
shadowMode,
132+
renderer,
133+
} = getVMBeingRendered()!;
134+
const hasStyleToken = !isUndefined(stylesheetToken);
135+
const isSyntheticShadow = shadowMode === ShadowMode.Synthetic;
136+
137+
let cacheKey = 0;
138+
if (hasStyleToken && hasScopedStyles) {
139+
cacheKey |= FragmentCache.HAS_SCOPED_STYLE;
140+
}
141+
if (hasStyleToken && isSyntheticShadow) {
142+
cacheKey |= FragmentCache.SHADOW_MODE_SYNTHETIC;
143+
}
144+
145+
if (!isUndefined(cache[cacheKey])) {
146+
return cache[cacheKey];
147+
}
148+
149+
const classToken = hasScopedStyles && hasStyleToken ? ' ' + stylesheetToken : '';
150+
const classAttrToken =
151+
hasScopedStyles && hasStyleToken ? ` class="${stylesheetToken}"` : '';
152+
const attrToken = hasStyleToken && isSyntheticShadow ? ' ' + stylesheetToken : '';
153+
154+
let htmlFragment = '';
155+
for (let i = 0, n = keys.length; i < n; i++) {
156+
switch (keys[i]) {
157+
case 0: // styleToken in existing class attr
158+
htmlFragment += strings[i] + classToken;
159+
break;
160+
case 1: // styleToken for added class attr
161+
htmlFragment += strings[i] + classAttrToken;
162+
break;
163+
case 2: // styleToken as attr
164+
htmlFragment += strings[i] + attrToken;
165+
break;
166+
case 3: // ${1}${2}
167+
htmlFragment += strings[i] + classAttrToken + attrToken;
168+
break;
169+
}
170+
}
171+
172+
htmlFragment += strings[strings.length - 1];
173+
174+
cache[cacheKey] = createFragmentFn(htmlFragment, renderer);
175+
176+
return cache[cacheKey];
177+
};
178+
};
179+
}
180+
181+
// Note: at the moment this code executes, we don't have a renderer yet.
182+
export const parseFragment = buildParseFragmentFn((html, renderer) => {
183+
const { createFragment } = renderer;
184+
return createFragment(html);
185+
});
186+
187+
export const parseSVGFragment = buildParseFragmentFn((html, renderer) => {
188+
const { createFragment, getFirstChild } = renderer;
189+
const fragment = createFragment('<svg>' + html + '</svg>');
190+
return getFirstChild(fragment);
191+
});
192+
116193
export function evaluateTemplate(vm: VM, html: Template): VNodes {
117194
if (process.env.NODE_ENV !== 'production') {
118195
assert.isTrue(

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ export const enum VNodeType {
1515
Comment,
1616
Element,
1717
CustomElement,
18+
Static,
1819
}
1920

20-
export type VNode = VText | VComment | VElement | VCustomElement;
21+
export type VNode = VText | VComment | VElement | VCustomElement | VStatic;
2122
export type VParentElement = VElement | VCustomElement;
2223
export type VNodes = Readonly<Array<VNode | null>>;
2324

@@ -29,6 +30,13 @@ export interface BaseVNode {
2930
owner: VM;
3031
}
3132

33+
export interface VStatic extends BaseVNode {
34+
type: VNodeType.Static;
35+
sel: undefined;
36+
key: Key;
37+
fragment: Element;
38+
}
39+
3240
export interface VText extends BaseVNode {
3341
type: VNodeType.Text;
3442
sel: undefined;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import './polyfills/aria-properties/main';
1111
// Tests -------------------------------------------------------------------------------------------
1212
import './testFeatureFlag.ts';
1313

14+
// Tests -------------------------------------------------------------------------------------------
15+
import './testFeatureFlag.ts';
16+
1417
// Engine-core public APIs -------------------------------------------------------------------------
1518
export {
1619
createContextProvider,
@@ -30,6 +33,8 @@ export {
3033
setHooks,
3134
getComponentDef,
3235
isComponentConstructor,
36+
parseFragment,
37+
parseSVGFragment,
3338
swapComponent,
3439
swapStyle,
3540
swapTemplate,

0 commit comments

Comments
 (0)