feat: skip over static subtrees (#12849)

* feat: skip over static subtrees

* regenerate

* a few more

* prettier
pull/12846/head
Rich Harris 1 month 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 {
type: 'Fragment',
nodes: [],
transparent
metadata: {
transparent,
dynamic: false
}
};
}

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

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

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

@ -2,6 +2,7 @@
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
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';
/**
@ -36,4 +37,6 @@ export function EachBlock(node, context) {
context.visit(node.body);
if (node.key) context.visit(node.key);
if (node.fallback) context.visit(node.fallback);
mark_subtree_dynamic(context.path);
}

@ -2,6 +2,7 @@
/** @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 {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 });
}

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

@ -6,6 +6,7 @@ import { should_proxy } from '../../3-transform/client/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {Identifier} node
@ -19,6 +20,8 @@ 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' &&

@ -1,5 +1,6 @@
/** @import { IfBlock } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.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);
}
mark_subtree_dynamic(context.path);
context.next();
}

@ -1,5 +1,6 @@
/** @import { KeyBlock } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.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, '#');
}
mark_subtree_dynamic(context.path);
context.next();
}

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

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

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

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

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

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

@ -1,6 +1,7 @@
/** @import { SvelteHead } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {SvelteHead} node
@ -11,5 +12,7 @@ export function SvelteHead(node, context) {
e.svelte_head_illegal_attribute(attribute);
}
mark_subtree_dynamic(context.path);
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_slot_attribute
} from './attribute.js';
import { mark_subtree_dynamic } from './fragment.js';
/**
* @param {Component | SvelteComponent | SvelteSelf} node
* @param {Context} context
*/
export function visit_component(node, context) {
mark_subtree_dynamic(context.path);
for (const attribute of node.attributes) {
if (
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 */
const has_declaration = node.fragment.nodes.some((node) => node.type === 'SnippetBlock');
const child_state = has_declaration
? { ...state, init: [], update: [], after_update: [] }
: state;
/** @type {typeof state} */
const child_state = { ...state, init: [], update: [], after_update: [] };
for (const node of hoisted) {
context.visit(node, child_state);
@ -353,6 +352,10 @@ export function RegularElement(node, context) {
...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) {

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

@ -29,12 +29,18 @@ export interface BaseNode {
export interface Fragment {
type: 'Fragment';
nodes: Array<Text | Tag | ElementLike | Block | Comment>;
/**
* 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),
* and should therefore delegate to parent scopes when generating unique identifiers
*/
transparent: boolean;
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),
* and should therefore delegate to parent scopes when generating unique identifiers
*/
transparent: boolean;
/**
* Whether or not we need to traverse into the fragment during mount/hydrate
*/
dynamic: boolean;
};
}
/**

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

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

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

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

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

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

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

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

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

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

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

@ -350,12 +350,10 @@
"name": "count"
}
}
],
"transparent": true
]
}
}
],
"transparent": false
]
},
"options": null,
"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 {
type: 'Fragment';
nodes: Array<Text | Tag | ElementLike | Block | Comment>;
/**
* 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),
* and should therefore delegate to parent scopes when generating unique identifiers
*/
transparent: boolean;
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),
* and should therefore delegate to parent scopes when generating unique identifiers
*/
transparent: boolean;
/**
* Whether or not we need to traverse into the fragment during mount/hydrate
*/
dynamic: boolean;
};
}
/**

Loading…
Cancel
Save