inline into template attributes

hoist-unmodified-var
Ben McCann 2 years ago
parent e0af98ba51
commit 57f1977051

@ -2,4 +2,4 @@
'svelte': patch 'svelte': patch
--- ---
perf: hoist variables which are not mutated or reassigned perf: hoist primitives which are not mutated or reassigned and inline into template attributes

@ -3,7 +3,8 @@ import type {
Statement, Statement,
LabeledStatement, LabeledStatement,
Identifier, Identifier,
PrivateIdentifier PrivateIdentifier,
Expression
} from 'estree'; } from 'estree';
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler'; import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
@ -45,7 +46,10 @@ export interface ComponentClientTransformState extends ClientTransformState {
/** Stuff that happens after the render effect (bindings, actions) */ /** Stuff that happens after the render effect (bindings, actions) */
readonly after_update: Statement[]; readonly after_update: Statement[];
/** The HTML template string */ /** The HTML template string */
readonly template: string[]; readonly template: {
quasi: string[];
expressions: Expression[];
};
readonly metadata: { readonly metadata: {
namespace: Namespace; namespace: Namespace;
/** `true` if the HTML template needs to be instantiated with `importNode` */ /** `true` if the HTML template needs to be instantiated with `importNode` */

@ -532,3 +532,21 @@ export function should_proxy_or_freeze(node) {
return true; return true;
} }
/**
* @param {import('#compiler').Binding | undefined} binding
* @param {import('#compiler').SvelteNode[]} path
* @returns {boolean}
*/
export function is_hoistable_declaration(binding, path) {
const is_top_level = path.at(-1)?.type === 'Program';
// TODO: allow object expressions that are not passed to functions or components as props
// and expressions as long as they do not reference non-hoistable variables
return (
is_top_level &&
!!binding &&
!binding.mutated &&
!binding.reassigned &&
binding?.initial?.type === 'Literal'
);
}

@ -2,7 +2,12 @@ import { get_rune } from '../../../scope.js';
import { is_hoistable_function, transform_inspect_rune } from '../../utils.js'; import { is_hoistable_function, transform_inspect_rune } from '../../utils.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import * as assert from '../../../../utils/assert.js'; import * as assert from '../../../../utils/assert.js';
import { create_state_declarators, get_prop_source, should_proxy_or_freeze } from '../utils.js'; import {
create_state_declarators,
get_prop_source,
is_hoistable_declaration,
should_proxy_or_freeze
} from '../utils.js';
import { unwrap_ts_expression } from '../../../../utils/ast.js'; import { unwrap_ts_expression } from '../../../../utils/ast.js';
/** @type {import('../types.js').ComponentVisitors} */ /** @type {import('../types.js').ComponentVisitors} */
@ -161,15 +166,7 @@ export const javascript_visitors_runes = {
if (!rune && init != null && declarator.id.type === 'Identifier') { if (!rune && init != null && declarator.id.type === 'Identifier') {
const is_top_level = path.at(-1)?.type === 'Program'; const is_top_level = path.at(-1)?.type === 'Program';
const binding = state.scope.owner(declarator.id.name)?.declarations.get(declarator.id.name); const binding = state.scope.owner(declarator.id.name)?.declarations.get(declarator.id.name);
// TODO: allow object expressions that are not passed to functions or components as props if (is_hoistable_declaration(binding, path)) {
// and expressions as long as they do not reference non-hoistable variables
if (
is_top_level &&
binding &&
!binding.mutated &&
!binding.reassigned &&
binding?.initial?.type === 'Literal'
) {
state.hoisted.push(b.declaration('const', declarator.id, init)); state.hoisted.push(b.declaration('const', declarator.id, init));
continue; continue;
} }

@ -19,6 +19,7 @@ import { error } from '../../../../errors.js';
import { import {
function_visitor, function_visitor,
get_assignment_value, get_assignment_value,
is_hoistable_declaration,
serialize_get_binding, serialize_get_binding,
serialize_set_binding serialize_set_binding
} from '../utils.js'; } from '../utils.js';
@ -35,6 +36,33 @@ import { regex_is_valid_identifier } from '../../../patterns.js';
import { javascript_visitors_runes } from './javascript-runes.js'; import { javascript_visitors_runes } from './javascript-runes.js';
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js'; import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
/**
* @param {import('../types.js').ComponentClientTransformState} state
* @param {string} quasi_to_add
* @returns
*/
function push_template_quasi(state, quasi_to_add) {
const { quasi } = state.template;
if (quasi.length === 0) {
quasi.push(quasi_to_add);
return;
}
quasi[quasi.length - 1] = quasi[quasi.length - 1].concat(quasi_to_add);
}
/**
* @param {import('../types.js').ComponentClientTransformState} state
* @param {import('estree').Expression} expression_to_add
*/
function push_template_expression(state, expression_to_add) {
const { expressions, quasi } = state.template;
if (quasi.length === 0) {
quasi.push('');
}
expressions.push(expression_to_add);
quasi.push('');
}
/** /**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
* @param {import('#compiler').Attribute} attribute * @param {import('#compiler').Attribute} attribute
@ -536,7 +564,18 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
} }
}; };
if (attribute.metadata.dynamic) { let is_in_hoistable = false;
if (Array.isArray(attribute.value)) {
for (let value of attribute.value) {
if (value.type === 'ExpressionTag' && value.expression.type === 'Identifier') {
const binding = context.state.scope
.owner(value.expression.name)
?.declarations.get(value.expression.name);
is_in_hoistable ||= is_hoistable_declaration(binding, context.path);
}
}
}
if (attribute.metadata.dynamic && !is_in_hoistable) {
const id = state.scope.generate(`${node_id.name}_${name}`); const id = state.scope.generate(`${node_id.name}_${name}`);
serialize_update_assignment( serialize_update_assignment(
state, state,
@ -548,7 +587,9 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
); );
return true; return true;
} else { } else {
state.init.push(assign(grouped_value).grouped); push_template_quasi(context.state, ` ${name}="`);
push_template_expression(context.state, grouped_value);
push_template_quasi(context.state, `"`);
return false; return false;
} }
} }
@ -1042,7 +1083,10 @@ function create_block(parent, name, nodes, context) {
update: [], update: [],
update_effects: [], update_effects: [],
after_update: [], after_update: [],
template: [], template: {
quasi: [],
expressions: []
},
metadata: { metadata: {
template_needs_import_node: false, template_needs_import_node: false,
namespace, namespace,
@ -1067,7 +1111,16 @@ function create_block(parent, name, nodes, context) {
const callee = namespace === 'svg' ? '$.svg_template' : '$.template'; const callee = namespace === 'svg' ? '$.svg_template' : '$.template';
context.state.hoisted.push( context.state.hoisted.push(
b.var(template_name, b.call(callee, b.template([b.quasi(state.template.join(''), true)], []))) b.var(
template_name,
b.call(
callee,
b.template(
state.template.quasi.map((quasi) => b.quasi(quasi, true)),
state.template.expressions
)
)
)
); );
body.push( body.push(
@ -1095,10 +1148,10 @@ function create_block(parent, name, nodes, context) {
state state
}); });
const template = state.template[0]; const quasi = state.template.quasi[0];
if (state.template.length === 1 && (template === ' ' || template === '<!>')) { if (state.template.quasi.length === 1 && (quasi === ' ' || quasi === '<!>')) {
if (template === ' ') { if (quasi === ' ') {
body.push(b.var(node_id, b.call('$.space', b.id('$$anchor'))), ...state.init); body.push(b.var(node_id, b.call('$.space', b.id('$$anchor'))), ...state.init);
close = b.stmt(b.call('$.close', b.id('$$anchor'), node_id)); close = b.stmt(b.call('$.close', b.id('$$anchor'), node_id));
} else { } else {
@ -1115,7 +1168,14 @@ function create_block(parent, name, nodes, context) {
state.hoisted.push( state.hoisted.push(
b.var( b.var(
template_name, template_name,
b.call(callee, b.template([b.quasi(state.template.join(''), true)], []), b.true) b.call(
callee,
b.template(
state.template.quasi.map((quasi) => b.quasi(quasi, true)),
state.template.expressions
),
b.true
)
) )
); );
@ -1433,11 +1493,11 @@ function process_children(nodes, parent, { visit, state }) {
} }
if (node.type === 'Text') { if (node.type === 'Text') {
state.template.push(node.raw); push_template_quasi(state, node.raw);
return; return;
} }
state.template.push(' '); push_template_quasi(state, ' ');
const text_id = get_node_id(expression, state, 'text'); const text_id = get_node_id(expression, state, 'text');
const singular = b.stmt( const singular = b.stmt(
@ -1479,7 +1539,7 @@ function process_children(nodes, parent, { visit, state }) {
return; return;
} }
state.template.push(' '); push_template_quasi(state, ' ');
const text_id = get_node_id(expression, state, 'text'); const text_id = get_node_id(expression, state, 'text');
const contains_call_expression = sequence.some( const contains_call_expression = sequence.some(
@ -1659,10 +1719,10 @@ export const template_visitors = {
}, },
Comment(node, context) { Comment(node, context) {
// We'll only get here if comments are not filtered out, which they are unless preserveComments is true // We'll only get here if comments are not filtered out, which they are unless preserveComments is true
context.state.template.push(`<!--${node.data}-->`); push_template_quasi(context.state, `<!--${node.data}-->`);
}, },
HtmlTag(node, context) { HtmlTag(node, context) {
context.state.template.push('<!>'); push_template_quasi(context.state, '<!>');
// push into init, so that bindings run afterwards, which might trigger another run and override hydration // push into init, so that bindings run afterwards, which might trigger another run and override hydration
context.state.init.push( context.state.init.push(
@ -1750,7 +1810,7 @@ export const template_visitors = {
); );
}, },
RenderTag(node, context) { RenderTag(node, context) {
context.state.template.push('<!>'); push_template_quasi(context.state, '<!>');
const binding = context.state.scope.get(node.expression.name); const binding = context.state.scope.get(node.expression.name);
const is_reactive = binding?.kind !== 'normal' || node.expression.type !== 'Identifier'; const is_reactive = binding?.kind !== 'normal' || node.expression.type !== 'Identifier';
@ -1823,7 +1883,7 @@ export const template_visitors = {
}, },
RegularElement(node, context) { RegularElement(node, context) {
if (node.name === 'noscript') { if (node.name === 'noscript') {
context.state.template.push('<!>'); push_template_quasi(context.state, '<!>');
return; return;
} }
@ -1833,7 +1893,7 @@ export const template_visitors = {
namespace: determine_element_namespace(node, context.state.metadata.namespace, context.path) namespace: determine_element_namespace(node, context.state.metadata.namespace, context.path)
}; };
context.state.template.push(`<${node.name}`); push_template_quasi(context.state, `<${node.name}`);
/** @type {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} */ /** @type {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} */
const attributes = []; const attributes = [];
@ -1974,7 +2034,8 @@ export const template_visitors = {
if (name !== 'class' || literal_value) { if (name !== 'class' || literal_value) {
// TODO namespace=foreign probably doesn't want to do template stuff at all and instead use programmatic methods // TODO namespace=foreign probably doesn't want to do template stuff at all and instead use programmatic methods
// to create the elements it needs. // to create the elements it needs.
context.state.template.push( push_template_quasi(
context.state,
` ${attribute.name}${ ` ${attribute.name}${
DOMBooleanAttributes.includes(name) && literal_value === true DOMBooleanAttributes.includes(name) && literal_value === true
? '' ? ''
@ -1997,7 +2058,7 @@ export const template_visitors = {
serialize_class_directives(class_directives, node_id, context, is_attributes_reactive); serialize_class_directives(class_directives, node_id, context, is_attributes_reactive);
serialize_style_directives(style_directives, node_id, context, is_attributes_reactive); serialize_style_directives(style_directives, node_id, context, is_attributes_reactive);
context.state.template.push('>'); push_template_quasi(context.state, '>');
/** @type {import('../types').ComponentClientTransformState} */ /** @type {import('../types').ComponentClientTransformState} */
const state = { const state = {
@ -2037,11 +2098,11 @@ export const template_visitors = {
); );
if (!VoidElements.includes(node.name)) { if (!VoidElements.includes(node.name)) {
context.state.template.push(`</${node.name}>`); push_template_quasi(context.state, `</${node.name}>`);
} }
}, },
SvelteElement(node, context) { SvelteElement(node, context) {
context.state.template.push(`<!>`); push_template_quasi(context.state, `<!>`);
/** @type {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} */ /** @type {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} */
const attributes = []; const attributes = [];
@ -2152,7 +2213,7 @@ export const template_visitors = {
let each_item_is_reactive = true; let each_item_is_reactive = true;
if (!each_node_meta.is_controlled) { if (!each_node_meta.is_controlled) {
context.state.template.push('<!>'); push_template_quasi(context.state, '<!>');
} }
if (each_node_meta.array_name !== null) { if (each_node_meta.array_name !== null) {
@ -2371,7 +2432,7 @@ export const template_visitors = {
} }
}, },
IfBlock(node, context) { IfBlock(node, context) {
context.state.template.push('<!>'); push_template_quasi(context.state, '<!>');
const consequent = /** @type {import('estree').BlockStatement} */ ( const consequent = /** @type {import('estree').BlockStatement} */ (
context.visit(node.consequent) context.visit(node.consequent)
@ -2395,7 +2456,7 @@ export const template_visitors = {
); );
}, },
AwaitBlock(node, context) { AwaitBlock(node, context) {
context.state.template.push('<!>'); push_template_quasi(context.state, '<!>');
context.state.after_update.push( context.state.after_update.push(
b.stmt( b.stmt(
@ -2436,7 +2497,7 @@ export const template_visitors = {
); );
}, },
KeyBlock(node, context) { KeyBlock(node, context) {
context.state.template.push('<!>'); push_template_quasi(context.state, '<!>');
const key = /** @type {import('estree').Expression} */ (context.visit(node.expression)); const key = /** @type {import('estree').Expression} */ (context.visit(node.expression));
const body = /** @type {import('estree').Expression} */ (context.visit(node.fragment)); const body = /** @type {import('estree').Expression} */ (context.visit(node.fragment));
context.state.after_update.push( context.state.after_update.push(
@ -2767,7 +2828,7 @@ export const template_visitors = {
} }
}, },
Component(node, context) { Component(node, context) {
context.state.template.push('<!>'); push_template_quasi(context.state, '<!>');
const binding = context.state.scope.get( const binding = context.state.scope.get(
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
@ -2795,12 +2856,12 @@ export const template_visitors = {
context.state.after_update.push(component); context.state.after_update.push(component);
}, },
SvelteSelf(node, context) { SvelteSelf(node, context) {
context.state.template.push('<!>'); push_template_quasi(context.state, '<!>');
const component = serialize_inline_component(node, context.state.analysis.name, context); const component = serialize_inline_component(node, context.state.analysis.name, context);
context.state.after_update.push(component); context.state.after_update.push(component);
}, },
SvelteComponent(node, context) { SvelteComponent(node, context) {
context.state.template.push('<!>'); push_template_quasi(context.state, '<!>');
let component = serialize_inline_component(node, '$$component', context); let component = serialize_inline_component(node, '$$component', context);
if (context.state.options.dev) { if (context.state.options.dev) {
@ -2895,7 +2956,7 @@ export const template_visitors = {
}, },
SlotElement(node, context) { SlotElement(node, context) {
// <slot {a}>fallback</slot> --> $.slot($$slots.default, { get a() { .. } }, () => ...fallback); // <slot {a}>fallback</slot> --> $.slot($$slots.default, { get a() { .. } }, () => ...fallback);
context.state.template.push('<!>'); push_template_quasi(context.state, '<!>');
/** @type {import('estree').Property[]} */ /** @type {import('estree').Property[]} */
const props = []; const props = [];

@ -3,17 +3,15 @@
import "svelte/internal/disclose-version"; import "svelte/internal/disclose-version";
import * as $ from "svelte/internal"; import * as $ from "svelte/internal";
const count = 0; const boolean = false;
var frag = $.template(`<p> </p>`); var frag = $.template(`<p contenteditable="${boolean}">hello world</p>`);
export default function Hoist_unmodified_var($$anchor, $$props) { export default function Hoist_unmodified_var($$anchor, $$props) {
$.push($$props, true); $.push($$props, true);
/* Init */ /* Init */
var p = $.open($$anchor, true, frag); var p = $.open($$anchor, true, frag);
var text = $.child(p);
text.nodeValue = $.stringify(count);
$.close($$anchor, p); $.close($$anchor, p);
$.pop(); $.pop();
} }

@ -5,8 +5,8 @@ import * as $ from "svelte/internal/server";
export default function Hoist_unmodified_var($$payload, $$props) { export default function Hoist_unmodified_var($$payload, $$props) {
$.push(true); $.push(true);
let count = 0; let boolean = false;
$$payload.out += `<p>${$.escape_text(count)}</p>`; $$payload.out += `<p${$.attr("contenteditable", boolean, false)}>hello world</p>`;
$.pop(); $.pop();
} }

@ -1,7 +1,7 @@
<svelte:options runes={true} /> <svelte:options runes={true} />
<script> <script>
let count = 0; let boolean = false;
</script> </script>
<p>{count}</p> <p contenteditable={boolean}>hello world</p>

Loading…
Cancel
Save