Astro Islands: Wann man NICHT hydratisieren sollte
LaunchFast LogoLaunchFast
Blog
1.976 Wörter10 Min. Lesezeit

Astro Islands: Wann man NICHT hydratisieren sollte

Hydration ist teuer. Ein praktischer Blick auf Astro Islands: die günstigeren Optionen, die man zuerst ausschöpfen sollte (statisches HTML, Daten zur Build-Zeit, Server-Inseln, Progressive Enhancement), und die Fälle, in denen Hydration sinnvoll ist.

Rishi Raj Jain
Rishi Raj JainAutor
Astro Islands: Wann man NICHT hydratisieren sollte

Wenn Entwickler von React oder Next.js zu Astro wechseln, greifen sie beim ersten interaktiven Element, das sie bauen, zu einer Framework-Insel. Aus einem Dropdown wird eine React-Komponente mit client:load. Der Grund, nicht immer Inseln zu verwenden, ist, dass Hydration teuer ist. Dieser Beitrag erklärt, warum, und gibt Ihnen eine Reihe von Optionen an die Hand, die Sie durchgehen sollten, bevor Sie eine Komponente an den Browser übergeben.

Voraussetzungen

Sie benötigen Folgendes:

  • Grundkenntnisse in Astro-Komponenten
  • Node.js 22 oder neuer

Warum Hydration teuer ist

Eine Client-Direktive weist Astro an, eine UI-Framework-Komponente an den Browser zu senden und sie dort zu hydratisieren, und:

  • Ihre Komponente wird zweimal ausgeführt: Der Server hat das HTML bereits gerendert. Bei der Hydration werden die Framework-Laufzeit und die Komponente heruntergeladen, und dann wird die Komponente im Browser erneut ausgeführt, um ihren Zustand wiederherzustellen und Event-Listener an bereits vorhandenes Markup anzuhängen.
  • Sie blockiert den Haupt-Thread: Hydration ist synchrone Arbeit, die mit den ersten Klicks und Tippgesten des Nutzers konkurriert. Das ist die Arbeit, die im Feld als Interaction to Next Paint (INP) sichtbar wird.
  • Sie skaliert mit dem Komponentenbaum, nicht mit der Interaktivität: Eine große Insel mit einem einzigen Button hydratisiert trotzdem den gesamten Baum.

Nun denken Sie vielleicht an client:idle und client:visible, aber diese verschieben nur die Laufzeit und das zweite Rendern. Sie beseitigen die teuren Operationen nicht, die Hydration mit sich bringt.

Komponenten sind keine Inseln

Eine häufige Verwechslung besteht darin, jede Komponente als Insel zu behandeln. Eine Seite in <Header />, <Posts /> und <Footer /> aufzuteilen, ist Komponentisierung. Das sind .astro-Dateien, sie rendern auf dem Server zu HTML und tragen kein JavaScript.

Eine Insel ist etwas Engeres: eine UI-Framework-Komponente (React, Vue, Svelte und so weiter), die eine client:*-Direktive trägt. Eine .astro-Komponente kann keine Client-Direktive annehmen, da sie ohnehin nie hydratisiert wird.

Eine Reihenfolge der Optionen, bevor Sie hydratisieren

Bevor Sie eine Client-Direktive hinzufügen, arbeiten Sie diese Liste durch:

  1. Statisches HTML: Kann dies auf dem Server gerendert werden und sich pro Nutzer nie ändern?
  2. Zur Build-Zeit aufgelöste Daten: Muss der Wert nur zum Zeitpunkt des letzten Deploys aktuell sein? Rufen Sie ihn während des Builds ab und backen Sie ihn in das HTML.
  3. Eine Server-Insel: Muss der Wert pro Anfrage frisch sein, etwa ein Warenkorb-Zähler oder ein angemeldeter Name? Rendern Sie ihn auf dem Server mit server:defer und halten Sie den Rest der Seite cachefähig.
  4. Ein Skript mit Progressive Enhancement: Handelt es sich um ein einmaliges DOM-Verhalten wie einen Umschalter, einen Dialog oder einen Tab-Wechsel? Rendern Sie das funktionierende Markup auf dem Server und hängen Sie ein kleines Skript an.
  5. Eine hydratisierte Insel: Benötigt dies einen fortlaufenden clientseitigen Zustand, von dem viele Teile der UI abgeleitet werden und auf den sie reagieren? Jetzt ist ein Framework unbedingt erforderlich, und Sie sollten die kleinste Komponente hydratisieren, die diesen Zustand hält.

Sehen wir uns einige Ansätze aus der Liste im Detail an.

Zur Build-Zeit aufgelöste Daten

Wenn ein Wert nur zum Zeitpunkt Ihres letzten Deploys aktuell sein muss, lösen Sie ihn während des Builds auf. Ein Aufrufzähler pro Beitrag ist der klassische Fall, für den Leute eine Insel bauen.

