Skip to content

fix: use mouseover+mousemove for preloading to reduce events#16325

Merged
Rich-Harris merged 5 commits into
version-3from
mousemover-mouseover
Jul 14, 2026
Merged

fix: use mouseover+mousemove for preloading to reduce events#16325
Rich-Harris merged 5 commits into
version-3from
mousemover-mouseover

Conversation

@dummdidumm

Copy link
Copy Markdown
Member

Instead of always firing the mousemove event, we first use mouseover which fires less often, and only if we enter a link we start the mousemove logic. We do that to preserve the "mouse comes to a rest" behavior of the previous solution.

Closes #12664

Instead of always firing the mousemove event, we first use mouseover which fires less often, and only if we enter a link we start the mousemove logic. We do that to preserve the "mouse comes to a rest" behavior of the previous solution.

Closes #12664
@pkg-svelte-dev

pkg-svelte-dev Bot commented Jul 10, 2026

Copy link
Copy Markdown

Install the latest version of @sveltejs/kit from 8fe0bfd:

pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/8fe0bfd8046d8e7c019bfdbbb6dd7bb09150f64f

Open in pkg.svelte.dev: https://pkg.svelte.dev/repos/kit/pr/16325

@changeset-bot

changeset-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8fe0bfd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@svelte-docs-bot

Copy link
Copy Markdown

Comment on lines +2153 to +2159
clear_hover_preload();
hovered_a = a;
start_hover_preload();
// Instead of just preloading right away, we start a mousemove listener to implement
// "mouse comes to a rest" behavior. This avoid false positives when you just move
// your mouse across the screen and happen to pass over a link.
container.addEventListener('mousemove', mousemove);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't mousemove immediately call start_hover_preload again if you move your mouse at all, which clears the timeout and starts it over?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but theoretically there could be no mousemove event after the mouseover, so it's safer to start the hover preload on mouseover, too.

Comment thread packages/kit/src/runtime/client/client.js
Comment thread packages/kit/src/runtime/client/client.js Outdated
Comment thread packages/kit/src/runtime/client/client.js
…never removed, cancelling hover-preload for the newly-hovered adjacent anchor

This commit fixes the issue reported at packages/kit/src/runtime/client/client.js:2120

## Bug

In `setup_preload()` (`packages/kit/src/runtime/client/client.js`), each preloadable anchor gets two listeners on `mouseover`:

```js
a.addEventListener('mousemove', start_hover_preload);
a.addEventListener('mouseleave', clear_hover_preload, { once: true });
```

But `clear_hover_preload()` only removed the `mousemove` listener:

```js
function clear_hover_preload() {
    clearTimeout(mousemove_timeout);
    hovered_a?.removeEventListener('mousemove', start_hover_preload);
    hovered_a = undefined;
}
```

The `mouseleave` listener on the previous anchor is left attached.

### Trigger: moving the cursor directly from anchor A to adjacent anchor B

Per the DOM UI Events spec, moving from element A to a sibling B dispatches events in this order:

```
mouseout(A) → mouseover(B) → mouseleave(A) → mouseenter(B)
```

Note `mouseover(B)` fires **before** `mouseleave(A)`.

1.  Hovering A: `hovered_a = A`; A has `mousemove` + `mouseleave` (once) listeners.
2.  `mouseover(B)` bubbles to `container`: `find_anchor` returns B, `B !== A`, so `clear_hover_preload()` runs (clears timeout, removes only `mousemove` from A). Then `hovered_a = B`, B gets its listeners, and `start_hover_preload()` schedules B's preload timeout. **A's `mouseleave` listener is still attached.**
3.  `mouseleave(A)` now fires the still-attached once-listener → `clear_hover_preload()` runs again → `clearTimeout` cancels B's just-scheduled preload, removes `mousemove` from `hovered_a` (now B), and sets `hovered_a = undefined`.

Net result: B never preloads on hover. In a nav menu / list of adjacent links this breaks hover-preload for essentially every direct link-to-link transition.

## Fix

Remove the `mouseleave` listener inside `clear_hover_preload` as well:

```js
function clear_hover_preload() {
    clearTimeout(mousemove_timeout);
    hovered_a?.removeEventListener('mousemove', start_hover_preload);
    hovered_a?.removeEventListener('mouseleave', clear_hover_preload);
    hovered_a = undefined;
}
```

During step 2, `clear_hover_preload()` now runs while `hovered_a` is still A, so A's `mouseleave` listener is removed *before* the browser dispatches `mouseleave(A)`. The stale handler therefore never fires, and B's preload survives. (Even though the listener was registered with `{ once: true }`, the browser only auto-removes it upon dispatch; explicit removal before dispatch prevents the firing.)


Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Rich-Harris <hello@rich-harris.dev>
@Rich-Harris Rich-Harris merged commit 4a513e2 into version-3 Jul 14, 2026
18 checks passed
@Rich-Harris Rich-Harris deleted the mousemover-mouseover branch July 14, 2026 01:56
Rich-Harris pushed a commit that referenced this pull request Jul 14, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to version-3, this PR
will be updated.

⚠️⚠️⚠️⚠️⚠️⚠️

`version-3` is currently in **pre mode** so this branch has prereleases
rather than normal releases. If you want to exit prereleases, run
`changeset pre exit` on `version-3`.

⚠️⚠️⚠️⚠️⚠️⚠️

# Releases
## @sveltejs/kit@3.0.0-next.8

### Major Changes

