Repository: sveltejs/svelte (2025-07-17T08:32:20Z)

Issue:
Optimize callbacks inside each block with loop variable names
### Describe the bug

Buttons B and C have identical callbacks, but the generated JS output differs:

```js
// B
button_1.__click = (e) => {
  const index = Number(e.currentTarget.dataset.index);
  console.log(index);
};
```

```js
// C
var on_click = (e, state) => {
  const i = Number(e.currentTarget.dataset.index);
  $.set(state, $.get(state) + (i + 1));
};
button_2.__click = [on_click];
```

This is because the `index` variable name is reused inside the callback:

```svelte
{#each { length: 1 } as _, index}
  <!-- New callback per-loop -->
  <button type="button" onclick={() => console.log(index)}>A</button>

  <!-- New callback per-loop -->
  <button
    type="button"
    data-index={index}
    onclick={(e) => {
      const index = Number(e.currentTarget.dataset.index)!;
      console.log(index);
    }}>B</button
  >

  <!-- Shared callback between loops -->
  <button
    type="button"
    data-index={index}
    onclick={(e) => {
      // Variable name has been changed for optimization.
      const i = Number(e.currentTarget.dataset.index)!;
      console.log(i);
    }}>C</button
  >
{/each}
```

If the loop variables (e.g. `index`) are redefined in the first line (e.g. `const index =`), they can be optimized.

(Without renaming `index` to `i` in this case.)

### Reproduction

https://svelte.dev/playground/93ce89c9e31248b3947dc23ae54e5992?version=5.36.5

### Logs

```shell

```

### System Info

```shell
-
```

### Severity

annoyance


Discussion:

Patch:
diff --git a/.changeset/fast-parrots-draw.md b/.changeset/fast-parrots-draw.md
new file mode 100644
index 000000000000..e56a35ff9b9c
--- /dev/null
+++ b/.changeset/fast-parrots-draw.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: delegate functions with shadowed variables if declared locally
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
index 773aa597444b..b13f3f89b624 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
@@ -192,8 +192,13 @@ function get_delegated_event(event_name, handler, context) {
 			return unhoisted;
 		}
 
-		// If we are referencing a binding that is shadowed in another scope then bail out.
-		if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
+		// If we are referencing a binding that is shadowed in another scope then bail out (unless it's declared within the function).
+		if (
+			local_binding !== null &&
+			binding !== null &&
+			local_binding.node !== binding.node &&
+			scope.declarations.get(reference) !== binding
+		) {
 			return unhoisted;
 		}
 


Test patch:
diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js
new file mode 100644
index 000000000000..0d95d8d33551
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js
@@ -0,0 +1,28 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/legacy';
+import * as $ from 'svelte/internal/client';
+
+var on_click = (e) => {
+	const index = Number(e.currentTarget.dataset.index);
+
+	console.log(index);
+};
+
+var root_1 = $.from_html(`<button type="button">B</button>`);
+
+export default function Delegated_locally_declared_shadowed($$anchor) {
+	var fragment = $.comment();
+	var node = $.first_child(fragment);
+
+	$.each(node, 0, () => ({ length: 1 }), $.index, ($$anchor, $$item, index) => {
+		var button = root_1();
+
+		$.set_attribute(button, 'data-index', index);
+		button.__click = [on_click];
+		$.append($$anchor, button);
+	});
+
+	$.append($$anchor, fragment);
+}
+
+$.delegate(['click']);
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js
new file mode 100644
index 000000000000..e465af6f8b25
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js
@@ -0,0 +1,13 @@
+import * as $ from 'svelte/internal/server';
+
+export default function Delegated_locally_declared_shadowed($$payload) {
+	const each_array = $.ensure_array_like({ length: 1 });
+
+	$$payload.out += `<!--[-->`;
+
+	for (let index = 0, $$length = each_array.length; index < $$length; index++) {
+		$$payload.out += `<button type="button"${$.attr('data-index', index)}>B</button>`;
+	}
+
+	$$payload.out += `<!--]-->`;
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/index.svelte b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/index.svelte
new file mode 100644
index 000000000000..d870a6b27025
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/index.svelte
@@ -0,0 +1,12 @@
+<script lang="ts"></script>
+
+{#each { length: 1 }, index}
+	<button
+		type="button"
+		data-index={index}
+		onclick={(e) => {
+			const index = Number(e.currentTarget.dataset.index)!;
+			console.log(index);
+		}}>B</button
+	>
+{/each}