Der Insel-Weg: Eine Framework-Komponente, die die Zahl im Browser abruft, nachdem sie hydratisiert wurde.

src/pages/[slug].astro
---
import ViewCount from '../components/ViewCount.jsx'
const { slug } = Astro.params
---
<ViewCount slug={slug} client:load />
src/components/ViewCount.jsx
import { useEffect, useState } from 'react'
export default function ViewCount({ slug }) {
const [views, setViews] = useState(null)
useEffect(() => {
fetch(`/api/views?slug=${slug}`)
.then((res) => res.json())
.then((data) => setViews(data.views))
}, [slug])
return <span>{views === null ? 'Loading...' : `${views} views`}</span>
}

Ja, wir sind alle daran gewöhnt, dafür React zu verwenden, und es ist vielleicht die einfachere Option, auf die man zurückgreift. Aber ergibt es für eine ganze Website, die nur wenige dynamische Komponenten hat, überhaupt Sinn, die gesamte React-Laufzeit und die Komponente zu laden, beim Laden zu hydratisieren und dann aus dem Browser jedes Besuchers eine Anfrage zu stellen, um die Aufrufe abzurufen?

Der Build-Zeit-Weg: Rufen Sie den Wert während des Builds ab und backen Sie die Zahl in das HTML.

src/components/ViewCount.astro
---
// Wird zur Build-Zeit für eine statische Seite ausgeführt. Die Zahl ist Teil des HTML.
import { getViews } from '../lib/analytics'
const { slug } = Astro.props
const views = await getViews(slug)
---
<span>{views} views</span>

Sie können dies nach einem Zeitplan neu bauen, etwa über einen täglichen Deploy-Hook, und die Zählung bleibt für die meisten Inhalte aktuell genug.

Was sich zwischen den beiden ändert:

  • JavaScript: die React-Laufzeit plus die Komponente vs. keines.
  • Netzwerk beim Aufruf: eine Anfrage von jedem Besucher vs. keine.
  • Erstes Rendern (First Paint): ein Ladezustand, der spät aufgelöst wird vs. die Zahl bereits im HTML.

Das Einzige, was Sie aufgeben, ist die Aktualität. Die Build-Zeit-Zahl ist nur so aktuell wie Ihr letzter Build. Für einen Aufrufzähler ist das in Ordnung, und Sie umgehen das Framework vollständig.

Eine Server-Insel für Daten pro Anfrage

Manchmal muss der Wert bei jeder Anfrage korrekt sein, etwa ein Warenkorb-Zähler oder ein angemeldeter Name. Build-Zeit-Backen hilft hier nicht, also ist der Reflex eine Client-Komponente, die beim Mounten abruft, was Sie direkt wieder dazu zwingt, eine Laufzeit und einen Ladezustand auszuliefern.

Eine Server-Insel liefert Ihnen frische Daten ohne all das. Sie rendern die Komponente auf dem Server, verzögert, sodass die statische Seite darum herum cachefähig bleibt.

src/components/CartCount.astro
---
import { getCartCount } from '../lib/cart'
const count = await getCartCount(Astro.request)
---
<span class="cart-count">{count}</span>
src/pages/index.astro
---
import CartCount from '../components/CartCount.astro'
---
<CartCount server:defer>
<span slot="fallback">0</span>
</CartCount>

Astro rendert zuerst die statische Hülle, zeigt den Fallback und füllt dann die Insel vom Server. Die Hülle kann aggressiv gecacht werden, der Zähler ist pro Anfrage korrekt, und der Browser erhält HTML ohne angehängte Framework-Laufzeit. Im Vergleich zum Client-Abruf behalten Sie die frischen Daten und lassen das JavaScript weg.

Progressive Enhancement mit einem Skript

Für ein einmaliges DOM-Verhalten wie einen Umschalter oder einen Tab-Wechsel benötigen Sie nicht die Reaktivität eines Frameworks. Sie benötigen einen Event-Listener. Astro verarbeitet <script>-Tags für Sie, bündelt sie und fügt sie als Module hinzu, sodass ein Skript im Browser laufen kann, ohne React oder Svelte hereinzuziehen.

Der Insel-Weg: Tabs als React-Komponente, beim Laden hydratisiert.

src/components/Tabs.jsx
import { useState } from 'react'
const tabs = ['Overview', 'Pricing', 'FAQ']
export default function Tabs() {
const [active, setActive] = useState(0)
return (
<div>
<div role="tablist">
{tabs.map((label, index) => (
<button
key={label}
role="tab"
aria-selected={index === active}
onClick={() => setActive(index)}
>
{label}
</button>
))}
</div>
{tabs.map((label, index) => (
<div key={label} role="tabpanel" hidden={index !== active}>
<p>{label} content</p>
</div>
))}
</div>
)
}

Mit <Tabs client:load /> verwendet, liefert dies die React-Laufzeit und die Komponente aus und hydratisiert dann das Ganze, um einen einzigen Zustand zu verwalten: welcher Tab offen ist.

