Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve hydration, claim static html elements using innerHTML instead of deopt to claiming every nodes #7426

Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/compiler/compile/Component.ts
Expand Up @@ -38,6 +38,7 @@ import compiler_warnings from './compiler_warnings';
import compiler_errors from './compiler_errors';
import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore';
import check_enable_sourcemap from './utils/check_enable_sourcemap';
import Tag from './nodes/shared/Tag';

interface ComponentOptions {
namespace?: string;
Expand Down Expand Up @@ -110,6 +111,8 @@ export default class Component {
slots: Map<string, Slot> = new Map();
slot_outlets: Set<string> = new Set();

tags: Tag[] = [];

constructor(
ast: Ast,
source: string,
Expand Down Expand Up @@ -761,6 +764,7 @@ export default class Component {

this.hoist_instance_declarations();
this.extract_reactive_declarations();
this.check_if_tags_content_dynamic();
}

post_template_walk() {
Expand Down Expand Up @@ -1479,6 +1483,12 @@ export default class Component {
unsorted_reactive_declarations.forEach(add_declaration);
}

check_if_tags_content_dynamic() {
this.tags.forEach(tag => {
tag.check_if_content_dynamic();
});
}

warn_if_undefined(name: string, node, template_scope: TemplateScope) {
if (name[0] === '$') {
if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) {
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/compile/nodes/Attribute.ts
Expand Up @@ -59,6 +59,11 @@ export default class Attribute extends Node {
return expression;
});
}

if (this.dependencies.size > 0) {
parent.cannot_use_innerhtml();
parent.not_static_content();
}
}

get_dependencies() {
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/AwaitBlock.ts
Expand Up @@ -27,6 +27,8 @@ export default class AwaitBlock extends Node {

constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();

this.expression = new Expression(component, this, scope, info.expression);

Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/EachBlock.ts
Expand Up @@ -33,6 +33,8 @@ export default class EachBlock extends AbstractBlock {

constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();

this.expression = new Expression(component, this, scope, info.expression);
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring
Expand Down
34 changes: 34 additions & 0 deletions src/compiler/compile/nodes/Element.ts
Expand Up @@ -15,6 +15,7 @@ import { is_name_contenteditable, get_contenteditable_attr, has_contenteditable_
import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character, regex_box_size } from '../../utils/patterns';
import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list';
import hash from '../utils/hash';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
Expand Down Expand Up @@ -503,6 +504,25 @@ export default class Element extends Node {
this.optimise();

component.apply_stylesheet(this);

if (this.parent) {
if (this.actions.length > 0 ||
this.animation ||
this.bindings.length > 0 ||
this.classes.length > 0 ||
this.intro || this.outro ||
this.handlers.length > 0 ||
this.styles.length > 0 ||
this.name === 'option' ||
this.is_dynamic_element ||
this.tag_expr.dynamic_dependencies().length ||
this.is_dynamic_element ||
component.compile_options.dev
) {
this.parent.cannot_use_innerhtml(); // need to use add_location
this.parent.not_static_content();
}
}
}

validate() {
Expand Down Expand Up @@ -1262,6 +1282,20 @@ export default class Element extends Node {
}
});
}

get can_use_textcontent() {
return this.is_static_content && this.children.every(node => node.type === 'Text' || node.type === 'MustacheTag');
}

get can_optimise_to_html_string() {
const can_use_textcontent = this.can_use_textcontent;
const is_template_with_text_content = this.name === 'template' && can_use_textcontent;
return !is_template_with_text_content && !this.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.children.length > 0;
}

hash() {
return `svelte-${hash(this.component.source.slice(this.start, this.end))}`;
}
}

const regex_starts_with_vowel = /^[aeiou]/;
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/Head.ts
Expand Up @@ -15,6 +15,8 @@ export default class Head extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);

this.cannot_use_innerhtml();

if (info.attributes.length) {
component.error(info.attributes[0], compiler_errors.invalid_attribute_head);
return;
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/IfBlock.ts
Expand Up @@ -18,6 +18,8 @@ export default class IfBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.scope = scope.child();
this.cannot_use_innerhtml();
this.not_static_content();

this.expression = new Expression(component, this, this.scope, info.expression);
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/compile/nodes/InlineComponent.ts
Expand Up @@ -28,6 +28,9 @@ export default class InlineComponent extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);

this.cannot_use_innerhtml();
this.not_static_content();

if (info.name !== 'svelte:component' && info.name !== 'svelte:self') {
const name = info.name.split('.')[0]; // accommodate namespaces
component.warn_if_undefined(name, info, scope);
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/KeyBlock.ts
Expand Up @@ -13,6 +13,8 @@ export default class KeyBlock extends AbstractBlock {

constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();

this.expression = new Expression(component, this, scope, info.expression);

Expand Down
5 changes: 5 additions & 0 deletions src/compiler/compile/nodes/RawMustacheTag.ts
Expand Up @@ -2,4 +2,9 @@ import Tag from './shared/Tag';

export default class RawMustacheTag extends Tag {
type: 'RawMustacheTag';
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
}
}
3 changes: 3 additions & 0 deletions src/compiler/compile/nodes/Slot.ts
Expand Up @@ -60,5 +60,8 @@ export default class Slot extends Element {
}

component.slots.set(this.slot_name, this);

this.cannot_use_innerhtml();
this.not_static_content();
}
}
8 changes: 8 additions & 0 deletions src/compiler/compile/nodes/Text.ts
Expand Up @@ -18,6 +18,7 @@ const elements_without_text = new Set([
]);

const regex_ends_with_svg = /svg$/;
const regex_non_whitespace_characters = /[\S\u00A0]/;

export default class Text extends Node {
type: 'Text';
Expand Down Expand Up @@ -63,4 +64,11 @@ export default class Text extends Node {

return false;
}

use_space(): boolean {
if (this.component.compile_options.preserveWhitespace) return false;
if (regex_non_whitespace_characters.test(this.data)) return false;

return !this.within_pre();
}
}
9 changes: 9 additions & 0 deletions src/compiler/compile/nodes/shared/Expression.ts
Expand Up @@ -197,6 +197,15 @@ export default class Expression {
});
}

dynamic_contextual_dependencies() {
return Array.from(this.contextual_dependencies).filter(name => {
return Array.from(this.template_scope.dependencies_for_name.get(name)).some(variable_name => {
const variable = this.component.var_lookup.get(variable_name);
return is_dynamic(variable);
});
});
}

// TODO move this into a render-dom wrapper?
manipulate(block?: Block, ctx?: string | void) {
// TODO ideally we wouldn't end up calling this method
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/compile/nodes/shared/Node.ts
Expand Up @@ -15,6 +15,7 @@ export default class Node {
next?: INode;

can_use_innerhtml: boolean;
is_static_content: boolean;
var: string;
attributes: Attribute[];

Expand All @@ -33,6 +34,9 @@ export default class Node {
value: parent
}
});

this.can_use_innerhtml = true;
this.is_static_content = true;
}

cannot_use_innerhtml() {
Expand All @@ -42,6 +46,11 @@ export default class Node {
}
}

not_static_content() {
this.is_static_content = false;
if (this.parent) this.parent.not_static_content();
}

find_nearest(selector: RegExp) {
if (selector.test(this.type)) return this;
if (this.parent) return this.parent.find_nearest(selector);
Expand Down
11 changes: 11 additions & 0 deletions src/compiler/compile/nodes/shared/Tag.ts
Expand Up @@ -8,11 +8,22 @@ export default class Tag extends Node {

constructor(component, parent, scope, info) {
super(component, parent, scope, info);
component.tags.push(this);
this.cannot_use_innerhtml();

this.expression = new Expression(component, this, scope, info.expression);

this.should_cache = (
info.expression.type !== 'Identifier' ||
(this.expression.dependencies.size && scope.names.has(info.expression.name))
);
}
is_dependencies_static() {
return this.expression.dynamic_contextual_dependencies().length === 0 && this.expression.dynamic_dependencies().length === 0;
}
check_if_content_dynamic() {
if (!this.is_dependencies_static()) {
this.not_static_content();
}
}
}
3 changes: 0 additions & 3 deletions src/compiler/compile/render_dom/wrappers/AwaitBlock.ts
Expand Up @@ -143,9 +143,6 @@ export default class AwaitBlockWrapper extends Wrapper {
) {
super(renderer, block, parent, node);

this.cannot_use_innerhtml();
this.not_static_content();

block.add_dependencies(this.node.expression.dependencies);

let is_dynamic = false;
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/compile/render_dom/wrappers/Comment.ts
Expand Up @@ -38,4 +38,10 @@ export default class CommentWrapper extends Wrapper {
parent_node
);
}

text() {
if (!this.renderer.options.preserveComments) return '';

return `<!--${this.node.data}-->`;
}
}
2 changes: 0 additions & 2 deletions src/compiler/compile/render_dom/wrappers/EachBlock.ts
Expand Up @@ -80,8 +80,6 @@ export default class EachBlockWrapper extends Wrapper {
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();

const { dependencies } = node.expression;
block.add_dependencies(dependencies);
Expand Down
3 changes: 0 additions & 3 deletions src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
Expand Up @@ -36,9 +36,6 @@ export class BaseAttributeWrapper {
this.parent = parent;

if (node.dependencies.size > 0) {
parent.cannot_use_innerhtml();
parent.not_static_content();

block.add_dependencies(node.dependencies);
}
}
Expand Down