Skip to content

Commit

Permalink
feat: memoize cmp bound props in scope
Browse files Browse the repository at this point in the history
  • Loading branch information
jodarove committed Jul 15, 2021
1 parent b9258ee commit 8f4ed06
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 41 deletions.
35 changes: 35 additions & 0 deletions packages/@lwc/template-compiler/src/codegen/NodeRefProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2018, 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
*/
export class NodeRefProxy {
instance: any;
target: any;

constructor(target: unknown) {
this.target = target;
this.instance = new Proxy(
{},
{
has: (dummy: any, property: PropertyKey) => {
return property in this.target;
},

get: (dummy: any, property: PropertyKey) => {
return this.target[property];
},

set: (dummy: any, property: PropertyKey, value: any) => {
this.target[property] = value;
return true;
},
}
);
}

swap(newTarget: any) {
this.target = newTarget;
}
}
6 changes: 5 additions & 1 deletion packages/@lwc/template-compiler/src/codegen/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as esutils from 'esutils';
import { ResolvedConfig } from '../config';

import * as t from '../shared/estree';
import { IRElement, LWCDirectiveRenderMode } from '../shared/types';
import { IRElement, IRNode, LWCDirectiveRenderMode, TemplateExpression } from '../shared/types';
import { toPropertyName } from '../shared/utils';
import { Scope } from './scope';

Expand Down Expand Up @@ -119,6 +119,10 @@ export default class CodeGen {
return this.currentKey++;
}

genBindExpression(expression: TemplateExpression, irNode: IRNode): t.Expression {
return this.currentScope.bindExpression(expression, irNode);
}

genElement(tagName: string, data: t.ObjectExpression, children: t.Expression) {
return this._renderApiCall(RENDER_APIS.element, [t.literal(tagName), data, children]);
}
Expand Down
23 changes: 12 additions & 11 deletions packages/@lwc/template-compiler/src/codegen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
} from '../shared/types';

