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 9fb41b6108c..b9d78ae7bd3 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 ebed483288a..9e14a3233eb 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 4a669b6365b..b61beab478b 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 4659a53d73c..b78f7033765 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 ee62d3e7e8b..d38c299d36d 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 1ebe0c053aa..83319092b3e 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 8351f1e0e27..4c337ac0cee 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 e9ac86a55cf..5fc7c89ae44 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 5261e14e0a0..cd0ea56f8ea 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 816ef8d7c57..99aeba26238 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 c21a48f5d66..9a0b1ea815f 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 ac6cd9b8287..d750477c8a7 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 65b10d04c82..900e78efc1f 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 3c39f6d9d34..9eeb110d201 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 bc826458d9a..23d94ce01e0 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 534595f4a9d..e6807621d27 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 a8c63b4227b..9c4e2ffdfb6 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 9c1af7ce44d..36f733caef9 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 61311fd83ce..4b77c808e4e 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 cefebfcff35..9906c29e391 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