Skip to content

Commit

Permalink
perf: apply static parts optimization to dynamic attributes (#4055)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmsjtu authored Mar 19, 2024
1 parent 13a7fa1 commit c49478f
Show file tree
Hide file tree
Showing 206 changed files with 3,974 additions and 785 deletions.
7 changes: 6 additions & 1 deletion packages/@lwc/engine-core/src/framework/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,13 @@ function ssf(slotName: unknown, factory: (value: any, key: any) => VFragment): V
}

// [st]atic node
function st(fragment: Element, key: Key, parts?: VStaticPart[]): VStatic {
function st(
fragmentFactory: (parts?: VStaticPart[]) => Element,
key: Key,
parts?: VStaticPart[]
): VStatic {
const owner = getVMBeingRendered()!;
const fragment = fragmentFactory(parts);
const vnode: VStatic = {
type: VNodeType.Static,
sel: undefined,
Expand Down
50 changes: 44 additions & 6 deletions packages/@lwc/engine-core/src/framework/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
StringToLowerCase,
APIFeature,
isAPIFeatureEnabled,
isFalse,
} from '@lwc/shared';

import { logError, logWarn } from '../shared/logger';
Expand Down Expand Up @@ -48,11 +49,12 @@ import {
VStatic,
VFragment,
isVCustomElement,
VStaticPart,
} from './vnodes';

import { patchProps } from './modules/props';
import { applyEventListeners } from './modules/events';
import { mountStaticParts } from './modules/static-parts';
import { hydrateStaticParts, traverseAndSetElements } from './modules/static-parts';
import { getScopeTokenClass, getStylesheetTokenHost } from './stylesheet';
import { renderComponent } from './component';
import { applyRefs } from './modules/refs';
Expand Down Expand Up @@ -226,9 +228,26 @@ function hydrateStaticElement(elm: Node, vnode: VStatic, renderer: RendererAPI):
return handleMismatch(elm, vnode, renderer);
}

return hydrateStaticElementParts(elm, vnode, renderer);
}

function hydrateStaticElementParts(elm: Element, vnode: VStatic, renderer: RendererAPI) {
const { parts } = vnode;

if (!isUndefined(parts)) {
// Elements must first be set on the static part to validate against.
traverseAndSetElements(elm, parts, renderer);
}

if (!haveCompatibleStaticParts(vnode, renderer)) {
return handleMismatch(elm, vnode, renderer);
}

vnode.elm = elm;

mountStaticParts(elm, vnode, renderer);
// Hydration only requires applying event listeners and refs.
// All other expressions should be applied during SSR or through the handleMismatch routine.
hydrateStaticParts(vnode, renderer);

return elm;
}
Expand Down Expand Up @@ -482,7 +501,7 @@ function isMatchingElement(
return false;
}

const hasCompatibleAttrs = validateAttrs(vnode, elm, renderer, shouldValidateAttr);
const hasCompatibleAttrs = validateAttrs(vnode, elm, vnode.owner, renderer, shouldValidateAttr);
const hasCompatibleClass = shouldValidateAttr('class')
? validateClassAttr(vnode, elm, renderer)
: true;
Expand Down Expand Up @@ -514,14 +533,15 @@ function attributeValuesAreEqual(
}

function validateAttrs(
vnode: VBaseElement,
node: VBaseElement | VStaticPart,
elm: Element,
owner: VM,
renderer: RendererAPI,
shouldValidateAttr: (attrName: string) => boolean
): boolean {
const {
data: { attrs = {} },
} = vnode;
} = node;

let nodesAreCompatible = true;

