feat: skip over static subtrees (#12849)

* feat: skip over static subtrees

* regenerate

* a few more

* prettier
pull/12846/head
Rich Harris 3 months ago committed by GitHub
parent 6b6f915f9f
commit 0a06a3f2b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: skip over static subtrees

@ -7,6 +7,9 @@ export function create_fragment(transparent = false) {
return { return {
type: 'Fragment', type: 'Fragment',
nodes: [], nodes: [],
transparent metadata: {
transparent,
dynamic: false
}
}; };
} }

@ -61,6 +61,7 @@ import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js
import { Text } from './visitors/Text.js'; import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js'; import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UpdateExpression } from './visitors/UpdateExpression.js';
import { UseDirective } from './visitors/UseDirective.js';
import { VariableDeclarator } from './visitors/VariableDeclarator.js'; import { VariableDeclarator } from './visitors/VariableDeclarator.js';
import is_reference from 'is-reference'; import is_reference from 'is-reference';
@ -166,6 +167,7 @@ const visitors = {
Text, Text,
TitleElement, TitleElement,
UpdateExpression, UpdateExpression,
UseDirective,
VariableDeclarator VariableDeclarator
}; };

@ -1,5 +1,5 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { Attribute, DelegatedEvent, RegularElement } from '#compiler' */ /** @import { Attribute, DelegatedEvent, RegularElement, SvelteNode } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { is_capture_event, is_delegated } from '../../../../utils.js'; import { is_capture_event, is_delegated } from '../../../../utils.js';
import { import {
@ -7,6 +7,7 @@ import {
get_attribute_expression, get_attribute_expression,
is_event_attribute is_event_attribute
} from '../../../utils/ast.js'; } from '../../../utils/ast.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {Attribute} node * @param {Attribute} node
@ -15,6 +16,14 @@ import {
export function Attribute(node, context) { export function Attribute(node, context) {
context.next(); context.next();
// 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.value !== true) { if (node.value !== true) {
for (const chunk of get_attribute_chunks(node.value)) { for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue; if (chunk.type !== 'ExpressionTag') continue;

@ -2,6 +2,7 @@
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js'; import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {AwaitBlock} node * @param {AwaitBlock} node
@ -38,5 +39,7 @@ export function AwaitBlock(node, context) {
} }
} }
mark_subtree_dynamic(context.path);
context.next(); context.next();
} }

@ -2,6 +2,7 @@
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */ /** @import { Scope } from '../../scope' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js'; import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
/** /**
@ -36,4 +37,6 @@ export function EachBlock(node, context) {
context.visit(node.body); context.visit(node.body);
if (node.key) context.visit(node.key); if (node.key) context.visit(node.key);
if (node.fallback) context.visit(node.fallback); if (node.fallback) context.visit(node.fallback);
mark_subtree_dynamic(context.path);
} }

@ -2,6 +2,7 @@
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js'; import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {ExpressionTag} node * @param {ExpressionTag} node
@ -14,5 +15,9 @@ 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 }); context.next({ ...context.state, expression: node.metadata.expression });
} }

@ -1,5 +1,6 @@
/** @import { HtmlTag } from '#compiler' */ /** @import { HtmlTag } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
import { validate_opening_tag } from './shared/utils.js'; import { validate_opening_tag } from './shared/utils.js';
/** /**
@ -11,5 +12,8 @@ export function HtmlTag(node, context) {
validate_opening_tag(node, context.state, '@'); validate_opening_tag(node, context.state, '@');
} }
// unfortunately this is necessary in order to fix invalid HTML
mark_subtree_dynamic(context.path);
context.next(); context.next();
} }

@ -6,6 +6,7 @@ import { should_proxy } from '../../3-transform/client/utils.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js'; import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {Identifier} node * @param {Identifier} node
@ -19,6 +20,8 @@ export function Identifier(node, context) {
return; return;
} }
mark_subtree_dynamic(context.path);
// If we are using arguments outside of a function, then throw an error // If we are using arguments outside of a function, then throw an error
if ( if (
node.name === 'arguments' && node.name === 'arguments' &&

@ -1,5 +1,6 @@
/** @import { IfBlock } from '#compiler' */ /** @import { IfBlock } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js'; import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
/** /**
@ -22,5 +23,7 @@ export function IfBlock(node, context) {
validate_opening_tag(node, context.state, expected); validate_opening_tag(node, context.state, expected);
} }
mark_subtree_dynamic(context.path);
context.next(); context.next();
} }

@ -1,5 +1,6 @@
/** @import { KeyBlock } from '#compiler' */ /** @import { KeyBlock } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js'; import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
/** /**
@ -13,5 +14,7 @@ export function KeyBlock(node, context) {
validate_opening_tag(node, context.state, '#'); validate_opening_tag(node, context.state, '#');
} }
mark_subtree_dynamic(context.path);
context.next(); context.next();
} }

@ -1,6 +1,7 @@
/** @import { OnDirective } from '#compiler' */ /** @import { OnDirective } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {OnDirective} node * @param {OnDirective} node
@ -21,5 +22,7 @@ export function OnDirective(node, context) {
context.state.analysis.event_directive_node ??= node; context.state.analysis.event_directive_node ??= node;
} }
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression }); context.next({ ...context.state, expression: node.metadata.expression });
} }

@ -3,6 +3,7 @@
import { unwrap_optional } from '../../../utils/ast.js'; import { unwrap_optional } from '../../../utils/ast.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js'; import { validate_opening_tag } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {RenderTag} node * @param {RenderTag} node
@ -33,5 +34,7 @@ export function RenderTag(node, context) {
e.render_tag_invalid_call_expression(node); e.render_tag_invalid_call_expression(node);
} }
mark_subtree_dynamic(context.path);
context.next({ ...context.state, render_tag: node }); context.next({ ...context.state, render_tag: node });
} }

@ -3,6 +3,7 @@
import { is_text_attribute } from '../../../utils/ast.js'; import { is_text_attribute } from '../../../utils/ast.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {SlotElement} node * @param {SlotElement} node
@ -13,6 +14,8 @@ export function SlotElement(node, context) {
w.slot_element_deprecated(node); w.slot_element_deprecated(node);
} }
mark_subtree_dynamic(context.path);
/** @type {string} */ /** @type {string} */
let name = 'default'; let name = 'default';

