Реализация Islands Architecture для веб-приложения
Islands Architecture — архитектурный паттерн, где страница состоит из статичного HTML-океана с изолированными «островами» интерактивности. Каждый остров — независимый компонент с собственным JS, который гидратируется отдельно, не зная о соседних островах.
Концепцию описал Джейсон Миллер (Preact) в 2020 году. Сегодня это основа Astro, Marko, и часть подхода в Qwik.
Архитектурная модель
Статичный HTML (сервер, нулевой JS):
┌─────────────────────────────────────┐
│ Header │ ← HTML
│ Logo | Nav links | ... │
├─────────────────────────────────────┤
│ Hero section │ ← HTML
│ H1, изображение, CTA │
├──────────────┬──────────────────────┤
│ Article text │ 🏝️ Island: │ ← JS только для острова
│ (HTML) │ TableOfContents.tsx │
│ │ (sticky, highlight) │
├──────────────┴──────────────────────┤
│ 🏝️ Island: CommentSection.tsx │ ← JS только для острова
│ (React, загружается при скролле) │
├─────────────────────────────────────┤
│ Footer │ ← HTML
└─────────────────────────────────────┘
Итог: JS грузится только для 2 островов, не для всей страницы
Реализация в Astro
Astro — первый фреймворк с нативной поддержкой Islands:
---
// src/pages/blog/[slug].astro
import type { GetStaticPaths } from 'astro';
import { getCollection } from 'astro:content';
// Серверные компоненты — .astro файлы без JS на клиенте
import BaseLayout from '@/layouts/BaseLayout.astro';
import ArticleHero from '@/components/ArticleHero.astro';
import Prose from '@/components/Prose.astro';
// Острова — React/Vue/Svelte компоненты с директивами
import TableOfContents from '@/islands/TableOfContents.tsx';
import CommentSection from '@/islands/CommentSection.tsx';
import ShareButtons from '@/islands/ShareButtons.svelte';
import NewsletterSignup from '@/islands/NewsletterSignup.vue';
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await getCollection('blog', p => !p.data.draft);
return posts.map(post => ({ params: { slug: post.slug }, props: { post } }));
};
const { post } = Astro.props;
const { Content, headings } = await post.render();
---
<BaseLayout title={post.data.title} description={post.data.description}>
<ArticleHero post={post} />
<div class="article-layout">
<!-- Остров 1: интерактивное оглавление -->
<!-- Загружается только когда браузер простаивает -->
<TableOfContents headings={headings} client:idle />
<article>
<Prose>
<Content />
</Prose>
</article>
</div>
<!-- Остров 2: кнопки шаринга -->
<!-- Загружается только при появлении в viewport -->
<ShareButtons
url={Astro.url.href}
title={post.data.title}
client:visible
/>
<!-- Остров 3: комментарии -->
<!-- Загружается только при появлении в viewport, задержка 500ms -->
<CommentSection
articleId={post.id}
client:visible={{ rootMargin: '0px 0px 200px 0px' }}
/>
<!-- Остров 4: подписка на рассылку -->
<!-- Загружается немедленно — в fold -->
<NewsletterSignup client:load />
</BaseLayout>
Острова с межостровной коммуникацией
Острова изолированы — у них нет общего контекста React. Для коммуникации используются:
Nano Stores (рекомендуется для Astro):
// src/stores/cart.ts
import { atom, computed } from 'nanostores';
import { persistentAtom } from '@nanostores/persistent';
export const cartItems = persistentAtom<CartItem[]>('cart', [], {
encode: JSON.stringify,
decode: JSON.parse,
});
export const cartCount = computed(cartItems, items => items.length);
export const cartTotal = computed(cartItems, items =>
items.reduce((sum, item) => sum + item.price * item.qty, 0)
);
export function addToCart(product: Product) {
const items = cartItems.get();
const existing = items.find(i => i.id === product.id);
if (existing) {
cartItems.set(items.map(i => i.id === product.id ? { ...i, qty: i.qty + 1 } : i));
} else {
cartItems.set([...items, { ...product, qty: 1 }]);
}
}
// islands/CartIcon.tsx — React остров
import { useStore } from '@nanostores/react';
import { cartCount } from '@/stores/cart';
export function CartIcon() {
const count = useStore(cartCount);
return (
<a href="/cart" className="relative">
<ShoppingCartIcon />
{count > 0 && <span className="badge">{count}</span>}
</a>
);
}
<!-- islands/AddToCartButton.svelte — Svelte остров -->
<script>
import { addToCart } from '@/stores/cart';
export let product;
let loading = false;
async function handleAdd() {
loading = true;
addToCart(product);
loading = false;
}
</script>
<button on:click={handleAdd} disabled={loading}>
{loading ? 'Добавляем...' : 'В корзину'}
</button>
Оба острова (React и Svelte) работают с одним хранилищем. Изменение в одном — немедленно отражается в другом.
Нативные браузерные события:
// Универсальная шина событий через CustomEvent
export function emit<T>(event: string, detail: T) {
window.dispatchEvent(new CustomEvent(event, { detail, bubbles: true }));
}
export function on<T>(event: string, handler: (detail: T) => void) {
const wrapped = (e: CustomEvent<T>) => handler(e.detail);
window.addEventListener(event, wrapped as EventListener);
return () => window.removeEventListener(event, wrapped as EventListener);
}
// В острове 1
emit('product:added', { id: product.id, name: product.name });
// В острове 2
useEffect(() => {
return on<{ id: string; name: string }>('product:added', ({ name }) => {
showToast(`${name} добавлен в корзину`);
});
}, []);
Рендеринг островов на нескольких фреймворках
Astro поддерживает несколько рендерер-плагинов одновременно:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import svelte from '@astrojs/svelte';
import preact from '@astrojs/preact';
export default defineConfig({
integrations: [
react(), // Для существующих React-компонентов
vue(), // Для Vue-компонентов из библиотеки клиента
svelte(), // Для лёгких интерактивных виджетов
preact({ compat: true }), // Preact как замена React для лёгких островов
],
});
На практике: разные фреймворки для разных целей в рамках одного сайта без конфликтов.
Производительность: цифры
Измерения на реальных проектах после миграции на Islands Architecture:
| Метрика | До (Full React SSR) | После (Astro Islands) |
|---|---|---|
| Initial JS | 180–250 KB | 15–40 KB |
| TTI | 3.2s | 0.8s |
| TBT | 480ms | 60ms |
| Lighthouse Performance | 62 | 96 |
Разброс зависит от количества и сложности островов.
Ограничения паттерна
Islands Architecture не подходит для:
- SPA-приложений — там нужно единое состояние между многими компонентами
- Дашбордов — слишком много интерактивности, острова теряют изоляцию
- Страниц, где всё интерактивно — выигрыш минимален
Идеальный случай: контентный сайт с несколькими интерактивными блоками на странице.
Сроки реализации
- Неделя 1–2: аудит существующего сайта, выявление интерактивных компонентов, настройка Astro + рендерер-плагины
- Неделя 3: перенос статичных страниц, разбивка на острова с директивами гидратации
- Неделя 4: межостровная коммуникация через nano stores, тестирование изоляции
- Неделя 5: измерение Core Web Vitals, сравнение с baseline, оптимизация директив
- Неделя 6: деплой (Cloudflare Pages / Netlify), Lighthouse CI, финальная документация







