【カウンター実装比較】フロントエンド開発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」しか機能しないコードであっても、その技術の全体設計方針が現れるのがカウンター実装です。
レビューアーとしては、この初期構造を観察することで、
- その技術が読みやすいか
 - チーム内で共有可能か
 - 将来的に責務を分離できるか
 
といった視点で選定の判断材料とすることができます。
カウンターは、構造の鏡です。