@ -1,10 +1,13 @@
/** @import { SpreadAttribute } from '#compiler' */ /** @import { SpreadAttribute } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {SpreadAttribute} node * @param {SpreadAttribute} node
* @param {Context} context * @param {Context} context
*/ */
export function SpreadAttribute(node, context) { export function SpreadAttribute(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression }); context.next({ ...context.state, expression: node.metadata.expression });
} }

@ -2,6 +2,7 @@
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { get_attribute_chunks } from '../../../utils/ast.js'; import { get_attribute_chunks } from '../../../utils/ast.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {StyleDirective} node * @param {StyleDirective} node
@ -21,6 +22,8 @@ export function StyleDirective(node, context) {
node.metadata.expression.has_state = true; node.metadata.expression.has_state = true;
} }
} }
mark_subtree_dynamic(context.path);
} else { } else {
context.next(); context.next();

@ -4,6 +4,7 @@ import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js';
import { is_text_attribute } from '../../../utils/ast.js'; import { is_text_attribute } from '../../../utils/ast.js';
import { check_element } from './shared/a11y.js'; import { check_element } from './shared/a11y.js';
import { validate_element } from './shared/element.js'; import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {SvelteElement} node * @param {SvelteElement} node
@ -58,5 +59,7 @@ export function SvelteElement(node, context) {
} }
} }
mark_subtree_dynamic(context.path);
context.next({ ...context.state, parent_element: null }); context.next({ ...context.state, parent_element: null });
} }

