Skip to content

Commit ea41be4

Browse files
jmsjtunolanlawson
andauthored
perf: apply static parts optimization to dynamic text (#4099)
* perf: enable static content optimization for dynamic text * test: new template-compiler tests * test: update existing template compiler tests * test: add karma tests * chore: update based on review feedback * chore: update test fixtures * Update packages/@lwc/template-compiler/src/codegen/codegen.ts Co-authored-by: Nolan Lawson <nlawson@salesforce.com> * chore: update based on pr feedback --------- Co-authored-by: Nolan Lawson <nlawson@salesforce.com>
1 parent 8d9da28 commit ea41be4

File tree

146 files changed

+8562
-1391
lines changed

Some content is hidden

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

146 files changed

+8562
-1391
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
VStatic,
5252
VStaticPart,
5353
VStaticPartData,
54+
VStaticPartType,
5455
VText,
5556
} from './vnodes';
5657
import { getComponentRegisteredName } from './component';
@@ -62,10 +63,14 @@ function addVNodeToChildLWC(vnode: VCustomElement) {
6263
}
6364

6465
// [s]tatic [p]art
65-
function sp(partId: number, data: VStaticPartData): VStaticPart {
66+
function sp(partId: number, data: VStaticPartData | null, text: string | null): VStaticPart {
67+
// Static part will always have either text or data, it's guaranteed by the compiler.
68+
const type = isNull(text) ? VStaticPartType.Element : VStaticPartType.Text;
6669
return {
70+
type,
6771
partId,
6872
data,
73+
text,
6974
elm: undefined, // elm is defined later
7075
};
7176
}

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

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import {
5151
isVCustomElement,
5252
VElementData,
5353
VStaticPartData,
54+
VStaticPartText,
55+
isVStaticPartElement,
5456
} from './vnodes';
5557

5658
import { patchProps } from './modules/props';
@@ -134,7 +136,11 @@ function hydrateNode(node: Node, vnode: VNode, renderer: RendererAPI): Node | nu
134136

135137
const NODE_VALUE_PROP = 'nodeValue';
136138

137-
function textNodeContentsAreEqual(node: Node, vnode: VText, renderer: RendererAPI): boolean {
139+
function textNodeContentsAreEqual(
140+
node: Node,
141+
vnode: VText | VStaticPartText,
142+
renderer: RendererAPI
143+
): boolean {
138144
const { getProperty } = renderer;
139145
const nodeValue = getProperty(node, NODE_VALUE_PROP);
140146

@@ -183,11 +189,20 @@ function hydrateText(node: Node, vnode: VText, renderer: RendererAPI): Node | nu
183189
if (!hasCorrectNodeType(vnode, node, EnvNodeTypes.TEXT, renderer)) {
184190
return handleMismatch(node, vnode, renderer);
185191
}
192+
return updateTextContent(node, vnode, vnode.owner, renderer);
193+
}
194+
195+
function updateTextContent(
196+
node: Node,
197+
vnode: VText | VStaticPartText,
198+
owner: VM,
199+
renderer: RendererAPI
200+
): Node | null {
186201
if (process.env.NODE_ENV !== 'production') {
187202
if (!textNodeContentsAreEqual(node, vnode, renderer)) {
188203
logWarn(
189204
'Hydration mismatch: text values do not match, will recover from the difference',
190-
vnode.owner
205+
owner
191206
);
192207
}
193208
}
@@ -794,7 +809,7 @@ function areCompatibleStaticNodes(client: Node, ssr: Node, vnode: VStatic, rende
794809
}
795810

796811
function haveCompatibleStaticParts(vnode: VStatic, renderer: RendererAPI) {
797-
const { parts } = vnode;
812+
const { parts, owner } = vnode;
798813

799814
if (isUndefined(parts)) {
800815
return true;
@@ -804,12 +819,24 @@ function haveCompatibleStaticParts(vnode: VStatic, renderer: RendererAPI) {
804819
// 1. It's never the case that `parts` is undefined on the server but defined on the client (or vice-versa)
805820
// 2. It's never the case that `parts` has one length on the server but another on the client
806821
for (const part of parts) {
807-
const { data, elm } = part;
808-
const hasMatchingAttrs = validateAttrs(vnode, elm!, data, renderer, () => true);
809-
const hasMatchingStyleAttr = validateStyleAttr(vnode, elm!, data, renderer);
810-
const hasMatchingClass = validateClassAttr(vnode, elm!, data, renderer);
811-
if (isFalse(hasMatchingAttrs && hasMatchingStyleAttr && hasMatchingClass)) {
812-
return false;
822+
const { elm } = part;
823+
if (isVStaticPartElement(part)) {
824+
if (!hasCorrectNodeType<Element>(vnode, elm!, EnvNodeTypes.ELEMENT, renderer)) {
825+
return false;
826+
}
827+
const { data } = part;
828+
const hasMatchingAttrs = validateAttrs(vnode, elm, data, renderer, () => true);
829+
const hasMatchingStyleAttr = validateStyleAttr(vnode, elm, data, renderer);
830+
const hasMatchingClass = validateClassAttr(vnode, elm, data, renderer);
831+
if (isFalse(hasMatchingAttrs && hasMatchingStyleAttr && hasMatchingClass)) {
832+
return false;
833+
}
834+
} else {
835+
// VStaticPartText
836+
if (!hasCorrectNodeType(vnode, elm!, EnvNodeTypes.TEXT, renderer)) {
837+
return false;
838+
}
839+
updateTextContent(elm, part as VStaticPartText, owner, renderer);
813840
}
814841
}
815842
return true;

packages/@lwc/engine-core/src/framework/modules/attrs.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ import {
1515
import { RendererAPI } from '../renderer';
1616

1717
import { EmptyObject } from '../utils';
18-
import { VBaseElement, VStatic, VStaticPart } from '../vnodes';
18+
import { VBaseElement, VStatic, VStaticPartElement } from '../vnodes';
1919

2020
const ColonCharCode = 58;
2121

2222
export function patchAttributes(
23-
oldVnode: VBaseElement | VStaticPart | null,
24-
vnode: VBaseElement | VStaticPart,
23+
oldVnode: VBaseElement | VStaticPartElement | null,
24+
vnode: VBaseElement | VStaticPartElement,
2525
renderer: RendererAPI
2626
) {
2727
const { data, elm } = vnode;

packages/@lwc/engine-core/src/framework/modules/computed-class-attr.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import { RendererAPI } from '../renderer';
1717

1818
import { EmptyObject, SPACE_CHAR } from '../utils';
19-
import { VBaseElement, VStaticPart } from '../vnodes';
19+
import { VBaseElement, VStaticPartElement } from '../vnodes';
2020

2121
const classNameToClassMap = create(null);
2222

@@ -58,8 +58,8 @@ export function getMapFromClassName(className: string | undefined): Record<strin
5858
}
5959

6060
export function patchClassAttribute(
61-
oldVnode: VBaseElement | VStaticPart | null,
62-
vnode: VBaseElement | VStaticPart,
61+
oldVnode: VBaseElement | VStaticPartElement | null,
62+
vnode: VBaseElement | VStaticPartElement,
6363
renderer: RendererAPI
6464
) {
6565
const {

packages/@lwc/engine-core/src/framework/modules/computed-style-attr.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
*/
77
import { isNull, isString, isUndefined } from '@lwc/shared';
88
import { RendererAPI } from '../renderer';
9-
import { VBaseElement, VStaticPart } from '../vnodes';
9+
import { VBaseElement, VStaticPartElement } from '../vnodes';
1010
import { logError } from '../../shared/logger';
1111
import { VM } from '../vm';
1212

1313
// The style property is a string when defined via an expression in the template.
1414
export function patchStyleAttribute(
15-
oldVnode: VBaseElement | VStaticPart | null,
16-
vnode: VBaseElement | VStaticPart,
15+
oldVnode: VBaseElement | VStaticPartElement | null,
16+
vnode: VBaseElement | VStaticPartElement,
1717
renderer: RendererAPI,
1818
owner: VM
1919
) {

packages/@lwc/engine-core/src/framework/modules/events.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
*/
77
import { isUndefined } from '@lwc/shared';
88
import { RendererAPI } from '../renderer';
9-
import { VBaseElement, VStaticPart } from '../vnodes';
9+
import { VBaseElement, VStaticPartElement } from '../vnodes';
1010

11-
export function applyEventListeners(vnode: VBaseElement | VStaticPart, renderer: RendererAPI) {
11+
export function applyEventListeners(
12+
vnode: VBaseElement | VStaticPartElement,
13+
renderer: RendererAPI
14+
) {
1215
const { elm, data } = vnode;
1316
const { on } = data;
1417

packages/@lwc/engine-core/src/framework/modules/refs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
*/
77
import { isUndefined } from '@lwc/shared';
88
import { RefVNodes, VM } from '../vm';
9-
import { VBaseElement, VStaticPart } from '../vnodes';
9+
import { VBaseElement, VStaticPartElement } from '../vnodes';
1010

1111
// Set a ref (lwc:ref) on a VM, from a template API
12-
export function applyRefs(vnode: VBaseElement | VStaticPart, owner: VM) {
12+
export function applyRefs(vnode: VBaseElement | VStaticPartElement, owner: VM) {
1313
const { data } = vnode;
1414
const { ref } = data;
1515

packages/@lwc/engine-core/src/framework/modules/static-parts.ts

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,21 @@
66
*/
77

88
import { isNull, isUndefined, assert, ArrayShift, ArrayUnshift } from '@lwc/shared';
9-
import { VStatic, VStaticPart } from '../vnodes';
9+
import {
10+
VStatic,
11+
VStaticPart,
12+
VStaticPartElement,
13+
VStaticPartText,
14+
isVStaticPartElement,
15+
isVStaticPartText,
16+
} from '../vnodes';
1017
import { RendererAPI } from '../renderer';
1118
import { applyEventListeners } from './events';
1219
import { applyRefs } from './refs';
1320
import { patchAttributes } from './attrs';
1421
import { patchStyleAttribute } from './computed-style-attr';
1522
import { patchClassAttribute } from './computed-class-attr';
23+
import { patchTextVStaticPart } from './text';
1624

1725
/**
1826
* Given an array of static parts, mounts the DOM element to the part based on the staticPartId
@@ -102,13 +110,22 @@ export function mountStaticParts(root: Element, vnode: VStatic, renderer: Render
102110

103111
// Currently only event listeners and refs are supported for static vnodes
104112
for (const part of parts) {
105-
// Event listeners only need to be applied once when mounting
106-
applyEventListeners(part, renderer);
107-
// Refs must be updated after every render due to refVNodes getting reset before every render
108-
applyRefs(part, owner);
109-
patchAttributes(null, part, renderer);
110-
patchClassAttribute(null, part, renderer);
111-
patchStyleAttribute(null, part, renderer, owner);
113+
if (isVStaticPartElement(part)) {
114+
// Event listeners only need to be applied once when mounting
115+
applyEventListeners(part, renderer);
116+
// Refs must be updated after every render due to refVNodes getting reset before every render
117+
applyRefs(part, owner);
118+
patchAttributes(null, part, renderer);
119+
patchClassAttribute(null, part, renderer);
120+
patchStyleAttribute(null, part, renderer, owner);
121+
} else {
122+
if (process.env.NODE_ENV !== 'production' && !isVStaticPartText(part)) {
123+
throw new Error(
124+
`LWC internal error, encountered unknown static part type: ${part.type}`
125+
);
126+
}
127+
patchTextVStaticPart(null, part as VStaticPartText, renderer);
128+
}
112129
}
113130
}
114131

@@ -144,11 +161,22 @@ export function patchStaticParts(n1: VStatic, n2: VStatic, renderer: RendererAPI
144161
// Patch only occurs if the vnode is newly generated, which means the part.elm is always undefined
145162
// Since the vnode and elements are the same we can safely assume that prevParts[i].elm is defined.
146163
part.elm = prevPart.elm;
147-
// Refs must be updated after every render due to refVNodes getting reset before every render
148-
applyRefs(part, currPartsOwner);
149-
patchAttributes(prevPart, part, renderer);
150-
patchClassAttribute(prevPart, part, renderer);
151-
patchStyleAttribute(prevPart, part, renderer, currPartsOwner);
164+
165+
if (process.env.NODE_ENV !== 'production' && prevPart.type !== part.type) {
166+
throw new Error(
167+
`LWC internal error, static part types do not match. Previous type was ${prevPart.type} and current type is ${part.type}`
168+
);
169+
}
170+
171+
if (isVStaticPartElement(part)) {
172+
// Refs must be updated after every render due to refVNodes getting reset before every render
173+
applyRefs(part, currPartsOwner);
174+
patchAttributes(prevPart as VStaticPartElement, part, renderer);
175+
patchClassAttribute(prevPart as VStaticPartElement, part, renderer);
176+
patchStyleAttribute(prevPart as VStaticPartElement, part, renderer, currPartsOwner);
177+
} else {
178+
patchTextVStaticPart(null, part as VStaticPartText, renderer);
179+
}
152180
}
153181
}
154182

@@ -171,9 +199,11 @@ export function hydrateStaticParts(vnode: VStatic, renderer: RendererAPI): void
171199
// which guarantees that the elements are the same.
172200
// We only need to apply the parts for things that cannot be done on the server.
173201
for (const part of parts) {
174-
// Event listeners only need to be applied once when mounting
175-
applyEventListeners(part, renderer);
176-
// Refs must be updated after every render due to refVNodes getting reset before every render
177-
applyRefs(part, owner);
202+
if (isVStaticPartElement(part)) {
203+
// Event listeners only need to be applied once when mounting
204+
applyEventListeners(part, renderer);
205+
// Refs must be updated after every render due to refVNodes getting reset before every render
206+
applyRefs(part, owner);
207+
}
178208
}
179209
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2024, 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+
import { isNull } from '@lwc/shared';
8+
import { RendererAPI } from '../renderer';
9+
import { lockDomMutation, unlockDomMutation } from '../restrictions';
10+
import { VComment, VStaticPartText, VText } from '../vnodes';
11+
12+
export function patchTextVNode(n1: VText, n2: VText, renderer: RendererAPI) {
13+
n2.elm = n1.elm;
14+
15+
if (n2.text !== n1.text) {
16+
updateTextContent(n2, renderer);
17+
}
18+
}
19+
20+
export function patchTextVStaticPart(
21+
n1: VStaticPartText | null,
22+
n2: VStaticPartText,
23+
renderer: RendererAPI
24+
) {
25+
if (isNull(n1) || n2.text !== n1.text) {
26+
updateTextContent(n2, renderer);
27+
}
28+
}
29+
30+
export function updateTextContent(
31+
vnode: VText | VComment | VStaticPartText,
32+
renderer: RendererAPI
33+
) {
34+
const { elm, text } = vnode;
35+
const { setText } = renderer;
36+
37+
if (process.env.NODE_ENV !== 'production') {
38+
unlockDomMutation();
39+
}
40+
setText(elm, text);
41+
if (process.env.NODE_ENV !== 'production') {
42+
lockDomMutation();
43+
}
44+
}

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

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import { applyStaticStyleAttribute } from './modules/static-style-attr';
7171
import { applyRefs } from './modules/refs';
7272
import { mountStaticParts, patchStaticParts } from './modules/static-parts';
7373
import { LightningElementConstructor } from './base-lightning-element';
74+
import { patchTextVNode, updateTextContent } from './modules/text';
7475

7576
export function patchChildren(
7677
c1: VNodes,
@@ -111,7 +112,7 @@ function patch(n1: VNode, n2: VNode, parent: ParentNode, renderer: RendererAPI)
111112
switch (n2.type) {
112113
case VNodeType.Text:
113114
// VText has no special capability, fallback to the owner's renderer
114-
patchText(n1 as VText, n2, renderer);
115+
patchTextVNode(n1 as VText, n2, renderer);
115116
break;
116117

117118
case VNodeType.Comment:
@@ -170,14 +171,6 @@ export function mount(node: VNode, parent: ParentNode, renderer: RendererAPI, an
170171
}
171172
}
172173

173-
function patchText(n1: VText, n2: VText, renderer: RendererAPI) {
174-
n2.elm = n1.elm;
175-
176-
if (n2.text !== n1.text) {
177-
updateTextContent(n2, renderer);
178-
}
179-
}
180-
181174
function mountText(vnode: VText, parent: ParentNode, anchor: Node | null, renderer: RendererAPI) {
182175
const { owner } = vnode;
183176
const { createText } = renderer;
@@ -535,19 +528,6 @@ function linkNodeToShadow(elm: Node, owner: VM, renderer: RendererAPI) {
535528
}
536529
}
537530

538-
function updateTextContent(vnode: VText | VComment, renderer: RendererAPI) {
539-
const { elm, text } = vnode;
540-
const { setText } = renderer;
541-
542-
if (process.env.NODE_ENV !== 'production') {
543-
unlockDomMutation();
544-
}
545-
setText(elm, text);
546-
if (process.env.NODE_ENV !== 'production') {
547-
lockDomMutation();
548-
}
549-
}
550-
551531
function insertFragmentOrNode(
552532
vnode: VNode,
553533
parent: Node,

0 commit comments

Comments
 (0)