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

  1. 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.

  2. Le navigateur affiche le HTML immédiatement — First Contentful Paint rapide. La page est entièrement visible et utilisable.

  3. L'hydratation scanne les bindings — Un petit script (~1KB) s'exécute après le chargement. Il trouve les éléments avec data-ek-bind et connecte les signals aux nœuds DOM via signal.subscribe().

  4. 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>&copy; 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

MotifComportement
<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.

Fonctionnement

  1. SSR (par défaut ssr="lazy") — Le serveur envoie un élément hôte léger avec votre fallback (placeholder), pas les children complets.
  2. Navigation client / hydratation — Après chaque render(), EmberKit exécute hydrateLazyInView() sur la racine app.
  3. IntersectionObserver — Quand l'hôte entre dans le viewport (plus rootMargin), les children sont rendus dans l'hôte et hydrateSubtree() câble click handlers et data-ek-bind dans 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

PropTypeDéfautDescription
childrenJSXNode ou () => JSXNodeContenu à rendre quand visible
fallbackJSXNodenullPlaceholder avant chargement
rootMarginstring'200px'Passé à IntersectionObserver (préchargement avant visible)
minHeightstring | numberEspace réservé sur l'élément hôte
oncebooleantrueDésinscrire le loader après premier montage
ssr'lazy' | 'eager''lazy'eager = rendre aussi les children côté serveur
asstring'div'Nom de tag de l'hôte
classNamestringClasses sur l'hôte

Modes SSR

ModePremier HTMLIdéal pour
lazy (défaut)Fallback uniquementPages longues, démos, sections marketing sous le fold
eagerChildren completsCopy 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 :

AttributSignification
data-ek-lazy-in-viewId registre pour cet hôte
data-ek-lazy-root-marginMarge 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

  1. Démarrez l'app docs : pnpm dev depuis la racine du repo (ou pnpm dev dans apps/docs).
  2. Ouvrez /, /docs/icons ou /docs/ui.
  3. Confirmez les placeholders pulse above the fold où les sections sont lazy.
  4. Scrollez — le contenu doit remplacer les placeholders ; les hôtes gagnent data-ek-lazy-loaded.
  5. 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

Ce qui pose problème

Flux typique sur une home SSR :

  1. Serveur rend avec liste vide (pas d'API côté serveur).
  2. Client précharge /api/... dans setCache() avant ou après render().
  3. Hydratation conserve la grille vide serveur dans le DOM.
  4. 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émentPropriété liée
<input>, <textarea>.value
<select>.value
<button> (signal booléen)disabled
Autres élémentstextContent (par défaut)

Impact performance

ApprocheTaille bundleFCPTTI
SPA traditionnelle~100KB+LentLent
Hydratation EmberKit~1-2KBRapideRapide
Statique seul0KBInstantanéInstantané
+ LazyInView sous le foldMême chunk route ; moins de render/hydratation initialChargement perçu plus rapide sur pages longuesSections 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 APILazyInView, hydrateLazyInView, clearLazyRegistry