@ -1,6 +1,7 @@
/** @import { SvelteHead } from '#compiler' */ /** @import { SvelteHead } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/** /**
* @param {SvelteHead} node * @param {SvelteHead} node
@ -11,5 +12,7 @@ export function SvelteHead(node, context) {
e.svelte_head_illegal_attribute(attribute); e.svelte_head_illegal_attribute(attribute);
} }
mark_subtree_dynamic(context.path);
context.next(); context.next();
} }

@ -0,0 +1,12 @@
/** @import { UseDirective } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {UseDirective} node
* @param {Context} context
*/
export function UseDirective(node, context) {
mark_subtree_dynamic(context.path);
context.next();
}

@ -8,12 +8,15 @@ import {
validate_attribute_name, validate_attribute_name,
validate_slot_attribute validate_slot_attribute
} from './attribute.js'; } from './attribute.js';
import { mark_subtree_dynamic } from './fragment.js';
/** /**
* @param {Component | SvelteComponent | SvelteSelf} node * @param {Component | SvelteComponent | SvelteSelf} node
* @param {Context} context * @param {Context} context
*/ */
export function visit_component(node, context) { export function visit_component(node, context) {
mark_subtree_dynamic(context.path);
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if ( if (
attribute.type !== 'Attribute' && attribute.type !== 'Attribute' &&

@ -0,0 +1,15 @@
/** @import { SvelteNode } from '#compiler' */
/**
* @param {SvelteNode[]} path
*/
export function mark_subtree_dynamic(path) {
let i = path.length;
while (i--) {
const node = path[i];
if (node.type === 'Fragment') {
if (node.metadata.dynamic) return;
node.metadata.dynamic = true;
}
}
}

@ -313,9 +313,8 @@ export function RegularElement(node, context) {
/** Whether or not we need to wrap the children in `{...}` to avoid declaration conflicts */ /** Whether or not we need to wrap the children in `{...}` to avoid declaration conflicts */
const has_declaration = node.fragment.nodes.some((node) => node.type === 'SnippetBlock'); const has_declaration = node.fragment.nodes.some((node) => node.type === 'SnippetBlock');
const child_state = has_declaration /** @type {typeof state} */
? { ...state, init: [], update: [], after_update: [] } const child_state = { ...state, init: [], update: [], after_update: [] };
: state;
for (const node of hoisted) { for (const node of hoisted) {
context.visit(node, child_state); context.visit(node, child_state);
@ -353,6 +352,10 @@ export function RegularElement(node, context) {
...child_state.after_update ...child_state.after_update
]) ])
); );
} else if (node.fragment.metadata.dynamic) {
context.state.init.push(...child_state.init);
context.state.update.push(...child_state.update);
context.state.after_update.push(...child_state.after_update);
} }
if (has_direction_attribute) { if (has_direction_attribute) {

@ -636,7 +636,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}, },
Fragment: (node, context) => { Fragment: (node, context) => {
const scope = context.state.scope.child(node.transparent); const scope = context.state.scope.child(node.metadata.transparent);
scopes.set(node, scope); scopes.set(node, scope);
context.next({ scope }); context.next({ scope });
}, },