Expand All @@ -531,7 +551,6 @@ function validateAttrs(
if (!shouldValidateAttr(attrName)) {
continue;
}
const { owner } = vnode;
const { getAttribute } = renderer;
const elmAttrValue = getAttribute(elm, attrName);
if (!attributeValuesAreEqual(attrValue, elmAttrValue)) {
Expand Down Expand Up @@ -755,3 +774,22 @@ function areCompatibleNodes(client: Node, ssr: Node, vnode: VNode, renderer: Ren

return isCompatibleElements;
}

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

if (isUndefined(parts)) {
return true;
}

// The validation here relies on 2 key invariants:
// 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 hasMatchingAttrs = validateAttrs(part, part.elm!, owner, renderer, () => true);
if (isFalse(hasMatchingAttrs)) {
return false;
}
}
return true;
}
15 changes: 9 additions & 6 deletions packages/@lwc/engine-core/src/framework/modules/attrs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,32 @@ import {
import { RendererAPI } from '../renderer';

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

const ColonCharCode = 58;

export function patchAttributes(
oldVnode: VBaseElement | null,
vnode: VBaseElement,
oldVnode: VBaseElement | VStaticPart | null,
vnode: VBaseElement | VStaticPart,
renderer: RendererAPI
) {
const { attrs, external } = vnode.data;
const { data, elm } = vnode;
const { attrs } = data;

if (isUndefined(attrs)) {
return;
}

const oldAttrs = isNull(oldVnode) ? EmptyObject : oldVnode.data.attrs;

// Attrs may be the same due to the static content optimization, so we can skip diffing
if (oldAttrs === attrs) {
return;
}

const { elm } = vnode;
// Note VStaticPartData does not contain the external property so it will always default to false.
const external = 'external' in data ? data.external : false;
const { setAttribute, removeAttribute, setProperty } = renderer;

for (const key in attrs) {
const cur = attrs[key];
const old = oldAttrs[key];
Expand Down
46 changes: 43 additions & 3 deletions packages/@lwc/engine-core/src/framework/modules/static-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,19 @@ import { VStatic, VStaticPart } from '../vnodes';
import { RendererAPI } from '../renderer';
import { applyEventListeners } from './events';
import { applyRefs } from './refs';
import { patchAttributes } from './attrs';

function traverseAndSetElements(root: Element, parts: VStaticPart[], renderer: RendererAPI): void {
/**
* Given an array of static parts, mounts the DOM element to the part based on the staticPartId
* @param root the root element
* @param parts an array of VStaticParts
* @param renderer the renderer to use
*/
export function traverseAndSetElements(
root: Element,
parts: VStaticPart[],
renderer: RendererAPI
): void {
const numParts = parts.length;

// Optimization given that, in most cases, there will be one part, and it's just the root
Expand Down Expand Up @@ -93,15 +104,17 @@ export function mountStaticParts(root: Element, vnode: VStatic, renderer: Render
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);
}
}

/**
* Mounts elements to the newly generated VStatic node
* Updates the static elements based on the content of the VStaticParts
* @param n1 the previous VStatic vnode
* @param n2 the current VStatic vnode
* @param renderer the renderer to use
*/
export function patchStaticParts(n1: VStatic, n2: VStatic) {
export function patchStaticParts(n1: VStatic, n2: VStatic, renderer: RendererAPI) {
// On the server, we don't support ref (because it relies on renderedCallback), nor do we
// support event listeners (no interactivity), so traversing parts makes no sense
if (!process.env.IS_BROWSER) {
Expand All @@ -128,5 +141,32 @@ export function patchStaticParts(n1: VStatic, n2: VStatic) {
part.elm = prevParts![i].elm;
// Refs must be updated after every render due to refVNodes getting reset before every render
applyRefs(part, currPartsOwner);
patchAttributes(prevParts![i], part, renderer);
}
}

/**
* Mounts the hydration specific attributes
* @param vnode the parent VStatic node
* @param renderer the renderer to use
*/
export function hydrateStaticParts(vnode: VStatic, renderer: RendererAPI): void {
if (!process.env.IS_BROWSER) {
return;
}

const { parts, owner } = vnode;
if (isUndefined(parts)) {
return;
}

// Note, hydration doesn't patch attributes because hydration validation occurs before this routine
// 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);
}
}
2 changes: 1 addition & 1 deletion packages/@lwc/engine-core/src/framework/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ function patchStatic(n1: VStatic, n2: VStatic, renderer: RendererAPI) {
// slotAssignments can only apply to the top level element, never to a static part.
patchSlotAssignment(n1, n2, renderer);
// The `refs` object is blown away in every re-render, so we always need to re-apply them
patchStaticParts(n1, n2);
patchStaticParts(n1, n2, renderer);
}

function patchElement(n1: VElement, n2: VElement, renderer: RendererAPI) {
Expand Down
Loading

0 comments on commit c49478f

Please sign in to comment.