- breaking: remove `experimental.handleRenderingErrors` flag
([#16265](#16265))

- breaking: make `getRequest` and `setResponse` synchronous
([#16280](#16280))

- breaking: make `page.url` immutable on a type level
([#16256](#16256))

- breaking: add `refreshAll` and deprecate `invalidateAll`
([#16289](#16289))

### Minor Changes

- feat: allow hyphens in param and matcher names
([#16284](#16284))

- feat: add `ErrorProps` to generated types
([#16272](#16272))

### Patch Changes

- fix: detect destructured `load` and `actions` exports during type
generation ([#16329](#16329))

- fix: ensure CSS URL references are absolute when `paths.relative` is
`false` ([#16315](#16315))

- fix: exclude deleted cookies from `cookies.getAll()` so it stays
consistent with `cookies.get()`
([#16297](#16297))

- fix: reset failed `<svelte:boundary>` on client navigation so a stale
`+error.svelte` is torn down
([#16296](#16296))

- fix: preserve shared client chunk hashes when the app version changes
([#16324](#16324))

- fix: align MAX_COOKIE_SIZE with RFC 6265bis
([#16322](#16322))

- fix: use mouseover+mousemove for preloading to reduce events
([#16325](#16325))
## @sveltejs/package@3.0.0-next.2

### Minor Changes

- feat: warn when using a `.server.` file or file inside a `server`
directory without importing a server-only module
([#16266](#16266))
## @sveltejs/adapter-auto@8.0.0-next.1

### Patch Changes

- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer
dependency range ([#16286](#16286))
- Updated dependencies
[[`737d119`](737d119),
[`fa78efb`](fa78efb),
[`07c207e`](07c207e),
[`a47071b`](a47071b),
[`14d7d5a`](14d7d5a),
[`5c38e51`](5c38e51),
[`0702baa`](0702baa),
[`e1938c6`](e1938c6),
[`8293144`](8293144),
[`f76d7d9`](f76d7d9),
[`ab5c253`](ab5c253),
[`b557b1b`](b557b1b),
[`4a513e2`](4a513e2)]:
  - @sveltejs/kit@3.0.0-next.8
## @sveltejs/adapter-cloudflare@8.0.0-next.2

### Patch Changes

- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer
dependency range ([#16286](#16286))
- Updated dependencies
[[`737d119`](737d119),
[`fa78efb`](fa78efb),
[`07c207e`](07c207e),
[`a47071b`](a47071b),
[`14d7d5a`](14d7d5a),
[`5c38e51`](5c38e51),
[`0702baa`](0702baa),
[`e1938c6`](e1938c6),
[`8293144`](8293144),
[`f76d7d9`](f76d7d9),
[`ab5c253`](ab5c253),
[`b557b1b`](b557b1b),
[`4a513e2`](4a513e2)]:
  - @sveltejs/kit@3.0.0-next.8
## @sveltejs/adapter-netlify@7.0.0-next.3

### Patch Changes

- fix: include `utils.js` in package.json `files`
([#16298](#16298))
- Updated dependencies
[[`737d119`](737d119),
[`fa78efb`](fa78efb),
[`07c207e`](07c207e),
[`a47071b`](a47071b),
[`14d7d5a`](14d7d5a),
[`5c38e51`](5c38e51),
[`0702baa`](0702baa),
[`e1938c6`](e1938c6),
[`8293144`](8293144),
[`f76d7d9`](f76d7d9),
[`ab5c253`](ab5c253),
[`b557b1b`](b557b1b),
[`4a513e2`](4a513e2)]:
  - @sveltejs/kit@3.0.0-next.8
## @sveltejs/adapter-node@6.0.0-next.3

### Patch Changes

- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer
dependency range ([#16286](#16286))
- Updated dependencies
[[`737d119`](737d119),
[`fa78efb`](fa78efb),
[`07c207e`](07c207e),
[`a47071b`](a47071b),
[`14d7d5a`](14d7d5a),
[`5c38e51`](5c38e51),
[`0702baa`](0702baa),
[`e1938c6`](e1938c6),
[`8293144`](8293144),
[`f76d7d9`](f76d7d9),
[`ab5c253`](ab5c253),
[`b557b1b`](b557b1b),
[`4a513e2`](4a513e2)]:
  - @sveltejs/kit@3.0.0-next.8
## @sveltejs/adapter-static@4.0.0-next.1

### Patch Changes

- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer
dependency range ([#16286](#16286))
- Updated dependencies
[[`737d119`](737d119),
[`fa78efb`](fa78efb),
[`07c207e`](07c207e),
[`a47071b`](a47071b),
[`14d7d5a`](14d7d5a),
[`5c38e51`](5c38e51),
[`0702baa`](0702baa),
[`e1938c6`](e1938c6),
[`8293144`](8293144),
[`f76d7d9`](f76d7d9),
[`ab5c253`](ab5c253),
[`b557b1b`](b557b1b),
[`4a513e2`](4a513e2)]:
  - @sveltejs/kit@3.0.0-next.8
## @sveltejs/adapter-vercel@7.0.0-next.2

### Patch Changes

- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer
dependency range ([#16286](#16286))
- Updated dependencies
[[`737d119`](737d119),
[`fa78efb`](fa78efb),
[`07c207e`](07c207e),
[`a47071b`](a47071b),
[`14d7d5a`](14d7d5a),
[`5c38e51`](5c38e51),
[`0702baa`](0702baa),
[`e1938c6`](e1938c6),
[`8293144`](8293144),
[`f76d7d9`](f76d7d9),
[`ab5c253`](ab5c253),
[`b557b1b`](b557b1b),
[`4a513e2`](4a513e2)]:
  - @sveltejs/kit@3.0.0-next.8

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mousemove event listener is being attached in setup_preload even if I am not using data-sveltekit-preload-[x]="hover"

4 participants