feat: only inject push/init/pop when necessary (#11319)

* feat: only inject push/init/pop when necessary - closes #11297

* regenerate

* differentiate between safe/unsafe

* only inject $$props when necessary

* more

* fix

* simplify

* handle store subscriptions
pull/11340/head
Rich Harris 1 year ago committed by GitHub
parent 8e43e9aae0
commit f1986da755
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: only inject push/init/pop when necessary

@ -370,6 +370,8 @@ export function analyze_component(root, source, options) {
uses_slots: false,
uses_component_bindings: false,
uses_render_tags: false,
needs_context: false,
needs_props: false,
custom_element: options.customElementOptions ?? options.customElement,
inject_styles: options.css === 'injected' || options.customElement,
accessors: options.customElement
@ -803,6 +805,8 @@ const legacy_scope_tweaker = {
return next();
}
state.analysis.needs_props = true;
if (!node.declaration) {
for (const specifier of node.specifiers) {
const binding = /** @type {import('#compiler').Binding} */ (
@ -931,6 +935,8 @@ const runes_scope_tweaker = {
}
if (rune === '$props') {
state.analysis.needs_props = true;
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
if (property.type !== 'Property') continue;
@ -1038,6 +1044,33 @@ const function_visitor = (node, context) => {
});
};
/**
* A 'safe' identifier means that the `foo` in `foo.bar` or `foo()` will not
* call functions that require component context to exist
* @param {import('estree').Expression | import('estree').Super} expression
* @param {Scope} scope
*/
function is_safe_identifier(expression, scope) {
let node = expression;
while (node.type === 'MemberExpression') node = node.object;
if (node.type !== 'Identifier') return false;
const binding = scope.get(node.name);
if (!binding) return true;
if (binding.kind === 'store_sub') {
return is_safe_identifier({ name: node.name.slice(1), type: 'Identifier' }, scope);
}
return (
binding.declaration_kind !== 'import' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'rest_prop'
);
}
/** @type {import('./types').Visitors} */
const common_visitors = {
_(node, context) {
@ -1209,6 +1242,8 @@ const common_visitors = {
}
const callee = node.callee;
const rune = get_rune(node, context.state.scope);
if (callee.type === 'Identifier') {
const binding = context.state.scope.get(callee.name);
@ -1216,7 +1251,7 @@ const common_visitors = {
binding.is_called = true;
}
if (get_rune(node, context.state.scope) === '$derived') {
if (rune === '$derived') {
// special case — `$derived(foo)` is treated as `$derived(() => foo)`
// for the purposes of identifying static state references
context.next({
@ -1228,6 +1263,16 @@ const common_visitors = {
}
}
if (rune === '$effect' || rune === '$effect.pre') {
// `$effect` needs context because Svelte needs to know whether it should re-run
// effects that invalidate themselves, and that's determined by whether we're in runes mode
context.state.analysis.needs_context = true;
} else if (rune === null) {
if (!is_safe_identifier(callee, context.state.scope)) {
context.state.analysis.needs_context = true;
}
}
context.next();
},
MemberExpression(node, context) {
@ -1235,6 +1280,10 @@ const common_visitors = {
context.state.expression.metadata.dynamic = true;
}
if (!is_safe_identifier(node, context.state.scope)) {
context.state.analysis.needs_context = true;
}
context.next();
},
BindDirective(node, context) {

@ -351,12 +351,11 @@ export function client_component(source, analysis, options) {
if (options.dev) push_args.push(b.id(analysis.name));
const component_block = b.block([
b.stmt(b.call('$.push', ...push_args)),
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
.../** @type {import('estree').Statement[]} */ (instance.body),
analysis.runes ? b.empty : b.stmt(b.call('$.init')),
analysis.runes || !analysis.needs_context ? b.empty : b.stmt(b.call('$.init')),
.../** @type {import('estree').Statement[]} */ (template.body)
]);
@ -392,11 +391,22 @@ export function client_component(source, analysis, options) {
: () => {};
append_styles();
component_block.body.push(
component_returned_object.length > 0
? b.return(b.call('$.pop', b.object(component_returned_object)))
: b.stmt(b.call('$.pop'))
);
const should_inject_context =
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0 ||
options.dev;
if (should_inject_context) {
component_block.body.unshift(b.stmt(b.call('$.push', ...push_args)));
component_block.body.push(
component_returned_object.length > 0
? b.return(b.call('$.pop', b.object(component_returned_object)))
: b.stmt(b.call('$.pop'))
);
}
if (analysis.uses_rest_props) {
const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);
@ -430,11 +440,19 @@ export function client_component(source, analysis, options) {
component_block.body.unshift(b.const('$$slots', b.call('$.sanitize_slots', b.id('$$props'))));
}
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
analysis.uses_props ||
analysis.uses_rest_props ||
analysis.uses_slots ||
analysis.slot_names.size > 0;
const body = [...state.hoisted, ...module.body];
const component = b.function_declaration(
b.id(analysis.name),
[b.id('$$anchor'), b.id('$$props')],
should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')],
component_block
);

@ -1228,6 +1228,8 @@ function serialize_event_handler(node, { state, visit }) {
return handler;
} else {
state.analysis.needs_props = true;
// Function + .call to preserve "this" context as much as possible
return b.function(
null,

@ -57,6 +57,8 @@ export interface ComponentAnalysis extends Analysis {
uses_slots: boolean;
uses_component_bindings: boolean;
uses_render_tags: boolean;
needs_context: boolean;
needs_props: boolean;
custom_element: boolean | SvelteOptions['customElement'];
/** If `true`, should append styles through JavaScript */
inject_styles: boolean;

@ -5,9 +5,7 @@ import TextInput from './Child.svelte';
var root_1 = $.template(`Something`, 1);
var root = $.template(`<!> `, 1);
export default function Bind_component_snippet($$anchor, $$props) {
$.push($$props, true);
export default function Bind_component_snippet($$anchor) {
let value = $.source('');
const _snippet = snippet;
var fragment_1 = root();
@ -33,5 +31,4 @@ export default function Bind_component_snippet($$anchor, $$props) {
$.render_effect(() => $.set_text(text, ` value: ${$.stringify($.get(value))}`));
$.append($$anchor, fragment_1);
$.pop();
}

@ -1,14 +1,10 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Bind_this($$anchor, $$props) {
$.push($$props, false);
$.init();
export default function Bind_this($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
$.bind_this(Foo(node, {}), ($$value) => foo = $$value, () => foo);
$.append($$anchor, fragment);
$.pop();
}

@ -3,9 +3,7 @@ import * as $ from "svelte/internal/client";
var root = $.template(`<div></div> <svg></svg> <custom-element></custom-element> <div></div> <svg></svg> <custom-element></custom-element>`, 3);
export default function Main($$anchor, $$props) {
$.push($$props, true);
export default function Main($$anchor) {
// needs to be a snapshot test because jsdom does auto-correct the attribute casing
let x = 'test';
let y = () => 'test';
@ -32,5 +30,4 @@ export default function Main($$anchor, $$props) {
});
$.append($$anchor, fragment);
$.pop();
}

@ -1,10 +1,7 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Each_string_template($$anchor, $$props) {
$.push($$props, false);
$.init();
export default function Each_string_template($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
@ -16,5 +13,4 @@ export default function Each_string_template($$anchor, $$props) {
});
$.append($$anchor, fragment);
$.pop();
}

@ -1,9 +1,7 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Function_prop_no_getter($$anchor, $$props) {
$.push($$props, true);
export default function Function_prop_no_getter($$anchor) {
let count = $.source(0);
function onmouseup() {
@ -27,5 +25,4 @@ export default function Function_prop_no_getter($$anchor, $$props) {
});
$.append($$anchor, fragment);
$.pop();
}

@ -3,12 +3,8 @@ import * as $ from "svelte/internal/client";
var root = $.template(`<h1>hello world</h1>`);
export default function Hello_world($$anchor, $$props) {
$.push($$props, false);
$.init();
export default function Hello_world($$anchor) {
var h1 = root();
$.append($$anchor, h1);
$.pop();
}

@ -3,14 +3,10 @@ import * as $ from "svelte/internal/client";
var root = $.template(`<h1>hello world</h1>`);
function Hmr($$anchor, $$props) {
$.push($$props, false);
$.init();
function Hmr($$anchor) {
var h1 = root();
$.append($$anchor, h1);
$.pop();
}
if (import.meta.hot) {

@ -10,9 +10,7 @@ function reset(_, str, tpl) {
var root = $.template(`<input> <input> <button>reset</button>`, 1);
export default function State_proxy_literal($$anchor, $$props) {
$.push($$props, true);
export default function State_proxy_literal($$anchor) {
let str = $.source('');
let tpl = $.source(``);
var fragment = root();
@ -30,7 +28,6 @@ export default function State_proxy_literal($$anchor, $$props) {
$.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value));
$.bind_value(input_1, () => $.get(tpl), ($$value) => $.set(tpl, $$value));
$.append($$anchor, fragment);
$.pop();
}
$.delegate(["click"]);

@ -2,13 +2,10 @@ import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Svelte_element($$anchor, $$props) {
$.push($$props, true);
let tag = $.prop($$props, "tag", 3, 'hr');
var fragment = $.comment();
var node = $.first_child(fragment);
$.element(node, tag, false);
$.append($$anchor, fragment);
$.pop();
}
Loading…
Cancel
Save