Skip to content

Commit f43e076

Browse files
committed
feat: add hydrate method, make hydration treeshakeable
Introduces a new `hydrate` method which does hydration. Refactors code so that hydration-related code is treeshaken out when not using that method. closes #9533 part of #9827
1 parent 9e98bb6 commit f43e076

File tree

12 files changed

+335
-148
lines changed

12 files changed

+335
-148
lines changed

.changeset/gorgeous-singers-rest.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: add hydrate method, make hydration treeshakeable

packages/svelte/scripts/check-treeshakeability.js

+74-24
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,33 @@ import path from 'node:path';
33
import { rollup } from 'rollup';
44
import virtual from '@rollup/plugin-virtual';
55
import { nodeResolve } from '@rollup/plugin-node-resolve';
6+
import { compile } from 'svelte/compiler';
7+
8+
async function bundle_code(entry) {
9+
const bundle = await rollup({
10+
input: '__entry__',
11+
plugins: [
12+
virtual({
13+
__entry__: entry
14+
}),
15+
nodeResolve({
16+
exportConditions: ['production', 'import', 'browser', 'default']
17+
})
18+
],
19+
onwarn: (warning, handle) => {
20+
// if (warning.code !== 'EMPTY_BUNDLE') handle(warning);
21+
}
22+
});
23+
24+
const { output } = await bundle.generate({});
25+
26+
if (output.length > 1) {
27+
throw new Error('errr what');
28+
}
29+
30+
const code = output[0].code.trim();
31+
return code;
32+
}
633

734
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
835

@@ -20,31 +47,8 @@ for (const key in pkg.exports) {
2047
if (!pkg.exports[key][type]) continue;
2148

2249
const subpackage = path.join(pkg.name, key);
23-
2450
const resolved = path.resolve(pkg.exports[key][type]);
25-
26-
const bundle = await rollup({
27-
input: '__entry__',
28-
plugins: [
29-
virtual({
30-
__entry__: `import ${JSON.stringify(resolved)}`
31-
}),
32-
nodeResolve({
33-
exportConditions: ['production', 'import', 'browser', 'default']
34-
})
35-
],
36-
onwarn: (warning, handle) => {
37-
// if (warning.code !== 'EMPTY_BUNDLE') handle(warning);
38-
}
39-
});
40-
41-
const { output } = await bundle.generate({});
42-
43-
if (output.length > 1) {
44-
throw new Error('errr what');
45-
}
46-
47-
const code = output[0].code.trim();
51+
const code = await bundle_code(`import ${JSON.stringify(resolved)}`);
4852

4953
if (code === '') {
5054
// eslint-disable-next-line no-console
@@ -59,6 +63,52 @@ for (const key in pkg.exports) {
5963
}
6064
}
6165

66+
const client_main = path.resolve(pkg.exports['.'].browser);
67+
const without_hydration = await bundle_code(
68+
// Use all features which contain hydration code to ensure it's treeshakeable
69+
compile(
70+
`
71+
<script>
72+
import { mount } from ${JSON.stringify(client_main)}; mount();
73+
let foo;
74+
</script>
75+
76+
<svelte:head><title>hi</title></svelte:head>
77+
78+
<a href={foo} class={foo}>a</a>
79+
<a {...foo}>a</a>
80+
<svelte:component this={foo} />
81+
<svelte:element this={foo} />
82+
<C {foo} />
83+
84+
{#if foo}
85+
{/if}
86+
{#each foo as bar}
87+
{/each}
88+
{#await foo}
89+
{/await}
90+
{#key foo}
91+
{/key}
92+
{#snippet x()}
93+
{/snippet}
94+
95+
{@render x()}
96+
{@html foo}
97+
`,
98+
{ filename: 'App.svelte' }
99+
).js.code
100+
);
101+
if (!without_hydration.includes('current_hydration_fragment')) {
102+
// eslint-disable-next-line no-console
103+
console.error(`✅ Hydration code treeshakeable`);
104+
} else {
105+
// eslint-disable-next-line no-console
106+
console.error(without_hydration);
107+
// eslint-disable-next-line no-console
108+
console.error(`❌ Hydration code not treeshakeable`);
109+
failed = true;
110+
}
111+
62112
// eslint-disable-next-line no-console
63113
console.groupEnd();
64114

packages/svelte/src/internal/client/each.js

+20-16
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
current_hydration_fragment,
1212
get_hydration_fragment,
1313
hydrate_block_anchor,
14+
hydrating,
1415
set_current_hydration_fragment
1516
} from './hydration.js';
1617
import { clear_text_content, empty, map_get, map_set } from './operations.js';
@@ -61,7 +62,10 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
6162
/** @type {null | import('./types.js').EffectSignal} */
6263
let render = null;
6364

