Skip to content

feat: dedupe serialized hydratable values#18406

Open
Rich-Harris wants to merge 8 commits into
mainfrom
dedupe-hydratables
Open

feat: dedupe serialized hydratable values#18406
Rich-Harris wants to merge 8 commits into
mainfrom
dedupe-hydratables

Conversation

@Rich-Harris

@Rich-Harris Rich-Harris commented Jun 8, 2026

Copy link
Copy Markdown
Member

If two hydratables share a reference to a single object, that object is serialized twice in the SSR'd output. For example if you have a getUser query and a requireUser query that calls getUser (and throws a redirect if null), then you might end up with something like this:

const h = (window.__svelte ??= {}).h ??= new Map();

for (const [k, v] of [
	["1tmltq2/getUser/","[{\"id\":1,\"handle\":2,\"display_name\":3,\"avatar_url\":4,\"created_at\":5,\"updated_at\":6},\"did:plc:anvvmj5rdxhzo26gmhkgshnn\",\"rich-harris.dev\",\"rich harris\",\"https://cdn.bsky.app/img/avatar/plain/did:plc:anvvmj5rdxhzo26gmhkgshnn/bafkreidhhkeoflxd5vgqayffe3c5dhfmd2dvhtxkipzd22rigbw42mu3ri\",[\"Date\",\"2026-06-07T19:40:24.493Z\"],[\"Date\",\"2026-06-08T15:20:40.795Z\"]]"],
	["1tmltq2/requireUser/","[{\"id\":1,\"handle\":2,\"display_name\":3,\"avatar_url\":4,\"created_at\":5,\"updated_at\":6},\"did:plc:anvvmj5rdxhzo26gmhkgshnn\",\"rich-harris.dev\",\"rich harris\",\"https://cdn.bsky.app/img/avatar/plain/did:plc:anvvmj5rdxhzo26gmhkgshnn/bafkreidhhkeoflxd5vgqayffe3c5dhfmd2dvhtxkipzd22rigbw42mu3ri\",[\"Date\",\"2026-06-07T19:40:24.493Z\"],[\"Date\",\"2026-06-08T15:20:40.795Z\"]]"],
	// ...
]) {
	h.set(k, v);
}

If we devalue.uneval all the entries together, we avoid this.

TODO

  • figure out how best to report serialization errors
  • delete some now-unused code (while still erroring on mismatches in dev)

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code within packages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

@changeset-bot

changeset-bot Bot commented Jun 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6f84293

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

This PR includes changesets to release 1 package
Name Type
svelte 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

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Playground

pnpm add https://pkg.pr.new/svelte@18406

@svelte-docs-bot

Copy link
Copy Markdown

@Rich-Harris Rich-Harris changed the title WIP dedupe hydratables feat: dedupe serialized hydratable values Jun 8, 2026
@Rich-Harris Rich-Harris marked this pull request as ready for review June 8, 2026 21:24
Comment thread packages/svelte/src/internal/server/renderer.js
Comment thread packages/svelte/src/internal/server/hydratable.js
@Rich-Harris

Copy link
Copy Markdown
Member Author

(It turns out this isn't sufficient to dedupe getUser and requireUser in the example above, because they're pre-serialized. But it is necessary for us to be able to solve that problem.)

vercel Bot and others added 3 commits June 8, 2026 21:35
…ng warnings due to missing `return` statement in catch block

This commit fixes the issue reported at packages/svelte/src/internal/server/hydratable.js:97

## Bug Analysis

### What's happening
The `compare` function in `hydratable.js` is designed to check whether `hydratable()` is called twice with the same key but different values. It tries to serialize both values and compare them:

```javascript
try {
    if ((await serialize(a.value)) === (await serialize(b.value))) {
        return;  // Values are equal, no error
    }
} catch {
    // disregard any errors that happen during serialization,
    // they will be dealt with separately
    // BUG: No return here!
}

// This code runs for BOTH "values differ" AND "serialization failed"
e.hydratable_clobbering(key, stack);  // Called incorrectly for serialization errors
```

### The problem
When `serialize()` throws an error:
1. The catch block executes
2. Code falls through to `e.hydratable_clobbering()` 
3. A false "clobbering" error is reported

The comment says errors "will be dealt with separately" - and indeed they are! The `renderer.js` file (around line 870) has proper handling using `e.hydratable_serialization_failed()` for serialization errors. The `compare` function should simply return and not interfere.

### Impact
Users would see confusing "Attempted to set `hydratable` with key ... twice with different values" errors when the actual issue is that the value is not serializable. This makes debugging harder since the error message is misleading.

### The fix
Add `return;` inside the catch block so that serialization errors cause the function to exit cleanly without reporting a clobbering error:

```javascript
} catch {
    // disregard any errors that happen during serialization,
    // they will be dealt with separately
    return;
}
```

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Rich-Harris <hello@rich-harris.dev>
Rich-Harris added a commit to sveltejs/kit that referenced this pull request Jun 11, 2026
I thought that sveltejs/svelte#18406 would be
necessary to do this, but I was wrong — it actually seems easier and
simpler to bypass `hydratable` altogether. Instead, we serialize all the
remote data in one go, allowing devalue to do its thing and deduplicate
everything. (I still think it's worth merging that PR.)

That way, if you have (for example) a `getUser(): Promise<User | null>`
and a `requireUser(): Promise<User>` that calls `getUser` internally
(and redirects if it returns `null`), the `User` object is only
serialized once.

I can't remember exactly why we chose to use `hydratable` in the first
place. Perhaps it was a lifecycle thing — we wanted to be careful about
data being stale by the time it was read? In which case I think that has
changed now that query lifecycle is determined by the garbage collector.
Maybe @elliott-with-the-longest-name-on-github remembers.

---

### Please don't delete this checklist! Before submitting the PR, please
make sure you do the following:
- [ ] It's really useful if your PR references an issue where it is
discussed ahead of time. In many cases, features are absent for a
reason. For large changes, please create an RFC:
https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it
solves.
- [x] Ideally, include a test that fails without this PR but passes with
it.

### Tests
- [ ] Run the tests with `pnpm test` and lint the project with `pnpm
lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more
packages' changelogs, generate a changeset by running `pnpm changeset`
and following the prompts. Changesets that add features should be
`minor` and those that fix bugs should be `patch`. Please prefix
changeset messages with `feat:`, `fix:`, or `chore:`.

### Edits

- [x] Please ensure that 'Allow edits from maintainers' is checked. PRs
without this option may be closed.

---------

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Elliott Johnson <hello@ell.iott.dev>
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.

1 participant