Skip to content

Commit

Permalink
feat: improve hydration, claim static html elements using innerHTML i…
Browse files Browse the repository at this point in the history
…nstead of deopt to claiming every nodes (#7426)

Related: #7341, #7226

For purely static HTML, instead of walking the node tree and claiming every node/text etc, hydration now uses the same innerHTML optimization technique for hydration compared to normal create. It uses a new data-svelte-h attribute which is added upon server side rendering containing a hash (computed at build time), and then comparing that hash in the client to ensure it's the same node. If the hash is the same, the whole child content is expected to be the same. If the hash is different, the whole child content is replaced with innerHTML.

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
  • Loading branch information
4 people committed Apr 20, 2023
1 parent 6f8cdf3 commit df2f656
Show file tree
Hide file tree
Showing 104 changed files with 384 additions and 638 deletions.
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

0 comments on commit df2f656

Please sign in to comment.