import CodeGen from './codegen';
import { bindExpression } from './scope';
import {
identifierFromComponentName,
objectToAST,
Expand Down Expand Up @@ -64,7 +63,7 @@ function transform(codeGen: CodeGen): t.Expression {

// Check wether it has the special directive lwc:dynamic
if (element.lwc && element.lwc.dynamic) {
const expression = bindExpression(element.lwc.dynamic, element);
const expression = codeGen.genBindExpression(element.lwc.dynamic, element);
res = codeGen.genDynamicElement(element.tag, expression, databag, children);
} else if (isCustomElement(element)) {
// Make sure to register the component
Expand Down Expand Up @@ -114,7 +113,9 @@ function transform(codeGen: CodeGen): t.Expression {

function transformText(text: IRText): t.Expression {
const { value } = text;
return codeGen.genText(typeof value === 'string' ? value : bindExpression(value, text));
return codeGen.genText(
typeof value === 'string' ? value : codeGen.genBindExpression(value, text)
);
}

function transformComment(comment: IRComment): t.Expression {
Expand Down Expand Up @@ -173,7 +174,7 @@ function transform(codeGen: CodeGen): t.Expression {

codeGen.popScope();

const testExpression = bindExpression(element.if!, element);
const testExpression = codeGen.genBindExpression(element.if!, element);

let leftExpression: t.Expression;
const modifier = element.ifModifier!;
Expand Down Expand Up @@ -214,7 +215,7 @@ function transform(codeGen: CodeGen): t.Expression {
);
codeGen.popScope();

const iterable = bindExpression(expression, element);
const iterable = codeGen.genBindExpression(expression, element);

return codeGen.genIterator(iterable, iterationFunction);
}
Expand Down Expand Up @@ -254,7 +255,7 @@ function transform(codeGen: CodeGen): t.Expression {

codeGen.popScope();

const iterable = bindExpression(expression, element);
const iterable = codeGen.genBindExpression(expression, element);

return codeGen.genIterator(iterable, iterationFunction);
}
Expand Down Expand Up @@ -311,7 +312,7 @@ function transform(codeGen: CodeGen): t.Expression {

switch (attr.type) {
case IRAttributeType.Expression: {
const expression = bindExpression(attr.value, element);
const expression = codeGen.genBindExpression(attr.value, element);

// TODO [#2012]: Normalize global boolean attrs values passed to custom elements as props
if (isUsedAsAttribute && isBooleanAttribute(attr.name, tagName)) {
Expand Down Expand Up @@ -408,7 +409,7 @@ function transform(codeGen: CodeGen): t.Expression {
// - string values are parsed and turned into a `classMap` object associating
// each individual class name with a `true` boolean.
if (value.type === IRAttributeType.Expression) {
const classExpression = bindExpression(value.value, element);
const classExpression = codeGen.genBindExpression(value.value, element);
data.push(t.property(t.identifier('className'), classExpression));
} else if (value.type === IRAttributeType.String) {
const classNames = parseClassNames(value.value);
Expand All @@ -423,7 +424,7 @@ function transform(codeGen: CodeGen): t.Expression {
// - string values are parsed and turned into a `styleMap` object associating
// each property name with its value.
if (value.type === IRAttributeType.Expression) {
const styleExpression = bindExpression(value.value, element);
const styleExpression = codeGen.genBindExpression(value.value, element);
data.push(t.property(t.identifier('style'), styleExpression));
} else if (value.type === IRAttributeType.String) {
const styleMap = parseStyleText(value.value);
Expand Down Expand Up @@ -463,7 +464,7 @@ function transform(codeGen: CodeGen): t.Expression {
// Key property on VNode
if (forKey) {
// If element has user-supplied `key` or is in iterator, call `api.k`
const forKeyExpression = bindExpression(forKey, element);
const forKeyExpression = codeGen.genBindExpression(forKey, element);
const generatedKey = codeGen.genKey(t.literal(codeGen.generateKey()), forKeyExpression);
data.push(t.property(t.identifier('key'), generatedKey));
} else {
Expand All @@ -475,7 +476,7 @@ function transform(codeGen: CodeGen): t.Expression {
// Event handler
if (on) {
const onObj = objectToAST(on, (key) => {
const componentHandler = bindExpression(on[key], element);
const componentHandler = codeGen.genBindExpression(on[key], element);
const handler = codeGen.genBind(componentHandler);

return memorizeHandler(codeGen, element, componentHandler, handler);
Expand Down
154 changes: 125 additions & 29 deletions packages/@lwc/template-compiler/src/codegen/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,57 @@ import { walk } from 'estree-walker';
import * as t from '../shared/estree';
import { TEMPLATE_PARAMS } from '../shared/constants';
import { isComponentProp } from '../shared/ir';
import { IRNode, TemplateExpression } from '../shared/types';
import { IRNode, TemplateExpression, TemplateIdentifier } from '../shared/types';
import { NodeRefProxy } from './NodeRefProxy';

interface ComponentPropsUsageData {
name: string;
localName: string;
usage: NodeRefProxy;
instances: number;
}

function dumpScope(scope: Scope, body: t.Statement[], scopeVars: Map<string, string>) {
const nextScopeVars = new Map(scopeVars);
const variablesInThisScope: [string, string][] = [];

for (const [name, propData] of scope.usedProps) {
// the variable is already defined in the scope.
const generatedNameInScope = scopeVars.get(name);
if (generatedNameInScope !== undefined) {
propData.usage.swap(t.identifier(generatedNameInScope));
} else if (propData.instances > 1 || scope.aggregatedPropNamesUsedInChildScopes.has(name)) {
// name is not defined in the ancestor scopes, and is:
// a) used multiple times in this scope.
// b) used one time in this scope and at least once in one of the child scopes. Therefore
// we can compute it in this scope.
const { localName } = propData;

nextScopeVars.set(name, localName);
propData.usage.swap(t.identifier(localName));
variablesInThisScope.push([name, localName]);
}
}

function dumpScope(scope: Scope, body: t.Statement[]) {
for (const childScope of scope.childScopes) {
body.unshift(childScope.scopeFn!);
dumpScope(childScope, childScope.scopeFn?.body!.body!);
dumpScope(childScope, childScope.scopeFn?.body!.body!, nextScopeVars);
}

// lastly, insert variables defined in this scope at the beginning of the body.
if (variablesInThisScope.length > 0) {
body.unshift(
t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern(
variablesInThisScope.map(([name, localName]) =>
t.assignmentProperty(t.identifier(name), t.identifier(localName))
)
),
t.identifier(TEMPLATE_PARAMS.INSTANCE)
),
])
);
}
}

Expand All @@ -24,6 +69,31 @@ export class Scope {
childScopes: Scope[] = [];
scopeFn: t.FunctionDeclaration | null = null;

usedProps = new Map<string, ComponentPropsUsageData>();
private cachedAggregatedProps: Set<string> | undefined;

private getPropName(identifier: TemplateIdentifier): t.MemberExpression | t.Identifier {
const { name } = identifier;
let memoizedPropName = this.usedProps.get(name);

if (!memoizedPropName) {
const generatedExpr = new NodeRefProxy(
t.memberExpression(t.identifier(TEMPLATE_PARAMS.INSTANCE), identifier)
);
memoizedPropName = {
name,
localName: `$cv${this.id}_${this.usedProps.size}`,
usage: generatedExpr,
instances: 0,
};
this.usedProps.set(name, memoizedPropName);
}

memoizedPropName.instances++;

return memoizedPropName.usage.instance;
}

constructor(id: number) {
this.id = id;
}
Expand All @@ -41,37 +111,63 @@ export class Scope {
}

serializeInto(body: t.Statement[]) {
dumpScope(this, body);
dumpScope(this, body, new Map());
}
}

/**
* Bind the passed expression to the component instance. It applies the following transformation to the expression:
* - {value} --> {$cmp.value}
* - {value[index]} --> {$cmp.value[$cmp.index]}
*/
export function bindExpression(expression: TemplateExpression, irNode: IRNode): t.Expression {
if (t.isIdentifier(expression)) {
if (isComponentProp(expression, irNode)) {
return t.memberExpression(t.identifier(TEMPLATE_PARAMS.INSTANCE), expression);
} else {
return expression;
/**
* Bind the passed expression to the component instance. It applies the following transformation to the expression:
* - {value} --> {$cmp.value}
* - {value[index]} --> {$cmp.value[$cmp.index]}
*/
bindExpression(expression: TemplateExpression, irNode: IRNode): t.Expression {
const self = this;
if (t.isIdentifier(expression)) {
if (isComponentProp(expression, irNode)) {
return this.getPropName(expression);
} else {
return expression;
}
}

walk(expression, {
leave(node, parent) {
if (
parent !== null &&
t.isIdentifier(node) &&
t.isMemberExpression(parent) &&
parent.object === node &&
isComponentProp(node, irNode)
) {
this.replace(self.getPropName(node));
}
},
});

return expression;
}

walk(expression, {
leave(node, parent) {
if (
parent !== null &&
t.isIdentifier(node) &&
t.isMemberExpression(parent) &&
parent.object === node &&
isComponentProp(node, irNode)
) {
this.replace(t.memberExpression(t.identifier(TEMPLATE_PARAMS.INSTANCE), node));
/**
* Returns a set of used component properties in descendant scopes.
* Note: Does not includes the current scope.
*/
get aggregatedPropNamesUsedInChildScopes(): Set<string> {
if (this.cachedAggregatedProps === undefined) {
const aggregatedScope = new Set<string>();
this.cachedAggregatedProps = aggregatedScope;

for (const scope of this.childScopes) {
// Aggregated props is defined as:
// props used in child scopes + props used in the aggregated props of child scope.
for (const [propName] of scope.usedProps) {
aggregatedScope.add(propName);
}

for (const propName of scope.aggregatedPropNamesUsedInChildScopes) {
aggregatedScope.add(propName);
}
}
},
});
}

return expression;
return this.cachedAggregatedProps;
}
}

0 comments on commit 8f4ed06

Please sign in to comment.