From 3c36a79dd381b486461e20a8b4912bf8862d0f9b Mon Sep 17 00:00:00 2001 From: adiguba Date: Thu, 25 Jun 2026 13:10:13 +0200 Subject: [PATCH 1/3] Fix memoizer on style/class directives --- .../client/visitors/RegularElement.js | 22 ++-------- .../class-directive-memoize/_config.js | 38 ++++++++++++++++ .../class-directive-memoize/main.svelte | 10 +++++ .../style-directive-memoize/_config.js | 44 +++++++++++++++++++ .../style-directive-memoize/main.svelte | 10 +++++ 5 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/class-directive-memoize/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-directive-memoize/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/style-directive-memoize/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/style-directive-memoize/main.svelte 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..e935a91c88 --- /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..1e905ebdb4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/_config.js @@ -0,0 +1,44 @@ +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 From bdcdb298892421dd6ebb37c26271ad48eece86c7 Mon Sep 17 00:00:00 2001 From: adiguba Date: Mon, 6 Jul 2026 19:25:25 +0200 Subject: [PATCH 2/3] fix lint --- .../runtime-runes/samples/class-directive-memoize/_config.js | 2 +- .../runtime-runes/samples/style-directive-memoize/_config.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) 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 index e935a91c88..b90705c3ce 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/_config.js @@ -19,7 +19,7 @@ export default test({ async test({ target, assert, logs }) { flushSync(); tick(); - + assert.deepEqual(logs, ['is_red()']); const btn = target.querySelector('button'); 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 index 1e905ebdb4..0fa21f2d48 100644 --- a/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/_config.js @@ -19,7 +19,7 @@ export default test({ async test({ target, assert, component, logs }) { flushSync(); tick(); - + assert.deepEqual(logs, ['makeColor()']); const btn = target.querySelector('button'); @@ -35,7 +35,6 @@ export default test({ flushSync(); - assert.equal(div?.style.backgroundColor, 'red'); assert.equal(div?.style.fontSize, '2em'); // only one call here From b6dbf46eb73eb540415c7fe38e6817feac2e9ba9 Mon Sep 17 00:00:00 2001 From: adiguba Date: Mon, 6 Jul 2026 19:27:05 +0200 Subject: [PATCH 3/3] add changset --- .changeset/lemon-bikes-marry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lemon-bikes-marry.md 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