Skip to content

UI derived state updates during preload with experimental.async: true #14931

@geodask

Description

@geodask

Describe the bug

When experimental.async: true is enabled in SvelteKit config, hovering over links (which triggers preloading) causes derived values to update inconsistently in the DOM. Specifically, when a $derived value is used in both an attribute spread and as text content, the attribute updates immediately on hover while the text content doesn't update until the link is actually clicked.
This inconsistent behavior only occurs with preloading (on hover by default). Setting data-sveltekit-preload-data="off" resolves the issue, confirming it's related to the preloading mechanism.

Reproduction

Minimal reproduction: https://stackblitz.com/edit/sveltejs-kit-template-default-dqe2gmpu?description=The%20default%20SvelteKit%20template,%20generated%20with%20create-svelte&title=SvelteKit%20Default%20Template

Steps to reproduce:

  1. Open the reproduction link above
  2. Navigate to /posts/1
  3. Hover over the link to /posts/2 (without clicking)
  4. Observe that the data-active attribute in the DOM becomes true
  5. Notice the text content still shows false
  6. Click the link
  7. Now the text content updates to true

Expected behavior: Neither the attribute nor the text content should update on hover. Both should only update when the link is actually clicked and navigation occurs. Preloading should be invisible to the UI.

<!-- src/routes/posts/[postId]/+page.svelte -->
<script lang="ts">
    import { page } from '$app/state';
    import Link from './link.svelte';

    const posts = [
        { id: '1', title: 'Post 1' },
        { id: '2', title: 'Post 2' },
        { id: '3', title: 'Post 3' }
    ];
</script>

<ul>
    {#each posts as post (post.id)}
        <li>
            <Link href="/posts/{post.id}" isActive={page.params.postId == post.id} />
        </li>
    {/each}
</ul>
<!-- src/routes/posts/[postId]/link.svelte -->
<script lang="ts">
    const { isActive, href }: { isActive: boolean; href: string } = $props();
    const id = $props.id();

    const attrs = $derived({
        'data-active': isActive
    });
</script>

<a {href} {...attrs}>
    {id}: {attrs['data-active']}
</a>

<style>
    [data-active='true'] {
        font-weight: bold;
    }
</style>
// svelte.config.js
import adapter from '@sveltejs/adapter-auto';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	kit: {
		// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
		// If your environment is not supported or you settled on a specific environment, switch out the adapter.
		// See https://kit.svelte.dev/docs/adapters for more information about adapters.
		adapter: adapter(),
	},
	compilerOptions: {
		experimental: {
			async: true
		}
	}
};

export default config;

Logs

System Info

npmPackages:
   @sveltejs/adapter-auto: ^7.0.0 => 7.0.0 
   @sveltejs/kit: ^2.48.5 => 2.48.5 
   @sveltejs/vite-plugin-svelte: ^6.2.1 => 6.2.1 
   svelte: ^5.43.7 => 5.43.7 
   vite: ^7.2.2 => 7.2.2

Severity

serious, but I can work around it

Additional Information

  • With experimental.async: false, everything works correctly - nothing updates on hover, both attribute and text update together on click
  • Setting data-sveltekit-preload-data="off" on the link prevents the issue
  • The issue appears to be that preloading is somehow causing page.params (or values derived from it) to update prematurely for attributes but not for text content
  • The same $derived value (attrs['data-active']) behaves differently depending on where it's used (attribute spread vs text content)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions