【カウンター実装比較】フロントエンド開発9種の構造と開発工数
はじめに:カウンターはただの練習問題ではない
「カウンターなんてHello Worldと同じでしょ?」
たしかに機能としてはそうかもしれません。しかし、実務でUIを設計・レビューする立場に立つと、カウンターは単なる加算機能ではなくなります。
- 状態をどこに定義するのか
- イベントと状態の結びつけ方は明示的か
- DOM更新のトリガーは何か
- 初期レンダリングと再描画の扱いに違いはあるか
- 構造が大きくなったときに状態が破綻しないか
そうした要素が、たった「+1」のボタンに全部詰まっているのです。
この記事では、主要9種のUIフレームワーク/ライブラリを横並びで比較し、構造・可読性・開発工数感・レビューしやすさを検証していきます。
比較対象:9種の主要フレームワーク/ライブラリ
今回比較するのは以下の9技術です。
- React(with Hooks)
- Vue(Composition API)
- SolidJS
- Preact
- Qwik
- Svelte
- Angular
- Lit
- Astro(+任意UIライブラリ)
比較の前提と想定シナリオ
前提仕様:
- UI上に「現在の数値」と「+1」ボタンを表示
- ボタンクリックで数値を加算
- 再描画は状態の変更によってトリガーされる
- 表示文例:
count: 5
評価軸:
- コード量(状態定義、DOM、イベント)
- 状態管理の明示性
- 記述の自然さ(JSX/テンプレート)
- 開発工数(セットアップ・理解コスト)
- レビュー観点(副作用、責務分離など)
React:状態とイベントをHooksで構造化
react
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}
分析:
useState
により状態の定義は明示的。- イベント(
onClick
)と状態変更が結合されており、構造が直感的。 - JSXに慣れていれば可読性は高いが、初学者にはJavaScriptとの混在が冗長に感じることも。
工数観点:
- Reactプロジェクトの起動・開発環境構築には
create-react-app
やVite
が必要。 - 状態が増えるとHooksの抽象化が必要になり、レビューでは依存追跡が課題になる。
Vue(Composition API):テンプレートとロジックの分離
vue
<template>
<div>
<p>count: {{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
</script>
分析:
- 状態は
ref()
で定義され、テンプレート上では``でバインディング。 - イベントとロジックを分離して記述できる。
ref.value++
などVue独自の書き方には慣れが必要。
工数観点:
script setup
構文を使えば、記述量はReactより少なめ。- テンプレート記述とロジックが明確に分かれており、UIと状態の責務が自然に読める。
- 状態が多くなると
watch
やcomputed
を追加する必要が出るが、構造自体は破綻しにくい。
SolidJS:最小粒度のリアクティブ構造
SolidJS
import { createSignal } from 'solid-js'
function Counter() {
const [count, setCount] = createSignal(0)
return (
<div>
<p>count: {count()}</p>
<button onClick={() => setCount(count() + 1)}>+1</button>
</div>
)
}
分析:
- 状態は
createSignal
で定義され、値の取得はcount()
という呼び出し形式。 - JSX構造はReactに近いが、再描画の仕組みは仮想DOMを使わず部分的に最適化されている。
- イベントとの結合も自然で、依存関係は自動で追跡される。
工数観点:
- React経験者なら習得は容易。
- 状態更新が関数呼び出し形式のため、初見では違和感を持つ可能性あり。
- コード量は少なく、リアクティブ性の理解さえあれば設計・レビューともに効率が良い。
Preact:React構文互換の超軽量実装
Preact
import { h } from 'preact'
import { useState } from 'preact/hooks'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}
分析:
- 構文・設計はReactと完全互換。違いはパッケージ名とパフォーマンス。
- 既存のReact知識でそのまま読み書きが可能。
- 仮想DOMやHooksも提供されており、開発者体験に大きな違いはない。
工数観点:
- Reactベースの既存コードをそのまま移植可能。
- ファイルサイズや初期読み込みが重要な環境では選定理由になる。
- 実質的にReactコードのレビュー手法がそのまま適用可能。
Qwik:遅延評価とResumable構造のカウンター
Qwik
import { component$, useSignal } from '@builder.io/qwik'
export const Counter = component$(() => {
const count = useSignal(0)
return (
<div>
<p>count: {count.value}</p>
<button onClick$={() => count.value++}>+1</button>
</div>
)
})
分析:
- 状態は
useSignal
で定義され、.value
でアクセス。 - JSX構文ながら、イベントは
onClick$
のようにQwik専用シンタックス。 - JavaScriptは初期描画時に実行されず、イベント発火まで遅延される(Resumable)。
工数観点:
- コンセプト理解に時間がかかる。特に復元・再実行タイミングの設計には習熟が必要。
- 小規模では過剰になりがちだが、パフォーマンス要件が厳しいプロジェクトでは有力。
- コード量は少なく見えるが、ランタイムの挙動まで把握する工数が別途必要。
Svelte:テンプレート駆動で直感的な構造
Svelte
<script>
let count = 0
const increment = () => count += 1
</script>
<div>
<p>count: {count}</p>
<button on:click={increment}>+1</button>
</div>
分析:
- 状態はプレーン変数で定義し、書き換えによって再描画が自動で走る。
- テンプレートとロジックが同一ファイルにあり、読みやすく書きやすい。
$:
やreactive statement
がないぶん、シンプルなカウンターでは明快。
工数観点:
- 学習コストは非常に低く、状態の扱いも自然。
- ビルドでDOM命令に変換されるため、構造は素直でもデバッグは若干注意が必要。
- 小〜中規模での導入は容易。構造が大きくなると状態の集中設計が必要。
Angular:型と構造で統制されたイベント設計
Angular
@Component({
selector: 'app-counter',
template: `
<div>
<p>count: </p>
<button (click)="increment()">+1</button>
</div>
`
})
export class CounterComponent {
count = 0
increment() {
this.count++
}
}
分析:
@Component
でテンプレートとロジックを明示的に分離。count
はクラス変数、イベントはメソッド呼び出しとして定義。- DI、モジュール構成、サービス注入などの影響がない分、シンプルな例では冗長に見える。
工数観点:
- セットアップと構造理解に一定の学習コストあり。
- 小さな構造では過剰な仕組みが多く感じられるが、大規模拡張を前提とした設計力を評価できる。
- レビュー時には責務の分離が明確で、意図の読み取りやすさは高い。
Lit:Web標準に即した再描画モデル
Lit
import { LitElement, html } from 'lit'
import { customElement, state } from 'lit/decorators.js'
@customElement('my-counter')
class MyCounter extends LitElement {
@state() count = 0
render() {
return html`
<div>
<p>count: ${this.count}</p>
<button @click=${() => this.count++}>+1</button>
</div>
`
}
}
分析:
@state()
でプロパティが変更可能状態になる。更新は自動的にDOMに反映。- HTMLテンプレートに近い構文だが、記述はJavaScript内。
- Shadow DOMによりスタイルや構造が分離され、独立性が高い。
工数観点:
- Web標準を活かして書けるため、長期的に保守性が高い。
- 状態やイベントは構造として読みやすいが、テンプレート記法にはやや癖がある。
- Web Componentsとの親和性が高く、再利用重視の場面に向く。
Astro:HTML重視構成にJavaScriptを限定的に使う
astro
// counter.astro
let count = 0
function increment() {
count += 1
}
<div>
<p>count: {count}</p>
<button on:click={increment}>+1</button>
</div>
分析:
- Astro単体では状態変更による再描画はできない。上記はビルド時の静的描画。
- 実際のカウンターを動かすにはReact/Svelte等の「Island」を別途導入する必要がある。
- ここでは「状態を持たない」構造を基本とし、動的要素は局所化される。
工数観点:
- 構造の把握はしやすい。HTMLとしてまず完成させられる。
- 動的UIの必要な範囲にだけJSを導入する思想は、レビューや保守性の面でも理にかなっている。
- ただし実装の「途中まで」は容易だが、インタラクション導入から一段難易度が上がる。
各フレームワークにおける工数感のまとめ
技術 | 状態定義 | JSX/テンプレート | 構造の直感性 | 学習負荷 | 初期セットアップ |
---|---|---|---|---|---|
React | useState | JSX | 中 | 中 | 中 |
Vue | ref | テンプレート | 高 | 中 | 中 |
SolidJS | createSignal | JSX | 高 | 中 | 中 |
Preact | useState | JSX | 中 | 低 | 低 |
Qwik | useSignal$ | JSX + Resumable | 低(要慣れ) | 高 | 高 |
Svelte | 変数 | テンプレート | 高 | 低 | 低 |
Angular | クラス変数 | テンプレート | 中〜高 | 高 | 高 |
Lit | @state | html`` | 中 | 中 | 中 |
Astro | HTMLベース | 静的 + Island | 高(静的部分) | 中 | 中 |
- 状態の扱いが素直なものは、実装が早くレビューも楽
- JSX/テンプレートの構造が実DOMと近いほど可読性が高い
- セットアップコストはプロジェクト体制に直結し、特に初期の技術選定時に重視される
まとめ:たかがカウンター、されどカウンター
UIを最小構成で書くとき、最も見えてくるのは「構造設計の思想」です。
- 状態をどう持つか
- どのようにDOMに反映させるか
- イベントと状態の結びつきをどこまで明示するか
たとえ「+1」しか機能しないコードであっても、その技術の全体設計方針が現れるのがカウンター実装です。
レビューアーとしては、この初期構造を観察することで、
- その技術が読みやすいか
- チーム内で共有可能か
- 将来的に責務を分離できるか
といった視点で選定の判断材料とすることができます。
カウンターは、構造の鏡です。
次回は同じ観点でフォーム入力と状態バインディングの比較を行う予定です。