64-
/** Whether or not there was a "rendered fallback but want to render items" (or vice versa) hydration mismatch */
65+
/**
66+
* Whether or not there was a "rendered fallback but want to render items" (or vice versa) hydration mismatch.
67+
* Needs to be a `let` or else it isn't treeshaken out
68+
*/
6569
let mismatch = false;
6670

6771
block.r =
@@ -107,7 +111,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
107111
// If the each block is controlled, then the anchor node will be the surrounding
108112
// element in which the each block is rendered, which requires certain handling
109113
// depending on whether we're in hydration mode or not
110-
if (current_hydration_fragment === null) {
114+
if (!hydrating) {
111115
// Create a new anchor on the fly because there's none due to the optimization
112116
anchor = empty();
113117
block.a.appendChild(anchor);
@@ -153,13 +157,13 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
153157

154158
const length = array.length;
155159

156-
if (current_hydration_fragment !== null) {
160+
if (hydrating) {
157161
const is_each_else_comment =
158162
/** @type {Comment} */ (current_hydration_fragment?.[0])?.data === 'ssr:each_else';
159163
// Check for hydration mismatch which can happen if the server renders the each fallback
160164
// but the client has items, or vice versa. If so, remove everything inside the anchor and start fresh.
161165
if ((is_each_else_comment && length) || (!is_each_else_comment && !length)) {
162-
remove(/** @type {import('./types.js').TemplateNode[]} */ (current_hydration_fragment));
166+
remove(current_hydration_fragment);
163167
set_current_hydration_fragment(null);
164168
mismatch = true;
165169
} else if (is_each_else_comment) {
@@ -306,22 +310,22 @@ function reconcile_indexed_array(
306310
}
307311
} else {
308312
var item;
309-
var is_hydrating = current_hydration_fragment !== null;
313+
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
314+
let mismatch = false;
310315
b_blocks = Array(b);
311-
if (is_hydrating) {
316+
if (hydrating) {
312317
// Hydrate block
313318
var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ (
314319
current_hydration_fragment
315320
);
316321
var hydrating_node = hydration_list[0];
317322
for (; index < length; index++) {
318-
var fragment = /** @type {Array<Text | Comment | Element>} */ (
319-
get_hydration_fragment(hydrating_node)
320-
);
323+
var fragment = get_hydration_fragment(hydrating_node);
321324
set_current_hydration_fragment(fragment);
322325
if (!fragment) {
323326
// If fragment is null, then that means that the server rendered less items than what
324327
// the client code specifies -> break out and continue with client-side node creation
328+
mismatch = true;
325329
break;
326330
}
327331

@@ -357,7 +361,7 @@ function reconcile_indexed_array(
357361
}
358362
}
359363

360-
if (is_hydrating && current_hydration_fragment === null) {
364+
if (mismatch) {
361365
// Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode
362366
set_current_hydration_fragment([]);
363367
}
@@ -425,23 +429,23 @@ function reconcile_tracked_array(
425429
var key;
426430
var item;
427431
var idx;
428-
var is_hydrating = current_hydration_fragment !== null;
432+
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
433+
let mismatch = false;
429434
b_blocks = Array(b);
430-
if (is_hydrating) {
435+
if (hydrating) {
431436
// Hydrate block
432437
var fragment;
433438
var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ (
434439
current_hydration_fragment
435440
);
436441
var hydrating_node = hydration_list[0];
437442
while (b > 0) {
438-
fragment = /** @type {Array<Text | Comment | Element>} */ (
439-
get_hydration_fragment(hydrating_node)
440-
);
443+
fragment = get_hydration_fragment(hydrating_node);
441444
set_current_hydration_fragment(fragment);
442445
if (!fragment) {
443446
// If fragment is null, then that means that the server rendered less items than what
444447
// the client code specifies -> break out and continue with client-side node creation
448+
mismatch = true;
445449
break;
446450
}
447451

@@ -594,7 +598,7 @@ function reconcile_tracked_array(
594598
}
595599
}
596600

597-
if (is_hydrating && current_hydration_fragment === null) {
601+
if (mismatch) {
598602
// Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode
599603
set_current_hydration_fragment([]);
600604
}

packages/svelte/src/internal/client/hydration.js

+22-9
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,37 @@
33
import { empty } from './operations.js';
44
import { schedule_task } from './runtime.js';
55

6-
/** @type {null | Array<import('./types.js').TemplateNode>} */
7-
export let current_hydration_fragment = null;
6+
/**
7+
* Use this variable to guard everything related to hydration code so it can be treeshaken out
8+
* if the user doesn't use the `hydrate` method and these code paths are therefore not needed.
9+
*/
10+
export let hydrating = false;
11+
12+
/**
13+
* Array of nodes to traverse for hydration. This will be null if we're not hydrating, but for
14+
* the sake of simplicity we're not going to use `null` checks everywhere and instead rely on
15+
* the `hydrating` flag to tell whether or not we're in hydration mode at which point this is set.
16+
* @type {import('./types.js').TemplateNode[]}
17+
*/
18+
export let current_hydration_fragment = /** @type {any} */ (null);
819

920
/**
10-
* @param {null | Array<import('./types.js').TemplateNode>} fragment
21+
* @param {null | import('./types.js').TemplateNode[]} fragment
1122
* @returns {void}
1223
*/
1324
export function set_current_hydration_fragment(fragment) {
14-
current_hydration_fragment = fragment;
25+
hydrating = fragment !== null;
26+
current_hydration_fragment = /** @type {import('./types.js').TemplateNode[]} */ (fragment);
1527
}
1628

1729
/**
1830
* Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered.
1931
* @param {Node | null} node
2032
* @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty
21-
* @returns {Array<import('./types.js').TemplateNode> | null}
33+
* @returns {import('./types.js').TemplateNode[] | null}
2234
*/
2335
export function get_hydration_fragment(node, insert_text = false) {
24-
/** @type {Array<import('./types.js').TemplateNode>} */
36+
/** @type {import('./types.js').TemplateNode[]} */
2537
const fragment = [];
2638

2739
/** @type {null | Node} */
@@ -66,9 +78,10 @@ export function get_hydration_fragment(node, insert_text = false) {
6678
* @returns {void}
6779
*/
6880
export function hydrate_block_anchor(anchor_node, is_controlled) {
69-
/** @type {Node} */
70-
let target_node = anchor_node;
71-
if (current_hydration_fragment !== null) {
81+
if (hydrating) {
82+
/** @type {Node} */
83+
let target_node = anchor_node;
84+
7285
if (is_controlled) {
7386
target_node = /** @type {Node} */ (target_node.firstChild);
7487
}

packages/svelte/src/internal/client/operations.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { current_hydration_fragment, get_hydration_fragment } from './hydration.js';
1+
import { current_hydration_fragment, get_hydration_fragment, hydrating } from './hydration.js';
22
import { get_descriptor } from './utils.js';
33

44
// We cache the Node and Element prototype methods, so that we can avoid doing
@@ -171,7 +171,7 @@ export function empty() {
171171
/*#__NO_SIDE_EFFECTS__*/
172172
export function child(node) {
173173
const child = first_child_get.call(node);
174-
if (current_hydration_fragment !== null) {
174+
if (hydrating) {
175175
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
176176
if (child === null) {
177177
const text = empty();
@@ -192,7 +192,7 @@ export function child(node) {
192192
*/
193193
/*#__NO_SIDE_EFFECTS__*/
194194
export function child_frag(node, is_text) {
195-
if (current_hydration_fragment !== null) {
195+
if (hydrating) {
196196
const first_node = /** @type {Node[]} */ (node)[0];
197197

198198
// if an {expression} is empty during SSR, there might be no
@@ -225,7 +225,7 @@ export function child_frag(node, is_text) {
225225
/*#__NO_SIDE_EFFECTS__*/
226226
export function sibling(node, is_text = false) {
227227
const next_sibling = next_sibling_get.call(node);
228-
if (current_hydration_fragment !== null) {
228+
if (hydrating) {
229229
// if a sibling {expression} is empty during SSR, there might be no
230230
// text node to hydrate — we must therefore create one
231231
if (is_text && next_sibling?.nodeType !== 3) {
@@ -276,14 +276,15 @@ export function create_element(name) {
276276
}
277277

278278
/**
279+
* Expects to only be called in hydration mode
279280
* @param {Node} node
280281
* @returns {Node}
281282
*/
282283
function capture_fragment_from_node(node) {
283284
if (
284285
node.nodeType === 8 &&
285286
/** @type {Comment} */ (node).data.startsWith('ssr:') &&
286-
/** @type {Array<Element | Text | Comment>} */ (current_hydration_fragment).at(-1) !== node
287+
current_hydration_fragment.at(-1) !== node
287288
) {
288289
const fragment = /** @type {Array<Element | Text | Comment>} */ (get_hydration_fragment(node));
289290
const last_child = fragment.at(-1) || node;

packages/svelte/src/internal/client/reconciler.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { append_child } from './operations.js';
2-
import { current_hydration_fragment, hydrate_block_anchor } from './hydration.js';
2+
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from './hydration.js';
33
import { is_array } from './utils.js';
44

55
/** @param {string} html */
@@ -92,7 +92,7 @@ export function remove(current) {
9292
*/
9393
export function reconcile_html(target, value, svg) {
9494
hydrate_block_anchor(target);
95-
if (current_hydration_fragment !== null) {
95+
if (hydrating) {
9696
return current_hydration_fragment;
9797
}
9898
var html = value + '';

0 commit comments

Comments
 (0)