diff --git a/.changeset/lemon-bikes-marry.md b/.changeset/lemon-bikes-marry.md new file mode 100644 index 0000000000..0bb6d5ef18 --- /dev/null +++ b/.changeset/lemon-bikes-marry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +distinct memoizer on style/class directives diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 8dcd3c7d6f..75fadd51f6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -4,7 +4,6 @@ /** @import { Scope } from '../../../scope' */ import { cannot_be_set_statically, - is_boolean_attribute, is_dom_property, is_load_error_element } from '../../../../../utils.js'; @@ -13,7 +12,6 @@ import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js' import * as b from '#compiler/builders'; import { create_attribute, - ExpressionMetadata, is_custom_element_node, is_customizable_select_element } from '../../../nodes.js'; @@ -528,18 +526,12 @@ export function build_class_directives_object( ) { let properties = []; - const metadata = new ExpressionMetadata(); - for (const d of class_directives) { - metadata.merge(d.metadata.expression); - const expression = /** @type Expression */ (context.visit(d.expression)); - properties.push(b.init(d.name, expression)); + properties.push(b.init(d.name, memoizer.add(expression, d.metadata.expression))); } - const directives = b.object(properties); - - return memoizer.add(directives, metadata); + return b.object(properties); } /** @@ -555,23 +547,17 @@ export function build_style_directives_object( const normal = b.object([]); const important = b.object([]); - const metadata = new ExpressionMetadata(); - for (const d of style_directives) { - metadata.merge(d.metadata.expression); - const expression = d.value === true ? build_getter(b.id(d.name), context.state) : build_attribute_value(d.value, context).value; const object = d.modifiers.includes('important') ? important : normal; - object.properties.push(b.init(d.name, expression)); + object.properties.push(b.init(d.name, memoizer.add(expression, d.metadata.expression))); } - const directives = important.properties.length ? b.array([normal, important]) : normal; - - return memoizer.add(directives, metadata); + return important.properties.length ? b.array([normal, important]) : normal; } /** diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/_config.js b/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/_config.js new file mode 100644 index 0000000000..b90705c3ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/_config.js @@ -0,0 +1,38 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +// This test counts mutations on hydration +// set_style() should not mutate style on hydration, except if mismatch +export default test({ + mode: ['server', 'hydrate', 'client'], + + ssrHtml: ` + +
+ `, + + html: ` + +
+ `, + + async test({ target, assert, logs }) { + flushSync(); + tick(); + + assert.deepEqual(logs, ['is_red()']); + + const btn = target.querySelector('button'); + const div = target.querySelector('div'); + + assert.equal(div?.className, 'red'); + + btn?.click(); + + flushSync(); + + assert.equal(div?.className, 'red active'); + // only one call here + assert.deepEqual(logs, ['is_red()']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/main.svelte new file mode 100644 index 0000000000..3a6d02e626 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/main.svelte @@ -0,0 +1,10 @@ + + +
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/_config.js b/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/_config.js new file mode 100644 index 0000000000..0fa21f2d48 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/_config.js @@ -0,0 +1,43 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +// This test counts mutations on hydration +// set_style() should not mutate style on hydration, except if mismatch +export default test({ + mode: ['server', 'hydrate', 'client'], + + ssrHtml: ` + +
+ `, + + html: ` + +
+ `, + + async test({ target, assert, component, logs }) { + flushSync(); + tick(); + + assert.deepEqual(logs, ['makeColor()']); + + const btn = target.querySelector('button'); + const div = target.querySelector('div'); + + // Note : we cannot compare HTML because set_style() use dom.style.cssText + // which can alter the format of the attribute... + + assert.equal(div?.style.backgroundColor, 'red'); + assert.equal(div?.style.fontSize, '1em'); + + btn?.click(); + + flushSync(); + + assert.equal(div?.style.backgroundColor, 'red'); + assert.equal(div?.style.fontSize, '2em'); + // only one call here + assert.deepEqual(logs, ['makeColor()']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/main.svelte b/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/main.svelte new file mode 100644 index 0000000000..444d686745 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/main.svelte @@ -0,0 +1,10 @@ + + +
\ No newline at end of file