simplify/speedup by doing the work once, during analysis

pull/14269/head
Rich Harris 2 years ago
parent a8b38c06d3
commit d52bfe734e

@ -24,6 +24,10 @@ export function Attribute(node, context) {
} }
} }
if (node.name.startsWith('on')) {
mark_subtree_dynamic(context.path);
}
if (node.value !== true) { if (node.value !== true) {
for (const chunk of get_attribute_chunks(node.value)) { for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue; if (chunk.type !== 'ExpressionTag') continue;

@ -178,6 +178,7 @@ export function CallExpression(node, context) {
if (!is_pure(node.callee, context) || context.state.expression.dependencies.size > 0) { if (!is_pure(node.callee, context) || context.state.expression.dependencies.size > 0) {
context.state.expression.has_call = true; context.state.expression.has_call = true;
context.state.expression.has_state = true; context.state.expression.has_state = true;
context.state.expression.can_inline = false;
} }
} }
} }

@ -23,15 +23,6 @@ export function Identifier(node, context) {
const attribute_parent = context.path.find((parent) => parent.type === 'Attribute'); const attribute_parent = context.path.find((parent) => parent.type === 'Attribute');
/**
* if the identifier is part of an expression tag of an attribute we want to check if it's inlinable
* before marking the subtree as dynamic. This is because if it's inlinable it will be inlined in the template
* directly making the whole thing actually static.
*/
if (!attribute_parent || !is_inlinable_expression(attribute_parent.value, context.state.scope)) {
mark_subtree_dynamic(context.path);
}
// If we are using arguments outside of a function, then throw an error // If we are using arguments outside of a function, then throw an error
if ( if (
node.name === 'arguments' && node.name === 'arguments' &&
@ -101,6 +92,13 @@ export function Identifier(node, context) {
if (context.state.expression) { if (context.state.expression) {
context.state.expression.dependencies.add(binding); context.state.expression.dependencies.add(binding);
context.state.expression.has_state ||= binding.kind !== 'normal'; context.state.expression.has_state ||= binding.kind !== 'normal';
// if the binding is outside module scope, the expression
// cannot be inlined (TODO allow inlining in more cases,
// e.g. primitive consts)
if (!!binding.scope.parent) {
context.state.expression.can_inline = false;
}
} }
if ( if (
@ -131,5 +129,18 @@ export function Identifier(node, context) {
) { ) {
w.reactive_declaration_module_script_dependency(node); w.reactive_declaration_module_script_dependency(node);
} }
} else if (context.state.expression) {
// no binding means global, and we can't inline e.g. `<span>{location}</span>`
// because it could change between component renders
context.state.expression.can_inline = false;
}
/**
* if the identifier is part of an expression tag of an attribute we want to check if it's inlinable
* before marking the subtree as dynamic. This is because if it's inlinable it will be inlined in the template
* directly making the whole thing actually static.
*/
if (!attribute_parent || !is_inlinable_expression(attribute_parent.value)) {
mark_subtree_dynamic(context.path);
} }
} }

@ -19,6 +19,7 @@ export function MemberExpression(node, context) {
if (context.state.expression && !is_pure(node, context)) { if (context.state.expression && !is_pure(node, context)) {
context.state.expression.has_state = true; context.state.expression.has_state = true;
context.state.expression.can_inline = false;
} }
if (!is_safe_identifier(node, context.state.scope)) { if (!is_safe_identifier(node, context.state.scope)) {

@ -10,6 +10,7 @@ export function TaggedTemplateExpression(node, context) {
if (context.state.expression && !is_pure(node.tag, context)) { if (context.state.expression && !is_pure(node.tag, context)) {
context.state.expression.has_call = true; context.state.expression.has_call = true;
context.state.expression.has_state = true; context.state.expression.has_state = true;
context.state.expression.can_inline = false;
} }
if (node.tag.type === 'Identifier') { if (node.tag.type === 'Identifier') {

@ -144,7 +144,7 @@ export function Fragment(node, context) {
const use_space_template = const use_space_template =
trimmed.some((node) => node.type === 'ExpressionTag') && trimmed.some((node) => node.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') && trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
!is_inlinable_expression(trimmed, context.state.scope); !is_inlinable_expression(trimmed);
if (use_space_template) { if (use_space_template) {
// special case — we can use `$.text` instead of creating a unique template // special case — we can use `$.text` instead of creating a unique template

@ -363,7 +363,7 @@ export function RegularElement(node, context) {
if (states_and_calls && states_and_calls.states === 0) { if (states_and_calls && states_and_calls.states === 0) {
let { value } = build_template_literal(trimmed, context.visit, child_state); let { value } = build_template_literal(trimmed, context.visit, child_state);
// if the expression is inlinable we just push it to the template // if the expression is inlinable we just push it to the template
if (is_inlinable_expression(trimmed, context.state.scope)) { if (is_inlinable_expression(trimmed)) {
escape_template_quasis(value); escape_template_quasis(value);
state.template.push(value); state.template.push(value);
} else { } else {
@ -381,7 +381,7 @@ export function RegularElement(node, context) {
let needs_reset = let needs_reset =
trimmed.some((node) => node.type !== 'Text') && trimmed.some((node) => node.type !== 'Text') &&
(!trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') || (!trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') ||
!is_inlinable_expression(trimmed, context.state.scope)); !is_inlinable_expression(trimmed));
// The same applies if it's a `<template>` element, since we need to // The same applies if it's a `<template>` element, since we need to
// set the value of `hydrate_node` to `node.content` // set the value of `hydrate_node` to `node.content`
@ -586,7 +586,7 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
// we need to special case textarea value because it's not an actual attribute // we need to special case textarea value because it's not an actual attribute
const inlinable_expression = const inlinable_expression =
is_inlinable_expression(attribute.value, context.state.scope) && is_inlinable_expression(attribute.value) &&
attribute.name !== 'value' && attribute.name !== 'value' &&
element.name !== 'textarea'; element.name !== 'textarea';
if (attribute.metadata.expression.has_state) { if (attribute.metadata.expression.has_state) {

@ -84,7 +84,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
state.update.push(update); state.update.push(update);
} else { } else {
// if the expression is inlinable we just push it to the template // if the expression is inlinable we just push it to the template
if (!is_text && is_inlinable_expression(sequence, state.scope)) { if (!is_text && is_inlinable_expression(sequence)) {
escape_template_quasis(value); escape_template_quasis(value);
state.template.push(value); state.template.push(value);
} else { } else {
@ -166,7 +166,7 @@ function is_static_element(node, state) {
!is_text_attribute(attribute) && !is_text_attribute(attribute) &&
// If the attribute is not a text attribute but is inlinable we will directly inline it in the // If the attribute is not a text attribute but is inlinable we will directly inline it in the
// the template so before returning false we need to check that the attribute is not inlinable // the template so before returning false we need to check that the attribute is not inlinable
!is_inlinable_expression(attribute.value, state.scope) !is_inlinable_expression(attribute.value)
) { ) {
return false; return false;
} }

@ -58,6 +58,7 @@ export function create_expression_metadata() {
return { return {
dependencies: new Set(), dependencies: new Set(),
has_state: false, has_state: false,
has_call: false has_call: false,
can_inline: true
}; };
} }

@ -1,68 +1,19 @@
/** @import { AST, Binding } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Scope } from './scope' */
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
/**
* @param {ESTree.Expression} expr
*/
export function extract_identifiers(expr) {
/** @type {ESTree.Identifier[]} */
let nodes = [];
walk(
expr,
{},
{
Identifier(node, { path }) {
const parent = path.at(-1);
if (parent?.type !== 'MemberExpression' || parent.property !== node || parent.computed) {
nodes.push(node);
}
}
}
);
return nodes;
}
/**
* Whether a variable can be referenced directly from template string.
* @param {Binding | undefined} binding
* @returns {boolean}
*/
function can_inline_variable(binding) {
return (
!!binding &&
// in a `<script module>` block
!binding.scope.parent &&
// to prevent the need for escaping
binding.initial?.type === 'Literal'
);
}
/** /**
* @param {AST.Attribute["value"]} value * @param {AST.Attribute["value"]} value
* @param {Scope} scope
*/ */
export function is_inlinable_expression(value, scope) { export function is_inlinable_expression(value) {
if (value === true) return false; // not an expression if (value === true) return false; // not an expression
let nodes = Array.isArray(value) ? value : [value]; let nodes = Array.isArray(value) ? value : [value];
let has_expression_tag = false; let has_expression_tag = false;
for (let value of nodes) { for (let value of nodes) {
if (value.type === 'ExpressionTag') { if (value.type === 'ExpressionTag') {
const identifiers = extract_identifiers(value.expression); if (!value.metadata.expression.can_inline) {
// if not every identifier is inlinable we bail
if (
identifiers.length > 0 &&
identifiers.some((id) => {
const binding = scope.owner(id.name)?.declarations.get(id.name);
return !can_inline_variable(binding);
})
) {
return false; return false;
} else if ( }
if (
// we need to special case null and boolean values because // we need to special case null and boolean values because
// we want to set them programmatically // we want to set them programmatically
value.expression.type === 'Literal' && value.expression.type === 'Literal' &&
@ -74,6 +25,7 @@ export function is_inlinable_expression(value, scope) {
) { ) {
return false; return false;
} }
has_expression_tag = true; has_expression_tag = true;
} }
} }

@ -317,6 +317,8 @@ export interface ExpressionMetadata {
has_state: boolean; has_state: boolean;
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call: boolean; has_call: boolean;
/** True if the expression can be inlined into a template */
can_inline: boolean;
} }
export * from './template.js'; export * from './template.js';

Loading…
Cancel
Save