chore: document `@html` and `<img src>` hydration change (#12373)

* chore: document `@html` and `<img src>` hydration change

Also add a test for it

closes #12333

* add a test

* Update sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* lint

* update example and wording

* update test

* since it turns out we already had a test, we can delete the new one

* fix test

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/12397/head
Simon H 2 months ago committed by GitHub
parent 4e8d1c8c52
commit 42a7a0ecd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -23,7 +23,7 @@ function check_hash(element, server_hash, value) {
const loc = element.__svelte_meta?.loc; const loc = element.__svelte_meta?.loc;
if (loc) { if (loc) {
location = `near ${loc.file}:${loc.line}:${loc.column}`; location = `near ${loc.file}:${loc.line}:${loc.column}`;
} else if (dev_current_component_function.filename) { } else if (dev_current_component_function?.filename) {
location = `in ${dev_current_component_function.filename}`; location = `in ${dev_current_component_function.filename}`;
} }
@ -59,6 +59,8 @@ export function html(node, get_value, svg, mathml) {
effect = branch(() => { effect = branch(() => {
if (hydrating) { if (hydrating) {
// We're deliberately not trying to repair mismatches between server and client,
// as it's costly and error-prone (and it's an edge case to have a mismatch anyway)
var hash = /** @type {Comment} */ (hydrate_node).data; var hash = /** @type {Comment} */ (hydrate_node).data;
var next = hydrate_next(); var next = hydrate_next();
var last = next; var last = next;

@ -69,7 +69,7 @@ export async function compile_directory(
fs.rmSync(output_dir, { recursive: true, force: true }); fs.rmSync(output_dir, { recursive: true, force: true });
for (const file of glob('**', { cwd, filesOnly: true })) { for (let file of glob('**', { cwd, filesOnly: true })) {
if (file.startsWith('_')) continue; if (file.startsWith('_')) continue;
let text = fs.readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r\n/g, '\n'); let text = fs.readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r\n/g, '\n');
@ -101,7 +101,17 @@ export async function compile_directory(
write(out, result); write(out, result);
} }
} else if (file.endsWith('.svelte')) { } else if (
file.endsWith('.svelte') &&
// Make it possible to compile separate versions for client and server to simulate
// cases where `{browser ? 'foo' : 'bar'}` is turning into `{'foo'}` on the server
// and `{bar}` on the client, assuming we have sophisticated enough treeshaking
// in the future to make this a thing.
(!file.endsWith('.server.svelte') || generate === 'server') &&
(!file.endsWith('.client.svelte') || generate === 'client')
) {
file = file.replace(/\.client\.svelte$/, '.svelte').replace(/\.server\.svelte$/, '.svelte');
if (preprocessor?.preprocess) { if (preprocessor?.preprocess) {
const preprocessed = await preprocess( const preprocessed = await preprocess(
text, text,

@ -1,19 +0,0 @@
import { test } from '../../test';
export default test({
server_props: {
browser: false
},
props: {
browser: true
},
compileOptions: {
dev: true
},
errors: [
'The value of an `{@html ...}` block in packages/svelte/tests/hydration/samples/html-tag-hydration-2/main.svelte changed between server and client renders. The client value will be ignored in favour of the server value'
]
});

@ -1,5 +0,0 @@
<script lang="ts">
let { browser } = $props();
</script>
{@html browser ? 'browser' : 'server'}

@ -0,0 +1,17 @@
import { test } from '../../test';
export default test({
server_props: {
src: 'server.jpg'
},
props: {
src: 'client.jpg'
},
test(assert, target) {
// We deliberately don't slow down hydration just for supporting this edge case mismatch.
assert.htmlEqual(target.innerHTML, '<img src="server.jpg" alt="">');
},
errors: [
'The `src` attribute on `...<img src="server.jpg" alt="">` changed its value between server and client renders. The client value, `client.jpg`, will be ignored in favour of the server value'
]
});

@ -0,0 +1,5 @@
<script>
let { src } = $props();
</script>
<img {src} alt="" />

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
test(assert, target) {
// This test case guards against a potential future bug where we could optimize html tags away for static content:
// Even if the {@html } block seems static, it should be preserved as such, because it could be dynamic originally
// (like {@html browser ? 'foo' : 'bar'} which is then different between client and server.
// Also see https://github.com/sveltejs/svelte/issues/8683 where this happened for Svelte 4.
assert.htmlEqual(target.innerHTML, 'Server');
},
errors: [
'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'
]
});

@ -0,0 +1 @@
{@html '<p>Client</p> <span>has more nodes so if we would walk this because we think it is static we would get an error</span>'}

@ -1,10 +1,21 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
// Even if the {@html } block seems static, it should be preserved as such, because it could be dynamic originally server_props: {
// (like {@html browser ? 'foo' : 'bar'} which is then different between client and server. html: 'Server'
// Question is whether that's actually something someone would do in practise, and why, so it's probably better to not },
// slow down hydration just for supporting this edge case - so far we've said no. If someone really needs this we could
// add something like {@html dynamic ...} props: {
skip: true html: 'Client'
},
test(assert, target) {
// We deliberately don't slow down hydration just for supporting this edge case mismatch.
// If someone really needs this and workarounds are insufficient we could add something like {@html dynamic ...}
assert.htmlEqual(target.innerHTML, 'Server');
},
errors: [
'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'
]
}); });

@ -1 +1,5 @@
{@html '<p>foo</p>'} <script>
let { html } = $props();
</script>
{@html html}

@ -287,3 +287,32 @@ Note that whereas Svelte 4 would treat `<svelte:element this="input">` (for exam
### `mount` plays transitions by default ### `mount` plays transitions by default
The `mount` function used to render a component tree plays transitions by default unless the `intro` option is set to `false`. This is different from legacy class components which, when manually instantiated, didn't play transitions by default. The `mount` function used to render a component tree plays transitions by default unless the `intro` option is set to `false`. This is different from legacy class components which, when manually instantiated, didn't play transitions by default.
### `<img src={...}>` and `{@html ...}` hydration mismatches are not repaired
In Svelte 4, if the value of a `src` attribute or `{@html ...}` tag differ between server and client (a.k.a. a hydration mismatch), the mismatch is repaired. This is very costly: setting a `src` attribute (even if it evaluates to the same thing) causes images and iframes to be reloaded, and reinserting a large blob of HTML is slow.
Since these mismatches are extremely rare, Svelte 5 assumes that the values are unchanged, but in development will warn you if they are not. To force an update you can do something like this:
```svelte
<script>
let { markup, src } = $props();
if (typeof window !== 'undefined') {
// stash the values...
const initial = { markup, src };
// unset them...
markup = src = undefined;
$effect(() => {
// ...and reset after we've mounted
markup = initial.markup;
src = initial.src;
});
}
</script>
{@html markup}
<img {src} />
```

Loading…
Cancel
Save