Hydratation
L'hydratation est le processus qui rend interactif le HTML rendu côté serveur. L'hydratation EmberKit est légère et ciblée — au lieu de re-rendre tout l'arbre de composants, elle connecte les signals à des éléments DOM précis via des attributs déclaratifs data-ek-bind.
Fonctionnement
-
Le SSR rend le HTML complet — Tous les composants sont rendus en HTML sur le serveur. Aucun JavaScript n'est nécessaire pour voir la page.
-
Le navigateur affiche le HTML immédiatement — First Contentful Paint rapide. La page est entièrement visible et utilisable.
-
L'hydratation scanne les bindings — Un petit script (
~1KB) s'exécute après le chargement. Il trouve les éléments avecdata-ek-bindet connecte les signals aux nœuds DOM viasignal.subscribe(). -
Mises à jour ciblées — Quand un signal change, seul l'élément DOM lié est mis à jour. Pas de virtual DOM, pas de diffing, pas de re-render.
// Static: pure HTML, zero JS
function Footer() {
return <footer>© 2025 EmberKit</footer>;
}
// Interactive: signal-driven DOM binding
function Counter() {
const [count, setCount] = createSignal(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+</button>
<span data-ek-bind={count}>{count()}</span>
<button onClick={() => setCount(c => c - 1)}>-</button>
</div>
);
}
Référence data-ek-bind
data-ek-bind
Connecte un getter de signal directement à un élément DOM. Le framework sérialise l'identité du signal pendant le SSR et la reconnecte à l'hydratation.
| Motif | Comportement |
|---|---|
<span data-ek-bind={count}>{count()}</span> | Met à jour textContent quand le signal change |
<div data-ek-bind={open} data-ek-show="opacity-100" data-ek-hide="opacity-0"> | Ajoute/retire des classes CSS selon la vérité |
<div data-ek-bind={tab} data-ek-show-when="preview"> | Bascule la classe hidden quand le signal égale une chaîne précise |
Liaison textContent (par défaut)
Aucun attribut supplémentaire — le texte de l'élément reste synchronisé :
const [name, setName] = createSignal('Alice');
return <p data-ek-bind={name}>{name()}</p>;
// When setName('Bob'), the <p> textContent updates.
Liaison bascule de classes
Utilisez data-ek-show / data-ek-hide pour basculer des jeux de classes CSS selon un signal booléen :
const [open, setOpen] = createSignal(false);
return (
<div data-ek-bind={open} data-ek-show="opacity-100" data-ek-hide="opacity-0 pointer-events-none" class="opacity-0 pointer-events-none">
Panel content
</div>
);
Liaison par correspondance de chaîne
Utilisez data-ek-show-when pour un basculement type onglets :
const [tab, setTab] = createSignal('preview');
return (
<>
<div data-ek-bind={tab} data-ek-show-when="preview" class="p-4">Preview panel</div>
<div data-ek-bind={tab} data-ek-show-when="code" class="p-4 hidden">Code panel</div>
</>
);
Personnalisez la classe de masquage avec data-ek-hide-class (par défaut "hidden").
Composants acceptant des signals
Les composants interactifs détectent quand une prop est un signal (a __idx / subscribe) et appliquent les bindings automatiquement. Le code consommateur reste propre :
const [open, setOpen] = createSignal(false);
// Pass the signal — Modal handles data-ek-bind internally
<Modal open={open} onClose={() => setOpen(false)} />
// Or pass a plain boolean — SSR only, no hydration
<Modal open={true} />
Ce qui s'hydrate
Seuls les éléments avec data-ek-bind ou gestionnaires d'événements reçoivent du JavaScript. Les click handlers sont sérialisés en attributs data-ekclick et câblés au chargement. Tout le reste reste du HTML pur.
function App() {
return (
<div>
{/* Zero JS — pure HTML */}
<header>
<h1>My App</h1>
<nav>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</header>
{/* Gets hydration — signal binding on value */}
<span data-ek-bind={count}>{count()}</span>
{/* Zero JS — pure content */}
<article>
<p>This content never needs JavaScript.</p>
</article>
</div>
);
}
Chargement différé viewport (LazyInView)
Différer le rendu d'UI lourde jusqu'à proximité du viewport. Le site docs utilise ceci pour les sections homepage sous le hero, la grille d'icônes et les grosses démos UI.
Différent du lazy loading d'images
LazyInView retarde le HTML de composants et le travail client d'une section. loading="lazy" sur images ne diffère que les requêtes image — voir Optimisation d'images.
Fonctionnement
- SSR (par défaut
ssr="lazy") — Le serveur envoie un élément hôte léger avec votrefallback(placeholder), pas les children complets. - Navigation client / hydratation — Après chaque
render(), EmberKit exécutehydrateLazyInView()sur la racine app. - IntersectionObserver — Quand l'hôte entre dans le viewport (plus
rootMargin), les children sont rendus dans l'hôte ethydrateSubtree()câble click handlers etdata-ek-binddans cette section.
Flux : chargement page → fallback visible → l'utilisateur scroll près de la section → LazyInView monte les children → hydrateSubtree câble cet hôte.
Usage de base
import { LazyInView } from '@emberkit/core';
function HomePage() {
return (
<div>
<Hero />
<LazyInView
minHeight="24rem"
rootMargin="200px"
fallback={
<div aria-hidden="true" className="min-h-96 animate-pulse rounded-2xl border border-white/5 bg-white/[0.02]" />
}
>
<FeaturesSection />
</LazyInView>
</div>
);
}
Utilisez minHeight (ou fallback à hauteur fixe) pour éviter un saut de layout à l'apparition du contenu.
Props
| Prop | Type | Défaut | Description |
|---|---|---|---|
children | JSXNode ou () => JSXNode | — | Contenu à rendre quand visible |
fallback | JSXNode | null | Placeholder avant chargement |
rootMargin | string | '200px' | Passé à IntersectionObserver (préchargement avant visible) |
minHeight | string | number | — | Espace réservé sur l'élément hôte |
once | boolean | true | Désinscrire le loader après premier montage |
ssr | 'lazy' | 'eager' | 'lazy' | eager = rendre aussi les children côté serveur |
as | string | 'div' | Nom de tag de l'hôte |
className | string | — | Classes sur l'hôte |
Modes SSR
| Mode | Premier HTML | Idéal pour |
|---|---|---|
lazy (défaut) | Fallback uniquement | Pages longues, démos, sections marketing sous le fold |
eager | Children complets | Copy SEO critique devant être dans view source |
Render prop lazy
Passez une fonction quand les children importent de gros modules :
<LazyInView fallback={<SectionSkeleton />}>
{() => {
// Evaluated only when the section enters the viewport
return <HeavyChart data={data} />;
}}
</LazyInView>
Attributs hôte (debug)
Inspectez le DOM en test :
| Attribut | Signification |
|---|---|
data-ek-lazy-in-view | Id registre pour cet hôte |
data-ek-lazy-root-margin | Marge de l'observer |
data-ek-lazy-loaded="true" | Les children ont été montés |
aria-busy="true" | Encore en attente (retiré après chargement) |
Tests locaux
- Démarrez l'app docs :
pnpm devdepuis la racine du repo (oupnpm devdansapps/docs). - Ouvrez
/,/docs/iconsou/docs/ui. - Confirmez les placeholders pulse above the fold où les sections sont lazy.
- Scrollez — le contenu doit remplacer les placeholders ; les hôtes gagnent
data-ek-lazy-loaded. - Optionnel : DevTools → Network → throttle Fast 3G, hard refresh, et comparez first paint vs après scroll.
Tests framework :
pnpm --filter @emberkit/core test -- src/viewport src/hydration
data-hydrate="lazy" (îlots interactifs)
Pour des éléments nécessitant l'hydratation mais pas immédiatement, marquez-les lazy pour que l'analyseur d'hydratation les traite en basse priorité :
<div data-hydrate="lazy" onClick={handleClick}>
Below-the-fold control
</div>
Complète LazyInView : lazy in view diffère le rendu ; data-hydrate="lazy" diffère le câblage des event handlers pour du HTML déjà rendu.
Listes dynamiques après SSR
Hydratation vs listes .map()
Après SSR, EmberKit hydrate le HTML existant. Il ne re-exécute pas JSX .map() quand un signal se met à jour — seuls les nœuds avec data-ek-bind restent synchronisés.
Ce qui pose problème
Flux typique sur une home SSR :
- Serveur rend avec liste vide (pas d'API côté serveur).
- Client précharge
/api/...danssetCache()avant ou aprèsrender(). - Hydratation conserve la grille vide serveur dans le DOM.
- Un signal est mis à jour avec les données fetchées, mais le markup de liste ne change pas.
L'API peut retourner 200 et le cache être plein tandis que l'UI affiche encore une section vide.
Patterns recommandés
1. Forcer un re-render client de la route quand des données async arrivent et le DOM liste est encore vide :
function refreshCurrentRoute() {
const url =
window.location.pathname + window.location.search + window.location.hash;
history.replaceState(null, '', url);
}
createEffect(() => {
void (async () => {
const list = await fetchProjects();
setProjects(list);
const grid = document.getElementById('portfolio-grid');
if (list.length > 0 && (grid?.children.length ?? 0) === 0) {
refreshCurrentRoute();
}
})();
});
Le history.replaceState patché dans render() rappelle renderCurrentRoute() avec hydrate: false, reconstruisant le HTML route depuis JSX.
2. Mises à jour DOM impératives (comme le pattern index blog docs) : rendez des cartes avec innerHTML ou createElement dans createEffect au lieu de compter sur .map() en JSX.
3. Loaders SSR — récupérez les données liste dans un loader de route pour que le HTML serveur inclue les items. Voir SSR et SSG.
4. LazyInView avec ssr="eager" — quand le SEO exige le HTML complet au first paint.
Liaisons input (0.8.0)
data-ek-bind synchronise maintenant les contrôles de formulaire à l'hydratation :
| Élément | Propriété liée |
|---|---|
<input>, <textarea> | .value |
<select> | .value |
<button> (signal booléen) | disabled |
| Autres éléments | textContent (par défaut) |
Impact performance
| Approche | Taille bundle | FCP | TTI |
|---|---|---|---|
| SPA traditionnelle | ~100KB+ | Lent | Lent |
| Hydratation EmberKit | ~1-2KB | Rapide | Rapide |
| Statique seul | 0KB | Instantané | Instantané |
| + LazyInView sous le fold | Même chunk route ; moins de render/hydratation initial | Chargement perçu plus rapide sur pages longues | Sections hydratées au scroll |
Prochaines étapes
- Signals — API signals et subscribe
- SSR — Rendu côté serveur
- Release 0.8.0 — Dev API, view transitions, notes d'hydratation
- Components — Composants statiques vs interactifs
- Référence API —
LazyInView,hydrateLazyInView,clearLazyRegistry