From df2f656557999b0912171b4513986f8526da7a5b Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Thu, 20 Apr 2023 15:47:38 +0800 Subject: [PATCH] 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 <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Simon Holthausen --- src/compiler/compile/Component.ts | 10 ++ src/compiler/compile/nodes/Attribute.ts | 5 + src/compiler/compile/nodes/AwaitBlock.ts | 2 + src/compiler/compile/nodes/EachBlock.ts | 2 + src/compiler/compile/nodes/Element.ts | 34 +++++++ src/compiler/compile/nodes/Head.ts | 2 + src/compiler/compile/nodes/IfBlock.ts | 2 + src/compiler/compile/nodes/InlineComponent.ts | 3 + src/compiler/compile/nodes/KeyBlock.ts | 2 + src/compiler/compile/nodes/RawMustacheTag.ts | 5 + src/compiler/compile/nodes/Slot.ts | 3 + src/compiler/compile/nodes/Text.ts | 8 ++ .../compile/nodes/shared/Expression.ts | 9 ++ src/compiler/compile/nodes/shared/Node.ts | 9 ++ src/compiler/compile/nodes/shared/Tag.ts | 11 +++ .../compile/render_dom/wrappers/AwaitBlock.ts | 3 - .../compile/render_dom/wrappers/Comment.ts | 6 ++ .../compile/render_dom/wrappers/EachBlock.ts | 2 - .../render_dom/wrappers/Element/Attribute.ts | 3 - .../render_dom/wrappers/Element/index.ts | 96 ++++++++++--------- .../compile/render_dom/wrappers/Head.ts | 2 - .../compile/render_dom/wrappers/IfBlock.ts | 3 - .../wrappers/InlineComponent/index.ts | 3 - .../compile/render_dom/wrappers/KeyBlock.ts | 3 - .../render_dom/wrappers/RawMustacheTag.ts | 2 - .../compile/render_dom/wrappers/Slot.ts | 2 - .../compile/render_dom/wrappers/Text.ts | 19 ++-- .../compile/render_dom/wrappers/shared/Tag.ts | 9 -- .../render_dom/wrappers/shared/Wrapper.ts | 15 --- src/compiler/compile/render_ssr/Renderer.ts | 1 + .../compile/render_ssr/handlers/Element.ts | 7 ++ .../compile/render_ssr/handlers/Text.ts | 4 +- .../utils/remove_whitespace_children.ts | 1 + src/compiler/compile/utils/stringify.ts | 3 +- src/runtime/internal/dom.ts | 4 + test/helpers.ts | 25 +++-- test/hydration/index.ts | 7 ++ test/hydration/samples/basic/_after.html | 2 +- test/hydration/samples/basic/_before.html | 2 +- test/hydration/samples/basic/_config.js | 7 -- .../samples/binding-input/_config.js | 7 +- .../samples/claim-comment/_after.html | 2 + .../samples/claim-comment/_before.html | 2 + .../samples/claim-comment/_config.js | 12 +-- .../samples/claim-comment/main.svelte | 2 + .../claim-static-incorrect-hash/_after.html | 8 ++ .../claim-static-incorrect-hash/_before.html | 8 ++ .../claim-static-incorrect-hash/main.svelte | 8 ++ .../samples/claim-static-no-hash/_after.html | 8 ++ .../samples/claim-static-no-hash/_before.html | 8 ++ .../samples/claim-static-no-hash/main.svelte | 8 ++ test/hydration/samples/claim-text/_config.js | 8 ++ .../samples/component-in-element/_after.html | 2 +- .../samples/component-in-element/_before.html | 2 +- .../samples/component-in-element/_config.js | 9 -- test/hydration/samples/component/_after.html | 2 +- test/hydration/samples/component/_before.html | 2 +- test/hydration/samples/component/_config.js | 7 -- .../samples/dynamic-text-changed/_config.js | 7 -- .../samples/dynamic-text-nil/_config.js | 8 -- .../hydration/samples/dynamic-text/_config.js | 7 -- .../samples/each-block-arg-clash/_config.js | 14 +-- test/hydration/samples/each-block/_config.js | 14 +-- .../element-attribute-added/_config.js | 6 -- .../element-attribute-changed/_config.js | 6 -- .../element-attribute-removed/_config.js | 6 -- .../element-attribute-unchanged/_config.js | 6 -- .../samples/element-nested-sibling/_config.js | 8 -- .../samples/element-nested/_after.html | 2 +- .../samples/element-nested/_before.html | 2 +- .../samples/element-nested/_config.js | 7 -- test/hydration/samples/element-ref/_config.js | 4 +- .../samples/event-handler/_config.js | 4 +- .../samples/expression-sibling/_config.js | 8 -- .../samples/if-block-anchor/_config.js | 9 -- .../samples/if-block-false/_config.js | 6 -- .../samples/if-block-update/_config.js | 5 +- test/hydration/samples/if-block/_config.js | 6 -- test/hydration/samples/raw/_config.js | 9 -- .../samples/collapse-literal-ssr/expected.js | 12 +-- test/js/samples/debug-ssr-foo/expected.js | 9 +- .../each-block-changed-check/expected.js | 4 +- .../samples/ssr-preserve-comments/expected.js | 6 +- test/runtime/index.ts | 15 ++- .../dynamic-element-action-update/_config.js | 4 +- .../dynamic-element-action-update/main.svelte | 8 +- .../fragment-trailing-whitespace/_config.js | 4 +- test/runtime/samples/pre-tag/_config.js | 52 ++++------ .../samples/preserve-whitespaces/_config.js | 33 ++----- .../raw-mustaches-preserved/_config.js | 2 +- .../samples/textarea-content/_config.js | 17 ++++ test/server-side-rendering/index.ts | 25 +++-- .../_expected.html | 4 +- .../samples/pre-tag/.editorconfig | 2 - .../samples/pre-tag/_config.js | 3 - .../samples/pre-tag/_expected.html | 39 -------- .../samples/pre-tag/main.svelte | 51 ---------- .../samples/preserve-whitespaces/_config.js | 6 -- .../preserve-whitespaces/_expected.html | 32 ------- .../samples/preserve-whitespaces/main.svelte | 34 ------- .../samples/textarea-content/.editorconfig | 2 - .../samples/textarea-content/_config.js | 3 - .../samples/textarea-content/_expected.html | 28 ------ .../samples/textarea-content/main.svelte | 40 -------- 104 files changed, 384 insertions(+), 638 deletions(-) create mode 100644 test/hydration/samples/claim-static-incorrect-hash/_after.html create mode 100644 test/hydration/samples/claim-static-incorrect-hash/_before.html create mode 100644 test/hydration/samples/claim-static-incorrect-hash/main.svelte create mode 100644 test/hydration/samples/claim-static-no-hash/_after.html create mode 100644 test/hydration/samples/claim-static-no-hash/_before.html create mode 100644 test/hydration/samples/claim-static-no-hash/main.svelte create mode 100644 test/hydration/samples/claim-text/_config.js delete mode 100644 test/server-side-rendering/samples/pre-tag/.editorconfig delete mode 100644 test/server-side-rendering/samples/pre-tag/_config.js delete mode 100644 test/server-side-rendering/samples/pre-tag/_expected.html delete mode 100644 test/server-side-rendering/samples/pre-tag/main.svelte delete mode 100644 test/server-side-rendering/samples/preserve-whitespaces/_config.js delete mode 100644 test/server-side-rendering/samples/preserve-whitespaces/_expected.html delete mode 100644 test/server-side-rendering/samples/preserve-whitespaces/main.svelte delete mode 100644 test/server-side-rendering/samples/textarea-content/.editorconfig delete mode 100644 test/server-side-rendering/samples/textarea-content/_config.js delete mode 100644 test/server-side-rendering/samples/textarea-content/_expected.html delete mode 100644 test/server-side-rendering/samples/textarea-content/main.svelte diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 9fb41b6108cc..b9d78ae7bd30 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -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; @@ -110,6 +111,8 @@ export default class Component { slots: Map = new Map(); slot_outlets: Set = new Set(); + tags: Tag[] = []; + constructor( ast: Ast, source: string, @@ -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() { @@ -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)) { diff --git a/src/compiler/compile/nodes/Attribute.ts b/src/compiler/compile/nodes/Attribute.ts index ebed483288a6..9e14a3233ebf 100644 --- a/src/compiler/compile/nodes/Attribute.ts +++ b/src/compiler/compile/nodes/Attribute.ts @@ -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() { diff --git a/src/compiler/compile/nodes/AwaitBlock.ts b/src/compiler/compile/nodes/AwaitBlock.ts index 4a669b6365b5..b61beab478bc 100644 --- a/src/compiler/compile/nodes/AwaitBlock.ts +++ b/src/compiler/compile/nodes/AwaitBlock.ts @@ -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); diff --git a/src/compiler/compile/nodes/EachBlock.ts b/src/compiler/compile/nodes/EachBlock.ts index 4659a53d73c7..b78f70337654 100644 --- a/src/compiler/compile/nodes/EachBlock.ts +++ b/src/compiler/compile/nodes/EachBlock.ts @@ -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 diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index ee62d3e7e8be..d38c299d36d3 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -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'; @@ -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() { @@ -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]/; diff --git a/src/compiler/compile/nodes/Head.ts b/src/compiler/compile/nodes/Head.ts index 1ebe0c053aa7..83319092b3e1 100644 --- a/src/compiler/compile/nodes/Head.ts +++ b/src/compiler/compile/nodes/Head.ts @@ -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; diff --git a/src/compiler/compile/nodes/IfBlock.ts b/src/compiler/compile/nodes/IfBlock.ts index 8351f1e0e271..4c337ac0cee8 100644 --- a/src/compiler/compile/nodes/IfBlock.ts +++ b/src/compiler/compile/nodes/IfBlock.ts @@ -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)); diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index e9ac86a55cf5..5fc7c89ae440 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -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); diff --git a/src/compiler/compile/nodes/KeyBlock.ts b/src/compiler/compile/nodes/KeyBlock.ts index 5261e14e0a0e..cd0ea56f8ea7 100644 --- a/src/compiler/compile/nodes/KeyBlock.ts +++ b/src/compiler/compile/nodes/KeyBlock.ts @@ -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); diff --git a/src/compiler/compile/nodes/RawMustacheTag.ts b/src/compiler/compile/nodes/RawMustacheTag.ts index 816ef8d7c579..99aeba26238e 100644 --- a/src/compiler/compile/nodes/RawMustacheTag.ts +++ b/src/compiler/compile/nodes/RawMustacheTag.ts @@ -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(); + } } diff --git a/src/compiler/compile/nodes/Slot.ts b/src/compiler/compile/nodes/Slot.ts index c21a48f5d665..9a0b1ea815f5 100644 --- a/src/compiler/compile/nodes/Slot.ts +++ b/src/compiler/compile/nodes/Slot.ts @@ -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(); } } diff --git a/src/compiler/compile/nodes/Text.ts b/src/compiler/compile/nodes/Text.ts index ac6cd9b82871..d750477c8a7f 100644 --- a/src/compiler/compile/nodes/Text.ts +++ b/src/compiler/compile/nodes/Text.ts @@ -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'; @@ -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(); + } } diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts index 65b10d04c820..900e78efc1fc 100644 --- a/src/compiler/compile/nodes/shared/Expression.ts +++ b/src/compiler/compile/nodes/shared/Expression.ts @@ -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 diff --git a/src/compiler/compile/nodes/shared/Node.ts b/src/compiler/compile/nodes/shared/Node.ts index 3c39f6d9d34b..9eeb110d201d 100644 --- a/src/compiler/compile/nodes/shared/Node.ts +++ b/src/compiler/compile/nodes/shared/Node.ts @@ -15,6 +15,7 @@ export default class Node { next?: INode; can_use_innerhtml: boolean; + is_static_content: boolean; var: string; attributes: Attribute[]; @@ -33,6 +34,9 @@ export default class Node { value: parent } }); + + this.can_use_innerhtml = true; + this.is_static_content = true; } cannot_use_innerhtml() { @@ -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); diff --git a/src/compiler/compile/nodes/shared/Tag.ts b/src/compiler/compile/nodes/shared/Tag.ts index bc826458d9a7..23d94ce01e02 100644 --- a/src/compiler/compile/nodes/shared/Tag.ts +++ b/src/compiler/compile/nodes/shared/Tag.ts @@ -8,6 +8,9 @@ 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 = ( @@ -15,4 +18,12 @@ export default class Tag extends Node { (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(); + } + } } diff --git a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts index 534595f4a9d9..e6807621d272 100644 --- a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts @@ -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; diff --git a/src/compiler/compile/render_dom/wrappers/Comment.ts b/src/compiler/compile/render_dom/wrappers/Comment.ts index a8c63b4227bf..9c4e2ffdfb6b 100644 --- a/src/compiler/compile/render_dom/wrappers/Comment.ts +++ b/src/compiler/compile/render_dom/wrappers/Comment.ts @@ -38,4 +38,10 @@ export default class CommentWrapper extends Wrapper { parent_node ); } + + text() { + if (!this.renderer.options.preserveComments) return ''; + + return ``; + } } diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts index 9c1af7ce44d4..36f733caef95 100644 --- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts @@ -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); diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts index 61311fd83ced..4b77c808e4ec 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts @@ -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); } } diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index cefebfcff35f..9906c29e3918 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -29,6 +29,7 @@ import is_dynamic from '../shared/is_dynamic'; import { is_name_contenteditable, has_contenteditable_attr } from '../../../utils/contenteditable'; import create_debugging_comment from '../shared/create_debugging_comment'; import { push_array } from '../../../../utils/push_array'; +import CommentWrapper from '../Comment'; interface BindingGroup { events: string[]; @@ -288,24 +289,6 @@ export default class ElementWrapper extends Wrapper { } }); - if (this.parent) { - if (node.actions.length > 0 || - node.animation || - node.bindings.length > 0 || - node.classes.length > 0 || - node.intro || node.outro || - node.handlers.length > 0 || - node.styles.length > 0 || - this.node.name === 'option' || - node.tag_expr.dynamic_dependencies().length || - node.is_dynamic_element || - renderer.options.dev - ) { - this.parent.cannot_use_innerhtml(); // need to use add_location - this.parent.not_static_content(); - } - } - this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling); this.element_data_name = block.get_unique_name(`${this.var.name}_data`); @@ -445,6 +428,7 @@ export default class ElementWrapper extends Wrapper { render_element(block: Block, parent_node: Identifier, parent_nodes: Identifier) { const { renderer } = this; + const hydratable = renderer.options.hydratable; if (this.node.name === 'noscript') return; @@ -458,13 +442,15 @@ export default class ElementWrapper extends Wrapper { b`${node} = ${render_statement};` ); - if (renderer.options.hydratable) { + const { can_use_textcontent, can_optimise_to_html_string } = this.node; + + if (hydratable) { if (parent_nodes) { block.chunks.claim.push(b` - ${node} = ${this.get_claim_statement(block, parent_nodes)}; + ${node} = ${this.get_claim_statement(block, parent_nodes, can_optimise_to_html_string)}; `); - if (!this.void && this.node.children.length > 0) { + if (!can_optimise_to_html_string && !this.void && this.node.children.length > 0) { block.chunks.claim.push(b` var ${nodes} = ${children}; `); @@ -502,15 +488,19 @@ export default class ElementWrapper extends Wrapper { // insert static children with textContent or innerHTML // skip textcontent for