Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3ce5ee7
fix: avoid marking subtree as dynamic for inlined attributes
paoloricciuti Nov 11, 2024
509ca70
fix: i'm a silly goose 🪿
paoloricciuti Nov 11, 2024
5770063
chore: refactor `is_inlinable_expression` to accept the attribute
paoloricciuti Nov 11, 2024
dec07f0
feat: inline dom expression too
paoloricciuti Nov 12, 2024
72244cd
fix: special case literals with `"` in it and fix standalone case
paoloricciuti Nov 12, 2024
63d96ca
chore: simpler check first
paoloricciuti Nov 12, 2024
d920a23
typo
Rich-Harris Nov 13, 2024
a8b38c0
add more stuff to snapshot test
Rich-Harris Nov 14, 2024
d52bfe7
simplify/speedup by doing the work once, during analysis
Rich-Harris Nov 14, 2024
406a530
simplify
Rich-Harris Nov 14, 2024
0e543de
simplify - no reason these cases should prevent inlining
Rich-Harris Nov 14, 2024
c445309
return template
Rich-Harris Nov 14, 2024
0e35df7
name is incorrect
Rich-Harris Nov 14, 2024
ecfcb10
name is incorrect
Rich-Harris Nov 14, 2024
ee6f03e
fix escaping
Rich-Harris Nov 14, 2024
76e3e8e
no longer necessary
Rich-Harris Nov 14, 2024
61ec033
remove obsolete description
Rich-Harris Nov 14, 2024
3e64dc5
better concatenation
Rich-Harris Nov 14, 2024
8987391
fix test
Rich-Harris Nov 14, 2024
2e13a2b
do the work at runtime
Rich-Harris Nov 14, 2024
cff8b1c
fix another thing
Rich-Harris Nov 14, 2024
2ee6cd3
tidy
Rich-Harris Nov 14, 2024
b296137
tidy up
Rich-Harris Nov 14, 2024
fc91139
simplify
Rich-Harris Nov 14, 2024
1a720ad
simplify
Rich-Harris Nov 14, 2024
eaa2df5
fix
Rich-Harris Nov 14, 2024
6f77bd2
note to self
Rich-Harris Nov 14, 2024
ce63d39
another
Rich-Harris Nov 14, 2024
7f0ca11
simplify
Rich-Harris Nov 14, 2024
839e7e3
more accurate name
Rich-Harris Nov 14, 2024
ca3d6a6
simplify
Rich-Harris Nov 14, 2024
c5e3548
simplify
Rich-Harris Nov 14, 2024
785f60d
explain what is happening
Rich-Harris Nov 14, 2024
6f1d6be
tidy up
Rich-Harris Nov 14, 2024
3acb74f
simplify
Rich-Harris Nov 14, 2024
4b8c249
better inlining
Rich-Harris Nov 14, 2024
5daef9b
update test
Rich-Harris Nov 14, 2024
16ec011
colocate some code
Rich-Harris Nov 14, 2024
056475f
better inlining
Rich-Harris Nov 14, 2024
73d6384
use attribute metadata
Rich-Harris Nov 14, 2024
cf7c15c
Update packages/svelte/src/compiler/phases/2-analyze/visitors/Identif…
Rich-Harris Nov 14, 2024
61b60ab
Apply suggestions from code review
Rich-Harris Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spotty-sheep-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: better inlining of static attributes
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand All @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 });
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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' &&
Expand Down Expand Up @@ -87,6 +84,12 @@ export function Identifier(node, context) {
}
}

// no binding means global, and we can't inline e.g. `<span>{location}</span>`
// 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);
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
52 changes: 6 additions & 46 deletions packages/svelte/src/compiler/phases/3-transform/client/utils.js
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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 `<script module>` block
!binding.scope.parent &&
// to prevent the need for escaping
binding.initial?.type === 'Literal'
);
}

/**
* @param {(AST.Text | AST.ExpressionTag) | (AST.Text | AST.ExpressionTag)[]} node_or_nodes
* @param {import('./types.js').ComponentClientTransformState} state
*/
export function is_inlinable_expression(node_or_nodes, state) {
let nodes = Array.isArray(node_or_nodes) ? node_or_nodes : [node_or_nodes];
let has_expression_tag = false;
for (let value of nodes) {
if (value.type === 'ExpressionTag') {
if (value.expression.type === 'Identifier') {
const binding = state.scope
.owner(value.expression.name)
?.declarations.get(value.expression.name);
if (!can_inline_variable(binding)) {
return false;
}
} else {
return false;
}
has_expression_tag = true;
}
}
return has_expression_tag;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @import { Expression, Identifier, Statement } from 'estree' */
/** @import { Expression, Identifier, Statement, TemplateElement } from 'estree' */
/** @import { AST, Namespace } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
Expand Down Expand Up @@ -141,14 +141,14 @@ export function Fragment(node, context) {
const id = b.id(context.state.scope.generate('fragment'));

const use_space_template =
trimmed.some((node) => node.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
trimmed.some((node) => node.type === 'ExpressionTag' && !node.metadata.expression.can_inline);

if (use_space_template) {
// special case — we can use `$.text` instead of creating a unique template
const id = b.id(context.state.scope.generate('text'));

process_children(trimmed, () => id, false, {
process_children(trimmed, () => id, null, {
...context,
state
});
Expand All @@ -158,12 +158,12 @@ export function Fragment(node, context) {
} else {
if (is_standalone) {
// no need to create a template, we can just use the existing block's anchor
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
process_children(trimmed, () => b.id('$$anchor'), null, { ...context, state });
} else {
/** @type {(is_text: boolean) => Expression} */
const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);

process_children(trimmed, expression, false, { ...context, state });
process_children(trimmed, expression, null, { ...context, state });

let flags = TEMPLATE_FRAGMENT;

Expand Down Expand Up @@ -212,12 +212,34 @@ function join_template(items) {
let quasi = b.quasi('');
const template = b.template([quasi], []);

/**
* @param {Expression} expression
*/
function push(expression) {
if (expression.type === 'TemplateLiteral') {
for (let i = 0; i < expression.expressions.length; i += 1) {
const q = expression.quasis[i];
const e = expression.expressions[i];

quasi.value.cooked += /** @type {string} */ (q.value.cooked);
push(e);
}

const last = /** @type {TemplateElement} */ (expression.quasis.at(-1));
quasi.value.cooked += /** @type {string} */ (last.value.cooked);
} else if (expression.type === 'Literal') {
/** @type {string} */ (quasi.value.cooked) += expression.value;
} else {
template.expressions.push(expression);
template.quasis.push((quasi = b.quasi('')));
}
}

for (const item of items) {
if (typeof item === 'string') {
quasi.value.cooked += item;
} else {
template.expressions.push(item);
template.quasis.push((quasi = b.quasi('')));
push(item);
}
}

Expand Down
Loading
Loading