diff --git a/.changeset/spotty-sheep-fetch.md b/.changeset/spotty-sheep-fetch.md new file mode 100644 index 0000000000..8a23f40e3a --- /dev/null +++ b/.changeset/spotty-sheep-fetch.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: better inlining of static attributes diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 2a281a1aa3..6931f873fb 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -1,7 +1,7 @@ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */ /** @import { AST, DelegatedEvent, SvelteNode } from '#compiler' */ /** @import { Context } from '../types' */ -import { is_capture_event, is_delegated } from '../../../../utils.js'; +import { is_boolean_attribute, is_capture_event, is_delegated } from '../../../../utils.js'; import { get_attribute_chunks, get_attribute_expression, @@ -16,14 +16,23 @@ import { mark_subtree_dynamic } from './shared/fragment.js'; export function Attribute(node, context) { context.next(); + const parent = /** @type {SvelteNode} */ (context.path.at(-1)); + // special case if (node.name === 'value') { - const parent = /** @type {SvelteNode} */ (context.path.at(-1)); if (parent.type === 'RegularElement' && parent.name === 'option') { mark_subtree_dynamic(context.path); } } + if (node.name.startsWith('on')) { + mark_subtree_dynamic(context.path); + } + + if (parent.type === 'RegularElement' && is_boolean_attribute(node.name.toLowerCase())) { + node.metadata.expression.can_inline = false; + } + if (node.value !== true) { for (const chunk of get_attribute_chunks(node.value)) { if (chunk.type !== 'ExpressionTag') continue; @@ -37,6 +46,7 @@ export function Attribute(node, context) { node.metadata.expression.has_state ||= chunk.metadata.expression.has_state; node.metadata.expression.has_call ||= chunk.metadata.expression.has_call; + node.metadata.expression.can_inline &&= chunk.metadata.expression.can_inline; } if (is_event_attribute(node)) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 957b27ae9b..2ae32e80e1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -178,6 +178,7 @@ export function CallExpression(node, context) { if (!is_pure(node.callee, context) || context.state.expression.dependencies.size > 0) { context.state.expression.has_call = true; context.state.expression.has_state = true; + context.state.expression.can_inline = false; } } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js index 32c8d2ca36..f59b7fc569 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js @@ -2,7 +2,6 @@ /** @import { Context } from '../types' */ import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js'; import * as e from '../../../errors.js'; -import { mark_subtree_dynamic } from './shared/fragment.js'; /** * @param {AST.ExpressionTag} node @@ -15,9 +14,5 @@ export function ExpressionTag(node, context) { } } - // TODO ideally we wouldn't do this here, we'd just do it on encountering - // an `Identifier` within the tag. But we currently need to handle `{42}` etc - mark_subtree_dynamic(context.path); - context.next({ ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index 79dccd5a7c..635f939c75 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -1,5 +1,4 @@ /** @import { Expression, Identifier } from 'estree' */ -/** @import { EachBlock } from '#compiler' */ /** @import { Context } from '../types' */ import is_reference from 'is-reference'; import { should_proxy } from '../../3-transform/client/utils.js'; @@ -20,8 +19,6 @@ export function Identifier(node, context) { return; } - mark_subtree_dynamic(context.path); - // If we are using arguments outside of a function, then throw an error if ( node.name === 'arguments' && @@ -87,6 +84,12 @@ export function Identifier(node, context) { } } + // no binding means global, and we can't inline e.g. `{location}` + // because it could change between component renders. if there _is_ a + // binding and it is outside module scope, the expression cannot + // be inlined (TODO allow inlining in more cases - e.g. primitive consts) + let can_inline = !!binding && !binding.scope.parent && binding.kind === 'normal'; + if (binding) { if (context.state.expression) { context.state.expression.dependencies.add(binding); @@ -122,4 +125,17 @@ export function Identifier(node, context) { w.reactive_declaration_module_script_dependency(node); } } + + if (!can_inline && context.state.expression) { + 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 (!can_inline || !context.path.find((node) => node.type === 'Attribute')) { + mark_subtree_dynamic(context.path); + } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js index 171a1106a8..adcc2da422 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js @@ -19,6 +19,7 @@ export function MemberExpression(node, context) { if (context.state.expression && !is_pure(node, context)) { context.state.expression.has_state = true; + context.state.expression.can_inline = false; } if (!is_safe_identifier(node, context.state.scope)) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js index eacb8a342a..724b9af311 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js @@ -10,6 +10,7 @@ export function TaggedTemplateExpression(node, context) { if (context.state.expression && !is_pure(node.tag, context)) { context.state.expression.has_call = true; context.state.expression.has_state = true; + context.state.expression.can_inline = false; } if (node.tag.type === 'Identifier') { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index c460905977..910f173f79 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,18 +1,18 @@ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */ -/** @import { AST, Binding, SvelteNode } from '#compiler' */ +/** @import { Binding, SvelteNode } from '#compiler' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { Analysis } from '../../types.js' */ /** @import { Scope } from '../../scope.js' */ -import * as b from '../../../utils/builders.js'; -import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js'; import { - PROPS_IS_LAZY_INITIAL, + PROPS_IS_BINDABLE, PROPS_IS_IMMUTABLE, + PROPS_IS_LAZY_INITIAL, PROPS_IS_RUNES, - PROPS_IS_UPDATED, - PROPS_IS_BINDABLE + PROPS_IS_UPDATED } from '../../../../constants.js'; import { dev } from '../../../state.js'; +import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js'; +import * as b from '../../../utils/builders.js'; import { get_value } from './visitors/shared/declarations.js'; /** @@ -311,43 +311,3 @@ export function create_derived_block_argument(node, context) { export function create_derived(state, arg) { return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); } - -/** - * Whether a variable can be referenced directly from template string. - * @param {import('#compiler').Binding | undefined} binding - * @returns {boolean} - */ -export function can_inline_variable(binding) { - return ( - !!binding && - // in a ` + +
{a} + {b} = {a + b}