はじめに
「Reactを使いたいけれど、バンドルサイズが気になる」——そんな悩みを抱えたことはありませんか。ランディングページやウィジェット、モバイル回線での表示速度がシビアなプロダクトでは、Reactのバンドルサイズがボトルネックになることがあります。
そこで候補に挙がるのがPreactです。Reactとほぼ同じAPIを持ちながら、コア部分はわずか3KB程度という驚異的な軽さを実現しています。今回はPreactの特徴から実際の使い方まで、手を動かしながら紹介します。
とはいえ、説明を読むよりも実際に触ってみる方が早いと思います。PreactのuseStateを使ったカウンターとテキスト入力を、このままブラウザ上で動かせるサンプルを用意しました。先に挙動を確認したい方はこちらからどうぞ。
Preactとは
Preactは、Reactと同じモダンなAPI(Components、Hooks、JSX)を持ちながら、圧倒的に小さなフットプリントで動作する仮想DOMライブラリです。公式サイトでは「Fast 3kB alternative to React with the same modern API」と紹介されており、React学習で得た知識をほぼそのまま活かせるのが最大の魅力です。
主な特徴
- 超軽量 - コア部分は3KB程度に収まっており、ページ読み込み速度やパースコストを大幅に削減できます
- Reactと同じAPI -
useStateやuseEffectなどのHooksをはじめ、クラスコンポーネント・関数コンポーネントの両方に対応しています - preact/compatによる互換性 - バンドラーの設定にエイリアスを1行追加するだけで、既存のReactライブラリの多くをそのまま利用できます
- DOMに近い実装 - 標準的なプラットフォーム機能に寄せた設計になっており、実際のイベントハンドラを登録するなど、シンプルで予測しやすい動作をします
インストール
新規プロジェクトを始める場合は、公式が提供するCLIが便利です。
npm init preact
既存のプロジェクトに導入する場合は、パッケージを直接追加します。
# npm
npm install preact
# yarn
yarn add preact
# pnpm
pnpm add preact
JSXを使う場合はビルド設定にjsxのインポート元を指定する必要があります。Viteを使う場合は@preact/preset-viteを利用すると簡単です。
npm install --save-dev @preact/preset-vite
// vite.config.js
import { defineConfig } from "vite";
import preact from "@preact/preset-vite";
export default defineConfig({
plugins: [preact()],
});
Preactのサンプルを動かす
説明の前に、実際にPreactを動かして手触りを確かめてみましょう。以下はpreact/hooksのuseStateを使ったカウンターと、onInputイベントで入力値をリアルタイムに反映するテキストフォームです。ボタンを押すとカウントが増減し、入力欄に文字を打つとそのままメッセージに反映されます。
要点だけを抜き出すと次のようになります。useStateが返す状態と更新関数をコンポーネント内で使い、onInputイベントハンドラでe.currentTarget.valueを受け取っている点がポイントです。
import { useState } from "preact/hooks";
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState("Preact");
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<input
value={name}
onInput={(e) => setName(e.currentTarget.value)}
/>
<p>こんにちは、{name}さん!</p>
</div>
);
}
実際にブラウザ上で編集・実行できるのが以下のサンプルです。ボタンを連打してカウントを増減させたり、入力欄の文字を消して空にしたりして、Preactの再描画の速さを確かめてみてください。
setCount((c) => c + 1)のように更新関数へ関数を渡すと、直前の状態を安全に参照できます。また入力欄を空にするとname || 'ゲスト'の部分でフォールバック表示に切り替わるのも確認できるはずです。このように、PreactのHooksはReactと全く同じ書き味で状態管理ができます。
基本的な使い方
まずはシンプルなカウンターコンポーネントを作ってみましょう。ReactのHooksをそのまま使っている感覚で書けます。
// Counter.jsx
import { useState } from "preact/hooks";
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={() => setCount((c) => c - 1)}>-1</button>
</div>
);
}
エントリーポイントでの描画も、Reactとほぼ同じ書き方です。
// main.jsx
import { render } from "preact";
import { Counter } from "./Counter";
render(<Counter />, document.getElementById("app"));
useEffectやuseContextなど、よく使うHooksもpreact/hooksから同じ名前でインポートできます。
import { useEffect, useState } from "preact/hooks";
export function UserStatus({ userId }) {
const [isOnline, setIsOnline] = useState(false);
useEffect(() => {
const timer = setInterval(() => {
setIsOnline((prev) => !prev);
}, 3000);
return () => clearInterval(timer);
}, [userId]);
return <span>{isOnline ? "オンライン" : "オフライン"}</span>;
}
実践的なユースケース
既存のReactライブラリを使う
preact/compatを使うと、React向けに作られたUIライブラリの多くをそのまま利用できます。バンドラーにエイリアスを追加するだけです。
// vite.config.js
import { defineConfig } from "vite";
import preact from "@preact/preset-vite";
export default defineConfig({
plugins: [preact()],
resolve: {
alias: {
react: "preact/compat",
"react-dom": "preact/compat",
},
},
});
これにより、reactやreact-domをimportしているサードパーティ製コンポーネントも、実行時にはPreactのランタイムで動作するようになります。
実際にpreact/compatが提供するAPIを覗いてみましょう。React生態系のライブラリでよく使われるforwardRefは、Preactのコア(preact)には無く、preact/compatだけが公開しているAPIです。バンドラーのエイリアス越しにreactをimportしても実体はpreact/compatが使われるため、forwardRefやmemoに依存したライブラリもそのまま動作します。
import { forwardRef, useState } from "preact/compat";
const FancyInput = forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
function App() {
const [value, setValue] = useState("");
return (
<FancyInput
value={value}
onInput={(e) => setValue(e.currentTarget.value)}
/>
);
}
実際に動かせるサンプルは以下のとおりです。入力欄に文字を打つと、下の表示にそのまま反映されます。
forwardRefをuseStateと組み合わせても動作はReactと変わりません。実際のプロジェクトではreactとreact-domをpreact/compatにエイリアスするだけで、このforwardRefやmemoを使った既存のReactコンポーネントライブラリもコード変更なしに読み込めます。
バンドルサイズがシビアな埋め込みウィジェット
自社サービスを他社サイトに埋め込むチャットウィジェットや広告ウィジェットのような、読み込み速度が直接ユーザー体験に影響するケースでは、Preactの軽さが特に活きます。
// widget.jsx
import { render } from "preact";
import { useState } from "preact/hooks";
function FeedbackWidget() {
const [sent, setSent] = useState(false);
const handleSubmit = (event) => {
event.preventDefault();
// フォーム送信処理はここに実装
setSent(true);
};
if (sent) {
return <p>フィードバックを送信しました。ありがとうございます!</p>;
}
return (
<form onSubmit={handleSubmit}>
<label>
ご意見をお聞かせください
<textarea name="feedback" required />
</label>
<button type="submit">送信</button>
</form>
);
}
const mountPoint = document.getElementById("feedback-widget");
if (mountPoint) {
render(<FeedbackWidget />, mountPoint);
}
このようなウィジェットをReactでビルドすると数十KB単位になりがちですが、Preactなら本体分のオーバーヘッドをほぼ無視できるレベルまで抑えられます。
実際に入力しながら動く形にしたのが以下のサンプルです。preact/hooksのuseStateを使い、textareaのonInputで文字数をリアルタイムにカウントしています。
import { useState } from "preact/hooks";
function FeedbackWidget() {
const [text, setText] = useState("");
return (
<div>
<textarea
value={text}
onInput={(e) => setText(e.currentTarget.value)}
/>
<p>{text.length} 文字入力中</p>
<button disabled={text.length === 0}>送信</button>
</div>
);
}
disabled={text.length === 0}のように、useStateで持っている文字数をそのままJSXの属性に渡せるのがPreactの書き味です。テキストエリアの中身を全部消すと送信ボタンが自動的に無効化される様子を確認してみてください。
静的サイトの部分的なインタラクティブ化
ほとんどが静的なコンテンツで、一部だけJavaScriptによる操作(アコーディオンやタブ切り替えなど)が必要なページにも向いています。ページ全体をSPA化せず、必要な箇所だけPreactでマウントすることで、軽快な読み込みを維持できます。
// accordion.jsx
import { render } from "preact";
import { useState } from "preact/hooks";
function Accordion({ title, children }) {
const [open, setOpen] = useState(false);
return (
<div class="accordion">
<button onClick={() => setOpen((prev) => !prev)}>
{title} {open ? "▲" : "▼"}
</button>
{open && <div class="accordion-content">{children}</div>}
</div>
);
}
document.querySelectorAll("[data-accordion]").forEach((el) => {
render(
<Accordion title={el.dataset.title}>{el.textContent}</Accordion>,
el
);
});
Accordionコンポーネントにtitleとchildrenという2つのpropsを渡している点に注目してください。Preactでも、Reactと同じように親から子へpropsでデータを渡し、子コンポーネント内でuseStateにより開閉状態を管理する、というコンポーネントの分割ができます。
import { useState } from "preact/hooks";
function Accordion({ title, children }) {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen((v) => !v)}>
{title} {open ? "▲" : "▼"}
</button>
{open && <div>{children}</div>}
</div>
);
}
実際に複数のAccordionを並べて、propsで内容だけを差し替えている例が以下です。それぞれ独立してクリックで開閉できます。
titleのテキストやchildrenの中身を書き換えるだけで、Accordionコンポーネント自体には一切手を加えずに表示内容を差し替えられます。これがコンポーネントをpropsで分割して再利用する、Preactの基本的な設計パターンです。
まとめ
Preactは、Reactと同じモダンなAPIを維持しながら、圧倒的な軽さを実現しているライブラリです。既存のReact知識をそのまま活かせるうえ、preact/compatによってエコシステムの多くの資産も利用できるため、乗り換えのハードルも低めです。
特に、埋め込みウィジェットやランディングページ、パフォーマンスがシビアに問われるプロダクトでは、Preactを検討する価値は十分にあります。まずは小さなコンポーネントから、npm init preactで試してみてはいかがでしょうか。