diff --git a/.changeset/spotty-sheep-fetch.md b/.changeset/spotty-sheep-fetch.md
new file mode 100644
index 000000000000..8a23f40e3a42
--- /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 2a281a1aa3d9..6931f873fbf7 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 957b27ae9bc8..2ae32e80e1ba 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 32c8d2ca3671..f59b7fc5692b 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 79dccd5a7cf5..635f939c7520 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 171a1106a8ce..adcc2da4226e 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 eacb8a342ac2..724b9af31185 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 c46090597709..910f173f79ae 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}