Der Skript-Weg: Dieselben Tabs als serverseitig gerendertes HTML mit einem angehängten Skript.

src/components/Tabs.astro
---
const tabs = ['Overview', 'Pricing', 'FAQ']
---
<div data-tabs>
<div role="tablist">
{
tabs.map((label, index) => (
<button
role="tab"
id={`tab-${index}`}
aria-controls={`panel-${index}`}
aria-selected={index === 0 ? 'true' : 'false'}
>
{label}
</button>
))
}
</div>
{
tabs.map((label, index) => (
<div
role="tabpanel"
id={`panel-${index}`}
aria-labelledby={`tab-${index}`}
hidden={index !== 0}
>
<p>{label} content</p>
</div>
))
}
</div>
<script>
function setupTabs() {
document.querySelectorAll('[data-tabs]').forEach((root) => {
if (root.dataset.ready) return
root.dataset.ready = 'true'
const buttons = root.querySelectorAll('[role="tab"]')
buttons.forEach((button) => {
button.addEventListener('click', () => {
buttons.forEach((other) => {
const selected = other === button
other.setAttribute('aria-selected', String(selected))
const panelId = other.getAttribute('aria-controls')
const panel = panelId ? document.getElementById(panelId) : null
if (panel) panel.hidden = !selected
})
})
})
})
}
setupTabs()
document.addEventListener('astro:page-load', setupTabs)
</script>

Was sich zwischen den beiden ändert:

  • JavaScript: die React-Laufzeit plus die Komponente vs. ein paar Zeilen gebündeltes Skript.
  • Zweites Rendern: die Insel führt die Komponente im Browser erneut aus, um onClick anzuhängen vs. das Skript hängt Listener an das Markup an.
  • Bevor JavaScript lädt: der erste Tab ist sichtbar und die ARIA-Rollen sind in beiden Fällen gesetzt, aber die Skript-Version funktioniert weiterhin als einfacher Inhalt, wenn das Skript fehlschlägt.

Da Astros gebündelte Modul-Skripte nur einmal ausgeführt werden, wird ein Skript auf oberster Ebene beim clientseitigen Routing (dem <ClientRouter />) nach der Navigation nicht erneut ausgeführt, und seine Listener funktionieren nicht mehr. Die Bindung an astro:page-load führt die Einrichtung bei jeder Navigation erneut aus, und der data-ready-Schutz verhindert, dass dasselbe Element zweimal gebunden wird. Dieses Muster deckt das meiste ab, wofür Leute Inseln bauen: Akkordeons, Dropdowns, Dialoge, Kopier-Buttons und Formulare.

Wann Hydration die richtige Wahl ist

Inseln zu überspringen sollte die Standardeinstellung sein, aber wenn die Interaktion einen echten clientseitigen Zustand hat, von dem viele Teile der UI abgeleitet werden und auf den sie im Laufe der Zeit reagieren, sollten Sie Inseln verwenden:

  • Ein Formular, bei dem Felder voneinander abhängen und sich die UI aktualisiert, während der Nutzer tippt.
  • Eine Ansicht, die Aktualisierungen über einen WebSocket erhält und häufig neu rendert.
  • Eine Canvas-, Drag-and-Drop- oder Diagrammfläche mit umfangreichem internem Zustand.
  • Ein bestehendes React- oder Vue-Widget eines Drittanbieters, das Sie lieber einbinden als neu schreiben möchten.

Wenn Sie hydratisieren, tun Sie es mit:

  • Hydratisieren Sie das Blatt, nicht den Baum: Setzen Sie die Direktive auf die kleinste Komponente, die den Zustand hält. Behalten Sie die statischen Teile darum herum als .astro.
  • Verzögern Sie, wenn Sie können: client:visible und client:idle verschieben die Kosten für alles unterhalb des sichtbaren Bereichs hinter das erste Rendern.
  • Achten Sie darauf, was Sie als Props übergeben: Astro serialisiert Insel-Props in das HTML, damit der Client mit denselben Werten rehydratisieren kann. Das Übergeben eines großen Objekts oder Arrays bläht die Seite für jeden Besucher auf. Übergeben Sie IDs und Primitive und rufen Sie den Rest innerhalb der Insel oder einer Server-Insel ab.
  • Vermeiden Sie client:only für Inhalte: Es überspringt das serverseitige Rendern vollständig, sodass die Komponente im initialen HTML leer ist. Das ist ein Problem für alles, was Sie indexiert haben möchten.

Fazit

Astro liefert standardmäßig kein JavaScript aus, und Hydration ist der Punkt, an dem Sie sich wieder für die Kosten eines Frameworks im Browser entscheiden können. Das ist teuer: zweites Rendern, Haupt-Thread-Zeit und eine Nutzlast, die mit der Größe der Komponente wächst.

Wenn Sie Fragen oder Anmerkungen haben, können Sie mich gerne auf Twitter kontaktieren.

Weiterlesen