Skip to content

Commit

Permalink
perf: apply static parts optimization to dynamic text (#4099)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
jmsjtu and nolanlawson committed Mar 27, 2024
1 parent 8d9da28 commit ea41be4
Show file tree
Hide file tree
Showing 146 changed files with 8,562 additions and 1,391 deletions.
7 changes: 6 additions & 1 deletion packages/@lwc/engine-core/src/framework/api.ts
Expand Up @@ -51,6 +51,7 @@ import {
VStatic,
VStaticPart,
VStaticPartData,
VStaticPartType,
VText,
} from './vnodes';
import { getComponentRegisteredName } from './component';
Expand All @@ -62,10 +63,14 @@ function addVNodeToChildLWC(vnode: VCustomElement) {
}

// [s]tatic [p]art
function sp(partId: number, data: VStaticPartData): VStaticPart {
function sp(partId: number, data: VStaticPartData | null, text: string | null): VStaticPart {
// Static part will always have either text or data, it's guaranteed by the compiler.
const type = isNull(text) ? VStaticPartType.Element : VStaticPartType.Text;
return {
type,
partId,
data,
text,
elm: undefined, // elm is defined later
};
}
Expand Down
45 changes: 36 additions & 9 deletions packages/@lwc/engine-core/src/framework/hydration.ts
Expand Up @@ -51,6 +51,8 @@ import {
isVCustomElement,
VElementData,
VStaticPartData,
VStaticPartText,
isVStaticPartElement,
} from './vnodes';

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

const NODE_VALUE_PROP = 'nodeValue';

function textNodeContentsAreEqual(node: Node, vnode: VText, renderer: RendererAPI): boolean {
function textNodeContentsAreEqual(
node: Node,
vnode: VText | VStaticPartText,
renderer: RendererAPI
): boolean {
const { getProperty } = renderer;
const nodeValue = getProperty(node, NODE_VALUE_PROP);

Expand Down Expand Up @@ -183,11 +189,20 @@ function hydrateText(node: Node, vnode: VText, renderer: RendererAPI): Node | nu
if (!hasCorrectNodeType(vnode, node, EnvNodeTypes.TEXT, renderer)) {
return handleMismatch(node, vnode, renderer);
}
return updateTextContent(node, vnode, vnode.owner, renderer);
}

function updateTextContent(
node: Node,
vnode: VText | VStaticPartText,
owner: VM,
renderer: RendererAPI
): Node | null {
if (process.env.NODE_ENV !== 'production') {
if (!textNodeContentsAreEqual(node, vnode, renderer)) {
logWarn(
'Hydration mismatch: text values do not match, will recover from the difference',
vnode.owner
owner
);
}
}
Expand Down Expand Up @@ -794,7 +809,7 @@ function areCompatibleStaticNodes(client: Node, ssr: Node, vnode: VStatic, rende
}

function haveCompatibleStaticParts(vnode: VStatic, renderer: RendererAPI) {
const { parts } = vnode;
const { parts, owner } = vnode;

if (isUndefined(parts)) {
return true;
Expand All @@ -804,12 +819,24 @@ function haveCompatibleStaticParts(vnode: VStatic, renderer: RendererAPI) {
// 1. It's never the case that `parts` is undefined on the server but defined on the client (or vice-versa)
// 2. It's never the case that `parts` has one length on the server but another on the client
for (const part of parts) {
const { data, elm } = part;
const hasMatchingAttrs = validateAttrs(vnode, elm!, data, renderer, () => true);
const hasMatchingStyleAttr = validateStyleAttr(vnode, elm!, data, renderer);
const hasMatchingClass = validateClassAttr(vnode, elm!, data, renderer);
if (isFalse(hasMatchingAttrs && hasMatchingStyleAttr && hasMatchingClass)) {
return false;
const { elm } = part;
if (isVStaticPartElement(part)) {
if (!hasCorrectNodeType<Element>(vnode, elm!, EnvNodeTypes.ELEMENT, renderer)) {
return false;
}
const { data } = part;
const hasMatchingAttrs = validateAttrs(vnode, elm, data, renderer, () => true);
const hasMatchingStyleAttr = validateStyleAttr(vnode, elm, data, renderer);
const hasMatchingClass = validateClassAttr(vnode, elm, data, renderer);
if (isFalse(hasMatchingAttrs && hasMatchingStyleAttr && hasMatchingClass)) {
return false;
}
} else {
// VStaticPartText
if (!hasCorrectNodeType(vnode, elm!, EnvNodeTypes.TEXT, renderer)) {
return false;
}
updateTextContent(elm, part as VStaticPartText, owner, renderer);
}
}
return true;
Expand Down
6 changes: 3 additions & 3 deletions packages/@lwc/engine-core/src/framework/modules/attrs.ts
Expand Up @@ -15,13 +15,13 @@ import {
import { RendererAPI } from '../renderer';

import { EmptyObject } from '../utils';
import { VBaseElement, VStatic, VStaticPart } from '../vnodes';
import { VBaseElement, VStatic, VStaticPartElement } from '../vnodes';

const ColonCharCode = 58;

export function patchAttributes(
oldVnode: VBaseElement | VStaticPart | null,
vnode: VBaseElement | VStaticPart,
oldVnode: VBaseElement | VStaticPartElement | null,
vnode: VBaseElement | VStaticPartElement,
renderer: RendererAPI
) {
const { data, elm } = vnode;
Expand Down
Expand Up @@ -16,7 +16,7 @@ import {
import { RendererAPI } from '../renderer';

import { EmptyObject, SPACE_CHAR } from '../utils';
import { VBaseElement, VStaticPart } from '../vnodes';
import { VBaseElement, VStaticPartElement } from '../vnodes';

const classNameToClassMap = create(null);

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

export function patchClassAttribute(
oldVnode: VBaseElement | VStaticPart | null,
vnode: VBaseElement | VStaticPart,
oldVnode: VBaseElement | VStaticPartElement | null,
vnode: VBaseElement | VStaticPartElement,
renderer: RendererAPI
) {
const {
Expand Down
Expand Up @@ -6,14 +6,14 @@
*/
import { isNull, isString, isUndefined } from '@lwc/shared';
import { RendererAPI } from '../renderer';
import { VBaseElement, VStaticPart } from '../vnodes';
import { VBaseElement, VStaticPartElement } from '../vnodes';
import { logError } from '../../shared/logger';
import { VM } from '../vm';

// The style property is a string when defined via an expression in the template.
export function patchStyleAttribute(
oldVnode: VBaseElement | VStaticPart | null,
vnode: VBaseElement | VStaticPart,
oldVnode: VBaseElement | VStaticPartElement | null,
vnode: VBaseElement | VStaticPartElement,
renderer: RendererAPI,
owner: VM
) {
Expand Down
7 changes: 5 additions & 2 deletions packages/@lwc/engine-core/src/framework/modules/events.ts
Expand Up @@ -6,9 +6,12 @@
*/
import { isUndefined } from '@lwc/shared';
import { RendererAPI } from '../renderer';
import { VBaseElement, VStaticPart } from '../vnodes';
import { VBaseElement, VStaticPartElement } from '../vnodes';

export function applyEventListeners(vnode: VBaseElement | VStaticPart, renderer: RendererAPI) {
export function applyEventListeners(
vnode: VBaseElement | VStaticPartElement,
renderer: RendererAPI
) {
const { elm, data } = vnode;
const { on } = data;

Expand Down
4 changes: 2 additions & 2 deletions packages/@lwc/engine-core/src/framework/modules/refs.ts
Expand Up @@ -6,10 +6,10 @@
*/
import { isUndefined } from '@lwc/shared';
import { RefVNodes, VM } from '../vm';
import { VBaseElement, VStaticPart } from '../vnodes';
import { VBaseElement, VStaticPartElement } from '../vnodes';

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

Expand Down
64 changes: 47 additions & 17 deletions packages/@lwc/engine-core/src/framework/modules/static-parts.ts
Expand Up @@ -6,13 +6,21 @@
*/

import { isNull, isUndefined, assert, ArrayShift, ArrayUnshift } from '@lwc/shared';
import { VStatic, VStaticPart } from '../vnodes';
import {
VStatic,
VStaticPart,
VStaticPartElement,
VStaticPartText,
isVStaticPartElement,
isVStaticPartText,
} from '../vnodes';
import { RendererAPI } from '../renderer';
import { applyEventListeners } from './events';
import { applyRefs } from './refs';
import { patchAttributes } from './attrs';
import { patchStyleAttribute } from './computed-style-attr';
import { patchClassAttribute } from './computed-class-attr';
import { patchTextVStaticPart } from './text';

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

// Currently only event listeners and refs are supported for static vnodes
for (const part of parts) {
// Event listeners only need to be applied once when mounting
applyEventListeners(part, renderer);
// Refs must be updated after every render due to refVNodes getting reset before every render
applyRefs(part, owner);
patchAttributes(null, part, renderer);
patchClassAttribute(null, part, renderer);
patchStyleAttribute(null, part, renderer, owner);
if (isVStaticPartElement(part)) {
// Event listeners only need to be applied once when mounting
applyEventListeners(part, renderer);
// Refs must be updated after every render due to refVNodes getting reset before every render
applyRefs(part, owner);
patchAttributes(null, part, renderer);
patchClassAttribute(null, part, renderer);
patchStyleAttribute(null, part, renderer, owner);
} else {
if (process.env.NODE_ENV !== 'production' && !isVStaticPartText(part)) {
throw new Error(
`LWC internal error, encountered unknown static part type: ${part.type}`
);
}
patchTextVStaticPart(null, part as VStaticPartText, renderer);
}
}
}

Expand Down Expand Up @@ -144,11 +161,22 @@ export function patchStaticParts(n1: VStatic, n2: VStatic, renderer: RendererAPI
// Patch only occurs if the vnode is newly generated, which means the part.elm is always undefined
// Since the vnode and elements are the same we can safely assume that prevParts[i].elm is defined.
part.elm = prevPart.elm;
// Refs must be updated after every render due to refVNodes getting reset before every render
applyRefs(part, currPartsOwner);
patchAttributes(prevPart, part, renderer);
patchClassAttribute(prevPart, part, renderer);
patchStyleAttribute(prevPart, part, renderer, currPartsOwner);

if (process.env.NODE_ENV !== 'production' && prevPart.type !== part.type) {
throw new Error(
`LWC internal error, static part types do not match. Previous type was ${prevPart.type} and current type is ${part.type}`
);
}

if (isVStaticPartElement(part)) {
// Refs must be updated after every render due to refVNodes getting reset before every render
applyRefs(part, currPartsOwner);
patchAttributes(prevPart as VStaticPartElement, part, renderer);
patchClassAttribute(prevPart as VStaticPartElement, part, renderer);
patchStyleAttribute(prevPart as VStaticPartElement, part, renderer, currPartsOwner);
} else {
patchTextVStaticPart(null, part as VStaticPartText, renderer);
}
}
}

Expand All @@ -171,9 +199,11 @@ export function hydrateStaticParts(vnode: VStatic, renderer: RendererAPI): void
// which guarantees that the elements are the same.
// We only need to apply the parts for things that cannot be done on the server.
for (const part of parts) {
// Event listeners only need to be applied once when mounting
applyEventListeners(part, renderer);
// Refs must be updated after every render due to refVNodes getting reset before every render
applyRefs(part, owner);
if (isVStaticPartElement(part)) {
// Event listeners only need to be applied once when mounting
applyEventListeners(part, renderer);
// Refs must be updated after every render due to refVNodes getting reset before every render
applyRefs(part, owner);
}
}
}
44 changes: 44 additions & 0 deletions packages/@lwc/engine-core/src/framework/modules/text.ts
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { isNull } from '@lwc/shared';
import { RendererAPI } from '../renderer';
import { lockDomMutation, unlockDomMutation } from '../restrictions';
import { VComment, VStaticPartText, VText } from '../vnodes';

export function patchTextVNode(n1: VText, n2: VText, renderer: RendererAPI) {
n2.elm = n1.elm;

if (n2.text !== n1.text) {
updateTextContent(n2, renderer);
}
}

export function patchTextVStaticPart(
n1: VStaticPartText | null,
n2: VStaticPartText,
renderer: RendererAPI
) {
if (isNull(n1) || n2.text !== n1.text) {
updateTextContent(n2, renderer);
}
}

export function updateTextContent(
vnode: VText | VComment | VStaticPartText,
renderer: RendererAPI
) {
const { elm, text } = vnode;
const { setText } = renderer;

if (process.env.NODE_ENV !== 'production') {
unlockDomMutation();
}
setText(elm, text);
if (process.env.NODE_ENV !== 'production') {
lockDomMutation();
}
}
24 changes: 2 additions & 22 deletions packages/@lwc/engine-core/src/framework/rendering.ts
Expand Up @@ -71,6 +71,7 @@ import { applyStaticStyleAttribute } from './modules/static-style-attr';
import { applyRefs } from './modules/refs';
import { mountStaticParts, patchStaticParts } from './modules/static-parts';
import { LightningElementConstructor } from './base-lightning-element';
import { patchTextVNode, updateTextContent } from './modules/text';

export function patchChildren(
c1: VNodes,
Expand Down Expand Up @@ -111,7 +112,7 @@ function patch(n1: VNode, n2: VNode, parent: ParentNode, renderer: RendererAPI)
switch (n2.type) {
case VNodeType.Text:
// VText has no special capability, fallback to the owner's renderer
patchText(n1 as VText, n2, renderer);
patchTextVNode(n1 as VText, n2, renderer);
break;

case VNodeType.Comment:
Expand Down Expand Up @@ -170,14 +171,6 @@ export function mount(node: VNode, parent: ParentNode, renderer: RendererAPI, an
}
}

function patchText(n1: VText, n2: VText, renderer: RendererAPI) {
n2.elm = n1.elm;

if (n2.text !== n1.text) {
updateTextContent(n2, renderer);
}
}

function mountText(vnode: VText, parent: ParentNode, anchor: Node | null, renderer: RendererAPI) {
const { owner } = vnode;
const { createText } = renderer;
Expand Down Expand Up @@ -535,19 +528,6 @@ function linkNodeToShadow(elm: Node, owner: VM, renderer: RendererAPI) {
}
}

function updateTextContent(vnode: VText | VComment, renderer: RendererAPI) {
const { elm, text } = vnode;
const { setText } = renderer;

if (process.env.NODE_ENV !== 'production') {
unlockDomMutation();
}
setText(elm, text);
if (process.env.NODE_ENV !== 'production') {
lockDomMutation();
}
}

function insertFragmentOrNode(
vnode: VNode,
parent: Node,
Expand Down

0 comments on commit ea41be4

Please sign in to comment.