Skip to content

Commit df2f656

Browse files
tanhauhaudummdidummbenmccann
authored
feat: improve hydration, claim static html elements using innerHTML instead 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 <[email protected]> Co-authored-by: Ben McCann <[email protected]> Co-authored-by: Simon Holthausen <[email protected]>
1 parent 6f8cdf3 commit df2f656

File tree

104 files changed

+384
-638
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+384
-638
lines changed

src/compiler/compile/Component.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import compiler_warnings from './compiler_warnings';
3838
import compiler_errors from './compiler_errors';
3939
import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore';
4040
import check_enable_sourcemap from './utils/check_enable_sourcemap';
41+
import Tag from './nodes/shared/Tag';
4142

4243
interface ComponentOptions {
4344
namespace?: string;
@@ -110,6 +111,8 @@ export default class Component {
110111
slots: Map<string, Slot> = new Map();
111112
slot_outlets: Set<string> = new Set();
112113

114+
tags: Tag[] = [];
115+
113116
constructor(
114117
ast: Ast,
115118
source: string,
@@ -761,6 +764,7 @@ export default class Component {
761764

762765
this.hoist_instance_declarations();
763766
this.extract_reactive_declarations();
767+
this.check_if_tags_content_dynamic();
764768
}
765769

766770
post_template_walk() {
@@ -1479,6 +1483,12 @@ export default class Component {
14791483
unsorted_reactive_declarations.forEach(add_declaration);
14801484
}
14811485

1486+
check_if_tags_content_dynamic() {
1487+
this.tags.forEach(tag => {
1488+
tag.check_if_content_dynamic();
1489+
});
1490+
}
1491+
14821492
warn_if_undefined(name: string, node, template_scope: TemplateScope) {
14831493
if (name[0] === '$') {
14841494
if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) {

src/compiler/compile/nodes/Attribute.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export default class Attribute extends Node {
5959
return expression;
6060
});
6161
}
62+
63+
if (this.dependencies.size > 0) {
64+
parent.cannot_use_innerhtml();
65+
parent.not_static_content();
66+
}
6267
}
6368

6469
get_dependencies() {

src/compiler/compile/nodes/AwaitBlock.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export default class AwaitBlock extends Node {
2727

2828
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
2929
super(component, parent, scope, info);
30+
this.cannot_use_innerhtml();
31+
this.not_static_content();
3032

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

src/compiler/compile/nodes/EachBlock.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export default class EachBlock extends AbstractBlock {
3333

3434
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
3535
super(component, parent, scope, info);
36+
this.cannot_use_innerhtml();
37+
this.not_static_content();
3638

3739
this.expression = new Expression(component, this, scope, info.expression);
3840
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring

src/compiler/compile/nodes/Element.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { is_name_contenteditable, get_contenteditable_attr, has_contenteditable_
1515
import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character, regex_box_size } from '../../utils/patterns';
1616
import fuzzymatch from '../../utils/fuzzymatch';
1717
import list from '../../utils/list';
18+
import hash from '../utils/hash';
1819
import Let from './Let';
1920
import TemplateScope from './shared/TemplateScope';
2021
import { INode } from './interfaces';
@@ -503,6 +504,25 @@ export default class Element extends Node {
503504
this.optimise();
504505

505506
component.apply_stylesheet(this);
507+
508+
if (this.parent) {
509+
if (this.actions.length > 0 ||
510+
this.animation ||
511+
this.bindings.length > 0 ||
512+
this.classes.length > 0 ||
513+
this.intro || this.outro ||
514+
this.handlers.length > 0 ||
515+
this.styles.length > 0 ||
516+
this.name === 'option' ||
517+
this.is_dynamic_element ||
518+
this.tag_expr.dynamic_dependencies().length ||
519+
this.is_dynamic_element ||
520+
component.compile_options.dev
521+
) {
522+
this.parent.cannot_use_innerhtml(); // need to use add_location
523+
this.parent.not_static_content();
524+
}
525+
}
506526
}
507527

508528
validate() {
@@ -1262,6 +1282,20 @@ export default class Element extends Node {
12621282
}
12631283
});
12641284
}
1285+
1286+
get can_use_textcontent() {
1287+
return this.is_static_content && this.children.every(node => node.type === 'Text' || node.type === 'MustacheTag');
1288+
}
1289+
1290+
get can_optimise_to_html_string() {
1291+
const can_use_textcontent = this.can_use_textcontent;
1292+
const is_template_with_text_content = this.name === 'template' && can_use_textcontent;
1293+
return !is_template_with_text_content && !this.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.children.length > 0;
1294+
}
1295+
1296+
hash() {
1297+
return `svelte-${hash(this.component.source.slice(this.start, this.end))}`;
1298+
}
12651299
}
12661300

12671301
const regex_starts_with_vowel = /^[aeiou]/;

src/compiler/compile/nodes/Head.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export default class Head extends Node {
1515
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
1616
super(component, parent, scope, info);
1717

18+
this.cannot_use_innerhtml();
19+
1820
if (info.attributes.length) {
1921
component.error(info.attributes[0], compiler_errors.invalid_attribute_head);
2022
return;

src/compiler/compile/nodes/IfBlock.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export default class IfBlock extends AbstractBlock {
1818
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
1919
super(component, parent, scope, info);
2020
this.scope = scope.child();
21+
this.cannot_use_innerhtml();
22+
this.not_static_content();
2123

2224
this.expression = new Expression(component, this, this.scope, info.expression);
2325
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));

src/compiler/compile/nodes/InlineComponent.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export default class InlineComponent extends Node {
2828
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
2929
super(component, parent, scope, info);
3030

31+
this.cannot_use_innerhtml();
32+
this.not_static_content();
33+
3134
if (info.name !== 'svelte:component' && info.name !== 'svelte:self') {
3235
const name = info.name.split('.')[0]; // accommodate namespaces
3336
component.warn_if_undefined(name, info, scope);

src/compiler/compile/nodes/KeyBlock.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export default class KeyBlock extends AbstractBlock {
1313

1414
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
1515
super(component, parent, scope, info);
16+
this.cannot_use_innerhtml();
17+
this.not_static_content();
1618

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

src/compiler/compile/nodes/RawMustacheTag.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@ import Tag from './shared/Tag';
22

33
export default class RawMustacheTag extends Tag {
44
type: 'RawMustacheTag';
5+
constructor(component, parent, scope, info) {
6+
super(component, parent, scope, info);
7+
this.cannot_use_innerhtml();
8+
this.not_static_content();
9+
}
510
}

src/compiler/compile/nodes/Slot.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,8 @@ export default class Slot extends Element {
6060
}
6161

6262
component.slots.set(this.slot_name, this);
63+
64+
this.cannot_use_innerhtml();
65+
this.not_static_content();
6366
}
6467
}

src/compiler/compile/nodes/Text.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const elements_without_text = new Set([
1818
]);
1919

2020
const regex_ends_with_svg = /svg$/;
21+
const regex_non_whitespace_characters = /[\S\u00A0]/;
2122

2223
export default class Text extends Node {
2324
type: 'Text';
@@ -63,4 +64,11 @@ export default class Text extends Node {
6364

6465
return false;
6566
}
67+
68+
use_space(): boolean {
69+
if (this.component.compile_options.preserveWhitespace) return false;
70+
if (regex_non_whitespace_characters.test(this.data)) return false;
71+
72+
return !this.within_pre();
73+
}
6674
}

src/compiler/compile/nodes/shared/Expression.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,15 @@ export default class Expression {
197197
});
198198
}
199199

200+
dynamic_contextual_dependencies() {
201+
return Array.from(this.contextual_dependencies).filter(name => {
202+
return Array.from(this.template_scope.dependencies_for_name.get(name)).some(variable_name => {
203+
const variable = this.component.var_lookup.get(variable_name);
204+
return is_dynamic(variable);
205+
});
206+
});
207+
}
208+
200209
// TODO move this into a render-dom wrapper?
201210
manipulate(block?: Block, ctx?: string | void) {
202211
// TODO ideally we wouldn't end up calling this method

src/compiler/compile/nodes/shared/Node.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default class Node {
1515
next?: INode;
1616

1717
can_use_innerhtml: boolean;
18+
is_static_content: boolean;
1819
var: string;
1920
attributes: Attribute[];
2021

@@ -33,6 +34,9 @@ export default class Node {
3334
value: parent
3435
}
3536
});
37+
38+
this.can_use_innerhtml = true;
39+
this.is_static_content = true;
3640
}
3741

3842
cannot_use_innerhtml() {
@@ -42,6 +46,11 @@ export default class Node {
4246
}
4347
}
4448

49+
not_static_content() {
50+
this.is_static_content = false;
51+
if (this.parent) this.parent.not_static_content();
52+
}
53+
4554
find_nearest(selector: RegExp) {
4655
if (selector.test(this.type)) return this;
4756
if (this.parent) return this.parent.find_nearest(selector);

src/compiler/compile/nodes/shared/Tag.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,22 @@ export default class Tag extends Node {
88

99
constructor(component, parent, scope, info) {
1010
super(component, parent, scope, info);
11+
component.tags.push(this);
12+
this.cannot_use_innerhtml();
13+
1114
this.expression = new Expression(component, this, scope, info.expression);
1215

1316
this.should_cache = (
1417
info.expression.type !== 'Identifier' ||
1518
(this.expression.dependencies.size && scope.names.has(info.expression.name))
1619
);
1720
}
21+
is_dependencies_static() {
22+
return this.expression.dynamic_contextual_dependencies().length === 0 && this.expression.dynamic_dependencies().length === 0;
23+
}
24+
check_if_content_dynamic() {
25+
if (!this.is_dependencies_static()) {
26+
this.not_static_content();
27+
}
28+
}
1829
}

src/compiler/compile/render_dom/wrappers/AwaitBlock.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,6 @@ export default class AwaitBlockWrapper extends Wrapper {
143143
) {
144144
super(renderer, block, parent, node);
145145

146-
this.cannot_use_innerhtml();
147-
this.not_static_content();
148-
149146
block.add_dependencies(this.node.expression.dependencies);
150147

151148
let is_dynamic = false;

src/compiler/compile/render_dom/wrappers/Comment.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,10 @@ export default class CommentWrapper extends Wrapper {
3838
parent_node
3939
);
4040
}
41+
42+
text() {
43+
if (!this.renderer.options.preserveComments) return '';
44+
45+
return `<!--${this.node.data}-->`;
46+
}
4147
}

src/compiler/compile/render_dom/wrappers/EachBlock.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@ export default class EachBlockWrapper extends Wrapper {
8080
next_sibling: Wrapper
8181
) {
8282
super(renderer, block, parent, node);
83-
this.cannot_use_innerhtml();
84-
this.not_static_content();
8583

8684
const { dependencies } = node.expression;
8785
block.add_dependencies(dependencies);

src/compiler/compile/render_dom/wrappers/Element/Attribute.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@ export class BaseAttributeWrapper {
3636
this.parent = parent;
3737

3838
if (node.dependencies.size > 0) {
39-
parent.cannot_use_innerhtml();
40-
parent.not_static_content();
41-
4239
block.add_dependencies(node.dependencies);
4340
}
4441
}

0 commit comments

Comments
 (0)