Old-School Front-End · Frameworks / 2026 ・ No.03
いにしえのフロントエンド入門 フレームワーク編
第3回 歴史②:React 以後 ── 作りなおして比べる流派と、シグナルの流派
公開
第2回では、2010年の3つの流派が、件数をずらさずに、しかも React より速く動くことを測りました。それでも 2013年に出た React は広まり、そのあと Vue、Angular、Svelte、Solid と、新しい答えが続きます。
第3回は、同じ明細の表をこの4つの最新版で書き、第1回からの React と並べて測ります。最後に、画面をサーバーで作る流れにもふれます。
React が持ちこんだもの
新井前回、React がいちばん遅かったじゃないですか。なんで勝ったんですか?
古井React が売ったのは速さではなく、わかりやすさだった。「状態が決まれば、画面は1つに決まる」。
全15回の路線図(いまここ:第3回)
第2回の Knockout や AngularJS は、値と画面を両方向につなぎました。画面が大きくなると、ある値を変えたのが入力欄なのか、ほかの計算なのか、別の見張りなのかを追うのがむずかしくなります。React は向きを片方向に決めました。状態を変えるのは setRows のような決まった入口だけで、画面は状態から毎回作りなおし、前回と比べてちがう所だけを書く(第1回の書き方 C)。比べるための画面の形の写しを、仮想 DOM と呼びます。
React 以後(年表)
この考え方は広く受け入れられ、そのあとの流派も「状態 → 画面」の片方向を土台にしています。ちがうのは、どこを書きかえるかをどう決めるかです。4つを、第1回からの明細の表で書きました。どれもビルドして JavaScript にしてから配り、サーバーはフロントエンド編 第15回の厳しい CSP のままです。
Vue:テンプレートと、見張る値
Vue(書き方 G)は 2014年に、AngularJS を使っていた開発者が、もっと軽いものとして作り始めました。HTML に近いテンプレートと v-model は AngularJS に似ていますが、値は ref に入れ、どの値がどこで読まれたかを Vue が覚えています(第2回の Knockout に近い)。
<!-- 書き方 G:Vue 3.5。値は ref に入れる。テンプレートの v-model と {{ }} が値と画面をつなぎ、ビルドのときに JavaScript になる -->
<script setup>
import { ref, computed, onMounted } from 'vue';
let nextId = 1;
const newRow = () => ({ id: nextId++, item: '', amount: '', currency: 'JPY' });
const T = ROWS_TEXT;
const { formatYen, needsReceipt } = window; // app.js の関数(テンプレートからは、ここに書いたものしか見えない)
const rows = ref([newRow()]);
const rate = ref(null);
function yenOf(row) { … } // 第1回と同じ(円に直す)
// 合計:中で読んだ値(行・金額・通貨・レート)が変わったときだけ、計算しなおす
const total = computed(() => calcTotal(rows.value.map(yenOf).filter((y) => y !== null && !Number.isNaN(y)).map((amount) => ({ amount }))));
const add = () => rows.value.push(newRow());
const remove = (row) => {
rows.value = rows.value.filter((r) => r !== row);
if (rows.value.length === 0) rows.value.push(newRow());
};
onMounted(() => { fetch('/api/rates').then((res) => res.json()).then((data) => { rate.value = data; }).catch(() => {}); });
</script>
<template>
<h1>{{ T.heading }} <span id="count">{{ T.count(rows.length) }}</span></h1>
<table>
<tbody id="items">
<tr v-for="(r, i) in rows" :key="r.id">
<td><input name="item" v-model="r.item"></td>
<td><input name="amount" v-model="r.amount"></td>
<td><select name="currency" v-model="r.currency">…</select></td>
<td><button @click="remove(r)">{{ T.remove }}</button></td>
</tr>
</tbody>
</table>
…
<output id="total">{{ formatYen(total) }}</output>
</template>
件数(★)はテンプレートに {{ T.count(rows.length) }} と書くだけです。ただし画面を直すときは、React と同じく、コンポーネントのテンプレートを作りなおして前回と比べます。値の見張りは細かく、画面の書きかえはコンポーネント単位。この組み合わせが、七の数字に出ます。
Angular:作りなおしと、シグナル
Angular(書き方 J)は、AngularJS を TypeScript で一から作りなおしたもので、2016年に 2.0 が出ました。名前は似ていますが、別物です。長いあいだ、zone.js という仕組みで「何か起きたかもしれない」ときに画面全体を確かめていましたが、2023年の 16 からシグナルが入り、2025年の 21 からは zone.js なしが新しいアプリの既定になりました。
// 書き方 J:Angular 22。値はシグナル(signal)に入れ、computed で合計を出す。テンプレートはビルドのときに JavaScript になる。
// 21 からは zone.js なし(zoneless)が既定。シグナルが変わったときだけ、画面を確かめなおす
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
type Row = { id: number; item: string; amount: string; currency: string };
type Rate = { rate: number; date: string };
@Component({
selector: 'rows-app',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h1>{{ T.heading }} <span id="count">{{ T.count(rows().length) }}</span></h1>
<table>
<tbody id="items">
@for (r of rows(); track r.id; let i = $index) {
<tr>
<td><input name="item" [value]="r.item" (input)="change(r.id, 'item', $event)"></td>
<td><input name="amount" [value]="r.amount" (input)="change(r.id, 'amount', $event)"></td>
<td><select name="currency" [value]="r.currency" (change)="change(r.id, 'currency', $event)">…</select></td>
<td><button (click)="remove(r.id)">{{ T.remove }}</button></td>
</tr>
}
</tbody>
</table>
…
<output id="total">{{ totalText() }}</output>
`,
})
class RowsComponent {
T = ROWS_TEXT;
max = MAX_ROWS;
rows = signal<Row[]>([newRow()]);
rate = signal<Rate | null>(null);
private yenOf(row: Row, rate: Rate | null) { … } // 第1回と同じ(円に直す)
// 合計:中で読んだシグナル(行・レート)が変わったときだけ、計算しなおす
total = computed(() => calcTotal(this.rows().map((r) => this.yenOf(r, this.rate())).filter((y): y is number => y !== null && !Number.isNaN(y)).map((amount) => ({ amount }))));
totalText = computed(() => formatYen(this.total()));
receipt = computed(() => needsReceipt(this.total()));
change(id: number, name: 'item' | 'amount' | 'currency', event: Event) {
const value = (event.target as HTMLInputElement).value;
this.rows.update((rows) => rows.map((r) => (r.id === id ? { ...r, [name]: value } : r)));
}
add() { … }
remove(id: number) { … }
}
signal は値の入れ物、computed はそこから計算する値です。テンプレートの @for は track r.id で行を見分けます。型の宣言がある分、4つの中でいちばん行が多くなりました。
Svelte:コンパイラが書く
Svelte(書き方 H)は 2016年に出て、コンパイラという立場をとりました。ブラウザで比べる仕事をせず、ビルドのときに「この値が変わったら、この文字を書きかえる」という JavaScript をはき出します。2024年の Svelte 5 からは、値を $state、計算する値を $derived と書きます(runes)。
<!-- 書き方 H:Svelte 5。$state に入れた値は、ふつうの変数のように読み書きできる。コンパイラが、どこを書きかえるかをビルドのときに決める -->
<script>
let nextId = 1;
const newRow = () => ({ id: nextId++, item: '', amount: '', currency: 'JPY' });
const T = ROWS_TEXT;
let rows = $state([newRow()]);
let rate = $state(null);
function yenOf(row) { … } // 第1回と同じ(円に直す)
// 合計:中で読んだ値が変わったときだけ、計算しなおす
const total = $derived(calcTotal(rows.map(yenOf).filter((y) => y !== null && !Number.isNaN(y)).map((amount) => ({ amount }))));
function remove(row) {
rows = rows.filter((r) => r !== row);
if (rows.length === 0) rows.push(newRow());
}
fetch('/api/rates').then((res) => res.json()).then((data) => { rate = data; }).catch(() => {});
</script>
<h1>{T.heading} <span id="count">{T.count(rows.length)}</span></h1>
<table>
<tbody id="items">
{#each rows as r, i (r.id)}
<tr>
<td><input name="item" bind:value={r.item}></td>
<td><input name="amount" bind:value={r.amount}></td>
<td><select name="currency" bind:value={r.currency}>…</select></td>
<td><button onclick={() => remove(r)}>{T.remove}</button></td>
</tr>
{/each}
</tbody>
</table>
…
<output id="total">{formatYen(total)}</output>
rows.push(newRow()) のように、ふつうの配列のように書いても画面がついてきます。4つの中でいちばん短い 44 行でした。
Solid:1回だけ動くコンポーネント
Solid(書き方 I)は、React と同じ JSX で書きますが、中身は正反対です。コンポーネントの関数は最初の1回しか動きません。JSX の {…} の中で読んだシグナルが変わると、その場所だけが書きかわります。
// 書き方 I:Solid 1.9。値はシグナル(ここでは行の集まりを store)に入れる。コンポーネントの関数は最初の1回しか動かず、
// JSX の {…} の中で読んだシグナルが変わると、その場所だけが書きかわる
import { createSignal, createMemo, For } from 'solid-js';
import { createStore } from 'solid-js/store';
import { render } from 'solid-js/web';
let nextId = 1;
const newRow = () => ({ id: nextId++, item: '', amount: '', currency: 'JPY' });
const T = ROWS_TEXT;
function Rows() {
const [rows, setRows] = createStore([newRow()]);
const [rate, setRate] = createSignal(null);
const yenOf = (row) => { … }; // 第1回と同じ(円に直す)
// 合計:中で読んだ値が変わったときだけ、計算しなおす
const total = createMemo(() => calcTotal(rows.map(yenOf).filter((y) => y !== null && !Number.isNaN(y)).map((amount) => ({ amount }))));
const change = (i, name) => (e) => setRows(i(), name, e.target.value);
const remove = (row) => {
const rest = rows.filter((r) => r !== row);
setRows(rest.length > 0 ? rest : [newRow()]);
};
return (
<>
<h1>{T.heading} <span id="count">{T.count(rows.length)}</span></h1>
<table>
<tbody id="items">
<For each={rows}>{(r, i) => (
<tr>
<td><input name="item" value={r.item} onInput={change(i, 'item')} /></td>
<td><input name="amount" value={r.amount} onInput={change(i, 'amount')} /></td>
<td><select name="currency" value={r.currency} onChange={change(i, 'currency')}>…</select></td>
<td><button onClick={() => remove(r)}>{T.remove}</button></td>
</tr>
)}</For>
</tbody>
</table>
…
<output id="total">{formatYen(total())}</output>
</>
);
}
見た目は React、動きは Knockout の流れ。Svelte・Angular(シグナル)とあわせて、この3つをシグナルの家族と呼ぶことにします。
同じ8つの操作
# 第1回と同じ8つの操作。フロントエンド編 第15回の厳しい CSP のまま 書き方件数のずれ合計のずれCSP のエラー追加・削除のあとのカーソル G Vue 3.5 0 / 8 0 / 8 0 「行を追加」 / BODY H Svelte 5 0 / 8 0 / 8 0 「行を追加」 / BODY I Solid 1.9 0 / 8 0 / 8 0 「行を追加」 / BODY J Angular 22 0 / 8 0 / 8 0 「行を追加」 / BODY C React 19 0 / 8 0 / 8 0 「行を追加」 / BODY # 3 言語とも同じ。どれもビルドしてから配るので、文字から関数を作らない
4つとも、件数も合計も一度もずれず、厳しい CSP でもエラーは0でした(中国語・英語のページでも同じ)。第2回の Backbone のひな形や Knockout とちがい、テンプレートはビルドのときに JavaScript になっているので、ブラウザで文字から関数を作りません。カーソルは、どれも何も書かなければ何もしません。
# 3行の表で、1行目の金額に 1・4・7・2・0 と打ったとき、1文字ごとの画面の変化(MutationObserver) 書き方変化の記録中身 G Vue 3.5 1 合計の文字を入れかえ H Svelte 5 1 合計の文字だけ I Solid 1.9 2 合計の文字と、注意書きの hidden J Angular 22 1 合計の文字だけ C React 19 23 21 は React が name・type を同じ値で付けなおす内部の手順
1文字で書きかわったのは、どれも合計の文字だけ(Solid は注意書きの hidden も付けなおしました)。React だけが、入力欄の属性を同じ値で付けなおす手順を 21 回ふくんでいます。画面に届く変化は、どの書き方もほとんど同じです。ちがいは、そこへ行くまでのJavaScript の仕事の量に出ます。
5000 行で、2つの家族
行を 10・1000・5000 にして、1文字の入力で JavaScript が働いた時間と、ブラウザが描いた時間を測りました。Vue・Svelte・Angular は画面の書きかえを少しあとに回すので、第2回の測り方(input を送って戻るまで)では足りません。今回は Chromium の記録(tracing)から、20 文字ぶんの時間を足しました。第2回の Knockout と AngularJS も、同じ測り方で測りなおしています。
# 行を 10・1000・5000 にして、1文字の入力にかかった時間と、開くまでの時間 書き方1文字の JS(ms)描く(ms)開く(ms) 10 1000 5000 5000 1000 5000 C React 19 2.0 20.4 99.6 124.3 603 2,280 G Vue 3.5 1.7 15.1 91.5 105.5 652 2,208 H Svelte 5 0.8 3.0 9.6 106.0 574 2,540 I Solid 1.9 1.3 2.1 11.4 117.5 473 1,936 J Angular 22 1.2 2.4 6.7 127.7 575 2,079 E Knockout 0.8 1.6 5.2 107.2 759 2,793 F AngularJS 1.7 3.1 24.4 125.1 814 3,432 # JS と描く時間は、Chromium の記録(tracing)から 20 文字ぶんを足して割った。3回の中央値。第2回(input を送って戻るまで)とは測り方がちがう
5000 行で1文字打つと、JavaScript は何ミリ秒働いたか
はっきり2つに分かれました。React と Vue は 90 ms 台、Svelte・Solid・Angular は 10 ms 前後で、2010年の Knockout(5.2 ms)と同じ側です。
2つの家族:作りなおして比べる/つながった所だけ
React と Vue は、金額が1つ変わっても、コンポーネントをもう一度動かし、5000 行ぶんの画面の形を作って比べます。シグナルの家族は、その金額を読んでいる所(合計)を最初から知っているので、比べる仕事がありません。React が 2013年に「わかりやすさ」のために払った代金を、2020年代のシグナルが取りもどした、と見ることもできます。ただし、描く時間(100 ms あまり)はどれも同じです。5000 行の表を画面に出すこと自体が重い、というのは第2回と同じ結論です。
新井じゃあ、React をやめてシグナルにすれば……。
古井10 行の表なら、どれも 2 ms ほどで終わる。React には React の縮め方がある。それは第8回で測る。
ビルドと大きさ
# 自分で書いた量と、ブラウザに届く大きさ(app.js と文字の分は、どれも同じなので除いた) 書き方書いた行届く大きさgzip ビルド G Vue 3.5 47 68,096 B 26,359 B Vite 8.3 0.2 s H Svelte 5 44 38,401 B 15,032 B Vite 8.3 0.2 s I Solid 1.9 54 18,583 B 7,232 B Vite 8.3 0.2 s J Angular 22 73 110,729 B 37,223 B Angular CLI 22 3.2〜4.4 s C React 19 59 226,513 B 70,123 B esbuild 0.28 73 ms # 書いた行は空行を除く(説明のコメントもふくむ)。J は型の宣言が 13 行ある。参考:第2回の Knockout は 70,680 B
届く大きさは、Solid の 18 KB から React の 226 KB まで、12 倍ちがいました。Svelte と Solid は、使った部品の分だけをはき出すので小さくなります。React は第1回のとおり、画面に書く係(react-dom)が大半です。ビルドは Vite なら 0.2 秒、Angular CLI でも数秒でした。第1回で「引きかえに増えるもの」と書いたビルドの手順は、いまはどの流派にもある、ふつうの手順になっています。
画面をサーバーで作る
ここまでの7つは、どれもブラウザの中で画面を作りました。最初に届く HTML はほとんど空で、JavaScript が届いて動くまで、明細の表は出ません。そこで、サーバーで HTML まで作って送る流れが戻ってきます。
- 2016年 ── Next.js 1.0。React の画面を、サーバーで HTML にしてから送る(SSR)
- 2023年 ── Next.js 13.4 の App Router。画面の部品ごとに、サーバーで動くかブラウザで動くかを分ける
- 2024年 ── React 19 で Server Components が正式に。サーバーだけで動く部品は、ブラウザへ JavaScript を送らない
バックエンド編で作った Spring Boot のサーバーが HTML を返していた時代と、ブラウザで全部作る時代の、ちょうど間です。この連載では第12・13回で、Next.js の上で実際に測ります。
自分の PC で
どの流派も、ひな形を作るコマンドがあります。Node.js が入っていれば、次の1行でそれぞれの練習用プロジェクトが作れます。
# Vite のひな形(react・vue・svelte・solid などから選ぶ) npm create vite@latest # Angular のひな形 npx @angular/cli new my-app
同じ画面を2つの流派で書き、開発者ツールの Performance タブで1文字打つところを記録すると、この回の「2つの家族」のちがいが自分の目で見えます。黄色(Scripting)の長さを比べてください。
結 — 一枚まとめと、次回
- React(2013)は片方向を持ちこんだ。状態から画面を作りなおし、前回と比べてちがう所を書く。
- Vue・Angular・Svelte・Solid の最新版も、同じ8つの操作でずれ0、厳しい CSP でもエラー0。どれもビルドしてから配る。
- 5000 行で1文字:作りなおして比べる React・Vue は 90 ms 台、シグナルの Svelte・Solid・Angular は 10 ms 前後。描く時間はどれも 100 ms あまり。
- 届く大きさは Solid 18 KB 〜 React 226 KB。
- 2016年からは、画面をサーバーで作る流れも戻ってきた(Next.js、Server Components)。
新井歴史はわかりました。で、この連載はどれで書くんですか?
古井React だ。いちばん使われていて、Next.js でサーバーまで続いている。遅い所も、測ってから直せばいい。
古井次回は React の最初の一歩。Vite でひな形を作り、JSX とコンポーネントで、経費精算の画面を作りはじめる。
参考
- React — Preserving and Resetting State
- Vue — Rendering Mechanism
- Svelte — What are runes?
- Solid — Intro to reactivity
- Angular — Signals
- InfoQ — Angular 21 Released(zoneless が既定に)
- React Blog — React v19
- Chrome DevTools — Performance features reference
記録は、筆者がクラウド上の Linux で、2026年11月1日に取ったものです。ページは練習用のサーバー(Node.js 22)から配り、Playwright で動かした Chromium 153.0.8010.12 で操作と測定をしました。G は Vue 3.5.44、H は Svelte 5.57.3、I は Solid 1.9.18 を Vite 8.3.5 で、J は Angular 22.2.4 を Angular CLI 22.2.5 でビルドしました。C は第1回の React 19.3.1 です。時間は、同じ欄に値を入れて input のイベントを送り、次の描画を待つことを 20 回くり返し、そのあいだの Chromium の記録から JavaScript(FunctionCall・RunMicrotasks・TimerFire など)と描く仕事(Layout・Paint など)の時間を重ならないように足して 20 で割り、3回の中央値をとりました。公開の年は npm の記録と参考の資料によります。ほかの PC では数字が変わります。
いにしえのフロントエンド入門 フレームワーク編 第3回 — 歴史②:React 以後 / 2026.11