Islandの容量削減
Islandのブラウザ向けReactをRedactやPreactへ置き換えると、配信するJavaScriptの容量を削減できます。静的HTMLの生成にはReactを使い、コンポーネントのimportもそのままにします。
容量削減が必要な場合に追加する設定です。Reactのまま進める場合は、次の静的サイト内検索へ進めます。
Table of Contents
置き換え先を選ぶ
| 置き換え先 | 特徴 | 設定 |
|---|---|---|
| Redact | Reactと同じAPIで、導入しやすい。nanoでさらに削減できる | 公式Viteプラグインを追加 |
| Preact | 容量削減を優先したい場合の候補 | ブラウザ側だけに適用する独自Viteプラグインを用意 |
PreactはRedactより小さな容量を目指せますが、実際の出力は使うAPIや依存ライブラリによって変わります。どちらもReactとは実装が異なるため、使用中のUIで動作を確認して選びます。二つを同時に適用せず、いずれかを利用してください。
容量の目安
useState()を使う小さなカウンターを一つ、client:loadのIslandにした場合の比較です。同じページとコンポーネントを使い、ブラウザ向けのランタイムだけを変更して本番ビルドしました。
| 構成 | 圧縮後のJavaScript | gzip容量 | React構成からのgzip削減率 |
|---|---|---|---|
| React+React DOM | 約225KB | 約71KB | — |
| Redact(full) | 約62KB | 約22KB | 約68% |
| Redact(nano+hydration) | 約45KB | 約17KB | 約76% |
| Preact(compat) | 約22KB | 約10KB | 約86% |
2026年10月5日、React・React DOM 19.3.0、Redact 0.1.4、Preact 11.0.0、Vite 8.3.2、@preact/preset-vite 2.10.6で測定しました。1KBは1,000バイトです。出力されたJavaScript全ファイルを合計し、gzip容量は各ファイルを個別に圧縮して合計しています。
ランタイム単体ではなく、カウンターとIslandの起動コードも含む値です。実際の容量は、使うAPI、Redactの機能フラグ、依存ライブラリなどによって変わります。
Redactを使う
RedactはReact互換のランタイムです。公式Viteプラグインを追加するだけで導入でき、コンポーネントのimport { useState } from "react"などを書き換える必要はありません。
npm install @tanstack/redactimport { defineConfig, pluginSsg, pluginIsland } from "minista"
import { redact } from "@tanstack/redact/vite"
export default defineConfig({
plugins: [pluginSsg(), pluginIsland(), redact()],
})この構成では、ministaはReactで静的HTMLを生成し、ブラウザ向けのimportをRedactへ置き換えます。変換はdevでも有効なので、npm run devで前のガイドのカウンターを確認できます。
nanoでさらに削減する
redact()の既定のプリセットはfullです。必要な機能を絞る場合はnanoを選べます。
redact({
preset: "nano",
features: {
hydration: true,
},
})nanoは任意機能を無効にして容量を削減します。client:loadなど、静的HTMLを引き継ぐIslandにはハイドレーションが必要なため、hydration: trueを指定してください。nanoだけではhydrateRoot()がエラーになります。
ContextやSuspenseなどを使う場合は、featuresで必要な機能も有効にします。無効にした機能は元の動作を保つわけではありません。設定の詳細はRedactの機能フラグを参照してください。
Reactとの動作の違いを確認する
APIの名前が同じでも、すべての動作がReactと同じではありません。Redactは同期的に描画し、Reactの並行レンダリングや優先度による処理の分割を再現しません。
例えばstartTransition()は同期実行され、useTransition()のpendingはfalse、useDeferredValue()は入力値をそのまま返します。StrictModeによる二重実行や、React DevTools・Fast Refreshの内部機構も再現されません。利用するAPIは互換性の一覧を確認してください。
Preactを使う
Preactは小さなUIライブラリです。容量削減を優先し、独自のVite設定を管理できる場合の候補になります。この例ではpreact/compatを使い、Reactのimportを維持したままブラウザ向けコードを置き換えます。
npm install preact
npm install --save-dev @preact/preset-vite@preact/preset-viteをそのまま追加すると、静的HTML生成側のJSXやReactのimportも変更されます。ministaのReactレンダラーへPreactの要素を渡さないよう、プリセットとimportの置き換えをブラウザ向けの環境だけへ適用します。
ブラウザ向けのViteプラグインを作る
プロジェクトルートに.vite-plugins/preact.tsを作ります。これはプロジェクト内で管理する独自プラグインです。
import preact from "@preact/preset-vite"
import type { Plugin } from "vite"
const clientAliases = {
"react-dom/test-utils": "preact/test-utils",
"react-dom": "preact/compat",
"react/jsx-dev-runtime": "preact/jsx-dev-runtime",
"react/jsx-runtime": "preact/jsx-runtime",
react: "preact/compat",
}
function pluginClientPreactResolve(): Plugin {
return {
name: "example:client-preact-resolve",
enforce: "pre",
applyToEnvironment: (environment) =>
environment.config.consumer === "client",
resolveId(source, importer, options) {
for (const [find, replacement] of Object.entries(clientAliases)) {
if (source !== find && !source.startsWith(`${find}/`)) continue
return this.resolve(
`${replacement}${source.slice(find.length)}`, importer,
{ ...options, skipSelf: true },
)
}
},
}
}
function pluginPreactOptimizeDeps(): Plugin {
return {
name: "example:preact-optimize-deps",
enforce: "post",
configResolved(config) {
for (const optimizeDeps of [
config.optimizeDeps,
config.environments.client?.optimizeDeps,
]) {
if (!optimizeDeps) continue
optimizeDeps.include = (optimizeDeps.include ?? []).filter(
(id) => id !== "react" && !id.startsWith("react/") &&
id !== "react-dom" && !id.startsWith("react-dom/"),
)
optimizeDeps.include.push("preact/compat", "preact/compat/client")
optimizeDeps.exclude = [
...new Set([
...(optimizeDeps.exclude ?? []),
...Object.keys(clientAliases),
"react-dom/client",
]),
]
}
},
}
}
export function pluginPreact(): Plugin[] {
const clientPreactPlugins = preact({
reactAliasesEnabled: false,
jsxImportSource: "react",
}).map((plugin): Plugin => ({
...plugin,
applyToEnvironment: (environment) =>
environment.config.consumer === "client",
}))
return [
pluginClientPreactResolve(),
...clientPreactPlugins,
pluginPreactOptimizeDeps(),
{
name: "example:preact-dedupe",
config: () => ({ resolve: { dedupe: ["preact"] } }),
},
]
}applyToEnvironmentでブラウザ向け環境を選び、JSXのimport先とReactの互換層を置き換えます。開発時の依存関係の事前バンドルも調整して、ブラウザ側でReactとPreactが混在するのを防ぎます。
import { defineConfig, pluginSsg, pluginIsland } from "minista"
import { pluginPreact } from "./.vite-plugins/preact"
export default defineConfig({
plugins: [pluginSsg(), pluginIsland(), pluginPreact()],
})この設定はdevと本番ビルドの両方で有効です。npm run devでカウンターを確認でき、コンポーネントのReactのimportを変更する必要はありません。
ReactとのAPIと実装の違いを確認する
Preact本体のAPIはReactと完全には一致しません。この構成ではpreact/compatで差を吸収しますが、ReactのすべてのAPIや依存ライブラリが同じように動くとは限りません。
PreactはDOMのネイティブイベントを使い、Reactの合成イベントを再実装していません。例えばPortal内のイベントはReactと同じようには親へ伝わりません。Preact本体では入力にonInputを使いますが、この例のpreact/compatはReactのonChangeに対応する変換を行います。詳しくはReactとの違いを参照してください。
コンポーネントは静的HTML生成側でReactとしても実行されます。Preact専用のAPIを直接importする場合は、React側でも描画できるか確認が必要です。
構成と出力を確認する
コンポーネントやページはIslandの例を利用できます。Preactの場合は独自プラグインのファイルが加わります。
my-minista-project/
├── .vite-plugins/
│ └── preact.ts
├── src/
│ ├── components/
│ │ └── counter.tsx
│ └── pages/
│ └── index.tsx
└── vite.config.tsdist/
├── index.html
└── assets/
└── island-0-[hash].js出力先の構成は変わらず、IslandのJavaScriptに含まれるランタイムが変わります。ファイル名や分割数、容量は構成によって変わります。置き換え前後の本番ビルドで、出力されたJavaScriptの合計容量を比較してください。
devとpreviewで確認する
どちらの構成も、開発中から置き換えたランタイムで動作を確認できます。
npm run devカウンターが増えることや、フォーム、Context、外部ライブラリを使うUIが動作することを確認します。コンポーネントの変更は静的HTMLにも影響するため、フルリロードで状態が初期化される場合があります。
本番用の出力でも確認します。
npm run build
npm run previewハイドレーションの警告や実行時エラーがないか確認してください。nanoなどの設定を変えた場合も、devとpreviewの両方で試します。
設定の実例はRedactのplaygroundとPreactのplaygroundを参照してください。
次は静的サイト内検索で、Islandとして検索UIを追加します。ランタイムを置き換えている場合は、検索UIの動作も確認してください。