@ -29,12 +29,18 @@ export interface BaseNode {
export interface Fragment { export interface Fragment {
type: 'Fragment'; type: 'Fragment';
nodes: Array<Text | Tag | ElementLike | Block | Comment>; nodes: Array<Text | Tag | ElementLike | Block | Comment>;
/** metadata: {
* Fragments declare their own scopes. A transparent fragment is one whose scope /**
* is not represented by a scope in the resulting JavaScript (e.g. an element scope), * Fragments declare their own scopes. A transparent fragment is one whose scope
* and should therefore delegate to parent scopes when generating unique identifiers * is not represented by a scope in the resulting JavaScript (e.g. an element scope),
*/ * and should therefore delegate to parent scopes when generating unique identifiers
transparent: boolean; */
transparent: boolean;
/**
* Whether or not we need to traverse into the fragment during mount/hydrate
*/
dynamic: boolean;
};
} }
/** /**

@ -20,8 +20,7 @@
"raw": "\n", "raw": "\n",
"data": "\n" "data": "\n"
} }
], ]
"transparent": false
}, },
"options": null, "options": null,
"instance": { "instance": {

@ -1108,12 +1108,10 @@
"raw": "Foo", "raw": "Foo",
"data": "Foo" "data": "Foo"
} }
], ]
"transparent": true
} }
} }
], ]
"transparent": false
}, },
"options": null "options": null
} }

@ -403,8 +403,7 @@
"type": "Root", "type": "Root",
"fragment": { "fragment": {
"type": "Fragment", "type": "Fragment",
"nodes": [], "nodes": []
"transparent": false
}, },
"options": null "options": null
} }

@ -144,8 +144,7 @@
} }
} }
} }
], ]
"transparent": true
} }
}, },
{ {
@ -155,8 +154,7 @@
"raw": "\n", "raw": "\n",
"data": "\n" "data": "\n"
} }
], ]
"transparent": false
}, },
"context": { "context": {
"type": "ObjectPattern", "type": "ObjectPattern",
@ -310,8 +308,7 @@
] ]
} }
} }
], ]
"transparent": false
}, },
"options": null "options": null
} }

@ -54,8 +54,7 @@
"raw": "foo", "raw": "foo",
"data": "foo" "data": "foo"
} }
], ]
"transparent": true
} }
}, },
{ {
@ -65,8 +64,7 @@
"raw": "\n", "raw": "\n",
"data": "\n" "data": "\n"
} }
], ]
"transparent": false
}, },
"alternate": { "alternate": {
"type": "Fragment", "type": "Fragment",
@ -94,8 +92,7 @@
"raw": "not foo", "raw": "not foo",
"data": "not foo" "data": "not foo"
} }
], ]
"transparent": true
} }
}, },
{ {
@ -105,12 +102,10 @@
"raw": "\n", "raw": "\n",
"data": "\n" "data": "\n"
} }
], ]
"transparent": false
} }
} }
], ]
"transparent": false
}, },
"options": null "options": null
} }

@ -87,8 +87,7 @@
"raw": "x is greater than 10", "raw": "x is greater than 10",
"data": "x is greater than 10" "data": "x is greater than 10"
} }
], ]
"transparent": true
} }
}, },
{ {
@ -98,8 +97,7 @@
"raw": "\n", "raw": "\n",
"data": "\n" "data": "\n"
} }
], ]
"transparent": false
}, },
"alternate": { "alternate": {
"type": "Fragment", "type": "Fragment",
@ -184,8 +182,7 @@
"raw": "x is less than 5", "raw": "x is less than 5",
"data": "x is less than 5" "data": "x is less than 5"
} }
], ]
"transparent": true
} }
}, },
{ {
@ -195,17 +192,14 @@
"raw": "\n", "raw": "\n",
"data": "\n" "data": "\n"
} }
], ]
"transparent": false
}, },
"alternate": null "alternate": null
} }
], ]
"transparent": false
} }
} }
], ]
"transparent": false
}, },
"options": null "options": null
} }

@ -38,13 +38,11 @@
"raw": "bar", "raw": "bar",
"data": "bar" "data": "bar"
} }
], ]
"transparent": false
}, },
"alternate": null "alternate": null
} }
], ]
"transparent": false
}, },
"options": null "options": null
} }

@ -21,8 +21,7 @@
"raw": "\n\n", "raw": "\n\n",
"data": "\n\n" "data": "\n\n"
} }
], ]
"transparent": false
}, },
"options": { "options": {
"start": 0, "start": 0,

@ -98,8 +98,7 @@
"raw": "\n\tSemicolon inside quotes\n", "raw": "\n\tSemicolon inside quotes\n",
"data": "\n\tSemicolon inside quotes\n" "data": "\n\tSemicolon inside quotes\n"
} }
], ]
"transparent": true
} }
}, },
{ {
@ -109,8 +108,7 @@
"raw": "\n", "raw": "\n",
"data": "\n" "data": "\n"
} }
], ]
"transparent": false
}, },
"options": null "options": null
} }

@ -112,8 +112,7 @@
"name": "msg" "name": "msg"
} }
} }
], ]
"transparent": true
} }
}, },
{ {
@ -123,8 +122,7 @@
"raw": "\n", "raw": "\n",
"data": "\n" "data": "\n"
} }
], ]
"transparent": false
} }
}, },
{ {
@ -189,8 +187,7 @@
"optional": false "optional": false
} }
} }
], ]
"transparent": false
}, },
"options": null, "options": null,
"instance": { "instance": {

@ -56,12 +56,10 @@
"attributes": [], "attributes": [],
"fragment": { "fragment": {
"type": "Fragment", "type": "Fragment",
"nodes": [], "nodes": []
"transparent": true
} }
} }
], ]
"transparent": true
} }
}, },
{ {
@ -71,8 +69,7 @@
"raw": "\n", "raw": "\n",
"data": "\n" "data": "\n"
} }
], ]
"transparent": true
} }
}, },
{ {
@ -115,12 +112,10 @@
"attributes": [], "attributes": [],
"fragment": { "fragment": {
"type": "Fragment", "type": "Fragment",
"nodes": [], "nodes": []
"transparent": true
} }
} }
], ]
"transparent": true
} }
}, },
{ {
@ -130,8 +125,7 @@
"raw": "\n", "raw": "\n",
"data": "\n" "data": "\n"
} }
], ]
"transparent": true
} }
}, },
{ {
@ -149,12 +143,10 @@
"attributes": [], "attributes": [],
"fragment": { "fragment": {
"type": "Fragment", "type": "Fragment",
"nodes": [], "nodes": []
"transparent": true
} }
} }
], ]
"transparent": false
}, },
"options": null "options": null
} }

@ -350,12 +350,10 @@
"name": "count" "name": "count"
} }
} }
], ]
"transparent": true
} }
} }
], ]
"transparent": false
}, },
"options": null, "options": null,
"instance": { "instance": {

@ -0,0 +1,10 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
var root = $.template(`<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header>`);
export default function Skip_static_subtree($$anchor) {
var header = root();
$.append($$anchor, header);
}

@ -0,0 +1,5 @@
import * as $ from "svelte/internal/server";
export default function Skip_static_subtree($$payload) {
$$payload.out += `<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header>`;
}

@ -0,0 +1,6 @@
<header>
<nav>
<a href="/">Home</a>
<a href="/away">Away</a>
</nav>
</header>

@ -1486,12 +1486,18 @@ declare module 'svelte/compiler' {
interface Fragment { interface Fragment {
type: 'Fragment'; type: 'Fragment';
nodes: Array<Text | Tag | ElementLike | Block | Comment>; nodes: Array<Text | Tag | ElementLike | Block | Comment>;
/** metadata: {
* Fragments declare their own scopes. A transparent fragment is one whose scope /**
* is not represented by a scope in the resulting JavaScript (e.g. an element scope), * Fragments declare their own scopes. A transparent fragment is one whose scope
* and should therefore delegate to parent scopes when generating unique identifiers * is not represented by a scope in the resulting JavaScript (e.g. an element scope),
*/ * and should therefore delegate to parent scopes when generating unique identifiers
transparent: boolean; */
transparent: boolean;
/**
* Whether or not we need to traverse into the fragment during mount/hydrate
*/
dynamic: boolean;
};
} }
/** /**

Loading…
Cancel
Save