Old-School Front-End · Frameworks / 2026 ・ No.02

いにしえのフロントエンド入門 フレームワーク編

第2回 歴史①:jQuery から MVC へ ── 2010年の3つの答え、Backbone・Knockout・AngularJS

公開

3つのブラウザの画面。どれも「経費の明細」の表で、第1回と同じ8つの操作をしたあと。左上は書き方 D(Backbone 1.6)、右上は E(Knockout 3.5)、下は F(AngularJS 1.8)。3つとも明細 3 件、金額 14720 と 100(ドル)、空の行が1つ、合計 29,843 円
第1回の明細の表を、2010年に出た3つの流派で書き、同じ操作をしました。3つとも件数も合計も正しい(筆者がクラウド上の Linux で、2026年11月1日に撮影。ウインドウの上の帯は説明のために描き足したもの)

第1回では、jQuery の画面が「どこを直すか」を人に覚えさせていて、件数を1つ出すだけでずれることを測りました。この悩みに、2010年、3つの答えがほぼ同時に出ます。Backbone、Knockout、AngularJS です。
第2回は、同じ明細の表をこの3つで書き、第1回と同じ操作をして比べます。ずれるか、何が書きかわるか、行が多いとどうか、そして、いまの厳しい CSP の下で動くか。

一

2010年、画面が大きくなった

新井React の前って、jQuery しかなかったんじゃないんですか?

古井2010年に、3つ出ている。しかも、どれも今でも動いているアプリがある。

新井化石ですね。

古井化石かどうか、測ってから決めよう。

全15回の路線図(いまここ:第2回)

1 なぜ FW 2 jQuery→MVC 3 React 以後 4 React の一歩 5 状態と再描画 6 フォーム 7 データ取得 8 再描画のコスト 9 状態の置き場所 10 画面遷移 11 楽観的更新 12 Server Comp. 13 キャッシュ 14 テスト 15 公開と選び方 いまここ なぜと歴史 React の基本 速さと置き場所 画面とサーバー Next.js 仕上げ

このころ、ページを読みこみなおさずに画面の中だけで動くアプリが増え、画面の JavaScript は数百行から数千行になっていきました。jQuery は「画面の部品を探して書きかえる」道具としては十分でしたが、本当の値をどこに置き、いつ画面に反映するかは、書く人に任されていました。第1回で測った「件数のずれ」は、その悩みそのものです。

3つの流派と、その前後(年表)

3つの流派と、その前後(年表) 2006 2008 2010 2012 2014 2016 2018 2020 2022 2024 2026 jQuery 2006 4.0 Knockout 1.0(2010.7) 3.5.3 Backbone 0.1(2010.10) 1.6.1 AngularJS 1.0(2012.6) サポート終了 React 2013.5 → 第3回 点線は、サポートが終わったあとも動いているアプリ。2010年に、3つの答えがほぼ同時に出た

3つとも、第1回の明細の表(行の追加・削除、金額、通貨、合計、そして件数)を書きました。計算はこれまでと同じ app.js です。時代の書き方に合わせて、サーバーは CSP などの守りのヘッダーを外した状態で動かし、CSP は八で別に試します。

二

Backbone:値を Model に置く

Backbone(書き方 D)は、Underscore と同じ作者の小さなライブラリです。考え方は MVC:本当の値は Model(1行)と Collection(行の集まり)に置き、画面の部品は View が受け持ちます。値が変わると Model がイベントを出し、View はそれを聞いて画面を書きかえます。

// 書き方 D:Backbone 1.6(2010年〜)
// 本当の値は Model(1行)と Collection(行の集まり)に持つ。値が変わると、Model がイベントを出す。
// 画面を書きかえるのは View。どのイベントで、どこを書きかえるかは、ここに自分で書く。
const Row = Backbone.Model.extend({ defaults: { item: '', amount: '', currency: 'JPY' } });
const rows = new Backbone.Collection([], { model: Row });
const settings = new Backbone.Model({ rate: null, date: '' }); // 為替レート

/** 1行の View:表の1行(tr)を受け持つ */
const RowView = Backbone.View.extend({
  tagName: 'tr',
  template: _.template($('#row-template').html()), // ひな形の文字を、関数に変える
  events: {
    'input [name="item"]': 'edit',
    'input [name="amount"]': 'edit',
    'change [name="currency"]': 'edit',
    'click [data-action="remove"]': 'drop',
  },
  edit(event) { this.model.set(event.target.name, event.target.value); }, // 画面 → Model は、自分で書く
  drop() { rows.remove(this.model); },
  render() { this.$el.html(this.template(this.model.toJSON())); return this; },
});

/** 画面全体の View */
const AppView = Backbone.View.extend({
  el: '#page',
  events: { 'click #add-row': 'add' },
  initialize() {
    this.listenTo(rows, 'add', this.addOne);
    this.listenTo(rows, 'remove', this.removeOne);
    this.listenTo(rows, 'update change', this.renderSummary); // 行が増えた・減った・値が変わった
    this.listenTo(settings, 'change', this.renderSummary);
  },
  add() { … },       // rows.add({}) と、新しい行へカーソル
  addOne(row) { … }, // RowView を作って表に足す
  removeOne(row) { … },
  /** 合計・件数・ボタン・レートを出しなおす */
  renderSummary() {
    const total = …; // rows.each で1行ずつ足す
    this.$('#total').text(formatYen(total));
    this.$('#receipt-note').prop('hidden', !needsReceipt(total));
    this.$('#count').text(ROWS_TEXT.count(rows.length)); // ★ 件数
    this.$('#add-row').prop('disabled', rows.length >= MAX_ROWS);
    this.$('#rate-note').text(rate === null ? '' : ROWS_TEXT.rate(rate, settings.get('date')));
  },
});

件数を出す1行(★)は、合計といっしょに renderSummary に書きました。第1回の jQuery と同じ置き方ですが、今回はずれません。renderSummary を呼ぶのはボタンではなく、Collection の update(行が増えた・減った)と change(値が変わった)だからです。どの操作から来ても、値が変われば同じイベントが出る。第1回の「操作 × 関数」の表が、「値 × 関数」の表に変わりました。

ただし、画面を書きかえるのは今も自分です。this.$('#total').text(…) は jQuery のころと同じ形で、画面 → Model の向き(edit)も自分で書いています。

三

Knockout:値と画面を結ぶ

Knockout(書き方 E)は MVVM という形です。値は observable(見張れる入れ物)に入れ、HTML の data-bind に「この部品は、どの値とつながるか」を書きます。

# rows-ko.html(抜粋):HTML に、どの値とつながるかを書く
<h1 …>経費の明細 <span id="count" data-bind="text: countText"></span></h1>
<tbody data-bind="foreach: rows">
  <tr>
    <td><input … data-bind="textInput: item"></td>
    <td><input … data-bind="textInput: amount"></td>
    <td><select … data-bind="value: currency">…</select></td>
    <td><button … data-bind="click: $root.remove">…</button></td>
  </tr>
</tbody>
<output id="total" … data-bind="text: totalText"></output>

# rows-ko.js(抜粋)
function RowModel() {
  this.item = ko.observable('');
  this.amount = ko.observable('');
  this.currency = ko.observable('JPY');
}

function RowsModel() {
  this.rows = ko.observableArray([new RowModel()]);
  this.rate = ko.observable(null);

  // 合計:中で読んだ observable(行の数・金額・通貨・レート)が変わると、計算しなおす
  this.total = ko.pureComputed(() => {
    const rate = this.rate();
    const items = [];
    for (const row of this.rows()) {
      const usd = row.currency() === 'USD';
      const amount = usd ? toCents(row.amount()) : toAmount(row.amount());
      if (amount === null || Number.isNaN(amount)) continue;
      if (!usd) items.push({ amount });
      else if (rate !== null) items.push({ amount: centsToYen(amount, rate.rate) });
    }
    return calcTotal(items);
  });
  this.totalText = ko.pureComputed(() => formatYen(this.total()));
  this.countText = ko.pureComputed(() => ROWS_TEXT.count(this.rows().length)); // ★ 件数

textInput: amount は、両方向のつながりです。欄で打てば値が変わり、値を変えれば欄が変わります。合計は pureComputed で、中で読んだ observable(行の数・金額・通貨・レート)を Knockout が覚えておき、どれかが変われば計算しなおします。件数(★)も1行。画面を書きかえる行は、JavaScript には1つもありません。

新井JavaScript が 37 行! jQuery の半分です。

古井そのかわり、HTML に 10 か所、式が書いてある。どちらも「書いたもの」だよ。

四

AngularJS:ふつうの値を見張る

AngularJS(書き方 F)は Google の開発者たちが作ったもので、値はふつうのオブジェクトに入れるだけです。HTML の ng-model が値と入力欄を両方向につなぎ、{{ }} が値を文字として出します。

# rows-ng.html(抜粋):ng-model で値と欄を、{{ }} で値と文字をつなぐ
<main … ng-app="rows">
<div ng-controller="RowsController as vm">
<h1 …>経費の明細 <span id="count">{{ vm.countText() }}</span></h1>
<tbody>
  <tr ng-repeat="r in vm.rows track by r.id">
    <td><input … ng-model="r.item"></td>
    <td><input … ng-model="r.amount"></td>
    <td><select … ng-model="r.currency">…</select></td>
    <td><button … ng-click="vm.remove(r)">…</button></td>
  </tr>
</tbody>
<output id="total" …>{{ vm.totalText() }}</output>

# rows-ng.js(抜粋):値は、ふつうのオブジェクトに入れるだけ
angular.module('rows', []).controller('RowsController', ['$http', function ($http) {
  const vm = this;
  let nextId = 1;
  const newRow = () => ({ id: nextId++, item: '', amount: '', currency: 'JPY' });
  vm.max = 10;
  vm.rows = [newRow()];
  vm.rate = null;

  vm.total = () => { … }; // vm.rows を1行ずつ足す(D・E と同じ計算)
  vm.totalText = () => formatYen(vm.total());
  vm.needsReceipt = () => needsReceipt(vm.total());
  vm.countText = () => ROWS_TEXT.count(vm.rows.length); // ★ 件数
  vm.rateText = () => (vm.rate ? ROWS_TEXT.rate(vm.rate.rate, vm.rate.date) : '');

observable のような入れ物がないのに、どうして変わったとわかるのか。AngularJS は、クリックや入力のたびに、画面に書いた式(見張り、watcher)を全部計算しなおし、前回の値と比べます。これをダーティチェックと呼びます。ちがっていたところだけを、画面に書きます。

3つの流派:値の置き場所から画面まで

D Backbone(MVC) Model に入れる set は自分で書く イベント change・update View の関数 画面は自分で書く 書く所は人が決める いつかは道具が知らせる E Knockout(MVVM) observable 画面と両方向 computed 読んだ値を覚える つながった所だけ data-bind 変わる所を道具が知る 細かくつながる F AngularJS ふつうの値 ng-model で両方向 ダーティチェック 見張りを全部比べる ちがう所だけ {{ }}・ng-* 調べて変わりを知る 見張りは行の数だけ増 jQuery では「どこを直すか」を全部、人が覚えていた。3つとも、その一部を道具に移した 移した量と、移し方がちがう。そのちがいが、速さと書き方と、CSP の結果に出る
五

同じ8つの操作

第1回と同じ8つの操作(金額を打つ、行を足す、ドルにする、行を消す)を Playwright でして、そのたびに画面を見ました。

# 第1回と同じ8つの操作(D・E・F は件数を出すところまで書いてから)。比べるため A と C も
  書き方件数のずれ合計のずれ追加のあとのカーソル削除のあと
  D Backbone    0 / 8         0 / 8         新しい行の内容「行を追加」
  E Knockout    0 / 8         0 / 8         「行を追加」BODY
  F AngularJS   0 / 8         0 / 8         「行を追加」BODY
  A jQuery      3 / 8         0 / 8         新しい行の内容「行を追加」
  C React       0 / 8         0 / 8         「行を追加」BODY
# D・E・F は 3 言語とも 0 回。カーソルは D だけが A と同じ(自分で書いたから)

3つとも、件数も合計も一度もずれませんでした(中国語・英語のページでも同じ)。2010年の時点で、第1回の悩みには答えが出ていたことになります。

カーソルの行き先は分かれました。D は jQuery と同じく、行を足すと新しい行へ、消すと「行を追加」へ移ります。これは Backbone がしたのではなく、自分で書いたからです。E と F は React(C)と同じで、何も書かなければ何もしません。

六

1文字で何が書きかわるか

3行の表で 14720 と1文字ずつ打ち、そのたびに画面の変化を MutationObserver で数えました(第1回と同じ測り方)。

# 3行の表で、1行目の金額に 1・4・7・2・0 と打ったとき、1文字ごとの画面の変化(MutationObserver)
  書き方変化の記録中身
  D Backbone    7           合計・件数・レートの文字を入れかえ、注意書きの hidden を付けなおす(変わっていなくても)
  E Knockout    1           合計の文字だけ
  F AngularJS   3 → 1       合計の文字だけ(最初の1文字だけ、欄の class に ng-dirty が付いて 3)
  A jQuery      5           合計と件数の文字を入れかえ
  C React       23          21 は React が name・type を同じ値で付けなおす内部の手順

差がはっきり出ました。D は、変わっていない件数やレートの文字まで毎回入れかえます。renderSummary は「何が変わったか」を知らずに、全部を出しなおすからです。E は合計の文字1つだけ。Knockout は、どの画面の部品がどの値に結ばれているかを覚えているので、変わった値の先だけを書きます。F も合計の文字だけでしたが、こちらは全部の見張りを比べた結果として、そうなっています。

七

行が多いとき

行を 10・1000・5000 にして、1文字打ったときにページの JavaScript が働いた時間と、ページを開いてから行がそろうまでの時間を測りました。比べるため、第1回の A(jQuery)と C(React)も同じ日に測りなおしています。

# 1文字の入力で、ページの JavaScript が働いた時間と、開くまでの時間(3回の中央値。1回は 20 回の中央値)
  書き方1文字の JS(ms)開いてから行がそろうまで(ms)
                10      1000    5000      1000    5000
  D Backbone    0.5     0.8     1.6       741     3,016
  E Knockout    0.4     1.2     3.9       817     2,968
  F AngularJS   1.0     2.9     16.4      970     3,931
  C React       1.8     19.0    83.0      668     2,483
  A jQuery      0.7     11.0    —         9,716   —
  (JS なし)値を入れて描くだけで 1000 行 28.5 ms、5000 行 131.6 ms(ブラウザの仕事)
# F の見張り(watcher)は 10 行で 76、1000 行で 7,006、5000 行で 35,006。1行に 7 つ
# A は行を1つ足すたびに全部の行の名前を付けなおすので、5000 行は測っていない
端末の画面。node keys2.js 5000 の結果。rows-react.html は JS 84.2 ms・描き終わるまで 223.2 ms、rows-bb.html は 1.5 ms・133.7 ms、rows-ko.html は 3.6 ms・105.0 ms、rows-ng.html は 16.3 ms・149.0 ms で watchers 35006
5000 行の表で1文字打つたびの時間を、1回だけ測った画面です(本文の数字は3回の中央値)。AngularJS の見張りは 35,006 個(筆者がクラウド上の Linux で、2026年11月1日に撮影。ウインドウの上の帯は説明のために描き足したもの)

5000 行で1文字打つと、JavaScript は何ミリ秒働いたか

5000 行の表で1文字打つと、JavaScript は何ミリ秒働いたか(実測) D Backbone 1.6 ms E Knockout 3.9 ms F AngularJS 16.4 ms C React(第1回) 83.0 ms (ブラウザが描く分) 132 ms 2010年の3つは、React より速かった。React は打つたびに表を全部計算しなおす この差をどう縮めるかは第8回(再描画のコスト)。それでも React が選ばれた理由は第3回

F の見張りは1行に7つ(3つの入力欄と、4つの読み上げの名前)。5000 行で 35,006 個になり、1文字ごとにそれを全部比べるので 16.4 ms かかりました。それでも E の 3.9 ms、D の 1.6 ms とともに、React の 83 ms より速い。React は打つたびに、5000 行ぶんの画面の形を作りなおして比べるからです(第8回で、ここを縮めます)。

もう1つ、5000 行では値を入れて描くだけで約 132 ms かかりました。これはブラウザの仕事で、どの書き方でも同じです。行が多い画面で効くのは、道具よりも、まず行の数です。なお A は行を1つ足すたびに全部の行の読み上げ名をつけなおすので、1000 行を開くのに 9.7 秒かかりました。

八

いまの CSP の下で

ここまでは守りのヘッダーを外していました。フロントエンド編 第15回の CSP(script-src 'self'、style-src 'self')を付けて開くと、こうなりました。

3つのブラウザの画面。フロントエンド編 第15回の厳しい CSP のまま開き、14720 と打ったあと。D Backbone は表の見出しだけで行が1つもなく、合計は 0。E Knockout は行が1つあり 14720 と入っているが、件数も合計も空で、領収書の注意書きが出ている。F AngularJS は明細 1 件、合計 14,720 円と動いているが、3万円未満なのに領収書の注意書きが出たまま
厳しい CSP のまま開きました。D は行が出ず、E は何もつながらず、F は動くものの、隠すはずの注意書きが出ています(筆者がクラウド上の Linux で、2026年11月1日に撮影。ウインドウの上の帯は説明のために描き足したもの)
# フロントエンド編 第15回の CSP(script-src 'self'、style-src 'self')のまま開き、1行目に 14720 と打った
  書き方エラーどうなったか
  D Backbone      1       行が1つも出ない。_.template が文字から関数を作れない
  E Knockout      1       何もつながらない(Unable to parse bindings)
  F AngularJS     1       動くが、ng-hide の CSS を足せず注意書きが出たまま
  F + ng-csp      0       ng-csp と angular-csp.css を足すと、エラーなし
  A / C           0       動く

D は、行のひな形を作る _.template が、文字から関数を作ろうとして止められました(Backbone 本体には、そういう所は見当たりません)。E は、data-bind の式を関数に変えるところで止まり、何もつながりませんでした。2010年ごろは、HTML や文字に書いた式を、その場で JavaScript にするのがふつうでした。CSP が広く使われる前の書き方です。

F は動きましたが、ng-hide のための CSS を <style> で足せず、3万円未満なのに注意書きが出たままでした。AngularJS は CSP を見越して、ng-csp と別ファイルの angular-csp.css を用意していて、それを足すとエラーなしで動きました。フロントエンド編 第15回で Vue がビルドなしでは動かなかったのと、同じ壁です。

九

大きさと、それぞれの今

# 自分で書いた量と、ブラウザに届く大きさ(app.js と文字の分は、どれも同じなので除いた)
  書き方JS の行HTML の結び目届く大きさgzip
  D Backbone    77        0               127,724 B     44,950 B
  E Knockout    37        10              70,680 B      26,300 B
  F AngularJS   35        17              179,261 B     62,828 B
  A jQuery      73        0               81,575 B      28,787 B
  C React       59        —               226,513 B     70,113 B
# JS の行は空行を除く(説明のコメントもふくむ)。HTML の結び目は data-bind・ng-*・{{ }} の数。D の内訳は jQuery 78,748、Underscore 19,668、Backbone 25,690

E と F は JavaScript が半分になったかわりに、HTML に式が入りました。D は jQuery と Underscore も要るので、3つ合わせて 128 KB。F は1つで 179 KB あり、どれも React(226 KB)よりは小さいです。

  名前最初の公開最新いま
  Backbone    2010年10月1.6.1 (2025年4月)いまも直されている
  Knockout    2010年7月3.5.3 (2026年3月)いまも直されている
  AngularJS   2010年(1.0 は 2012年6月)1.8.3 (2022年4月)2021年12月31日でサポート終了。npm でも非推奨
# 最新の版と日付は npm の記録(2026年11月1日に確認)

AngularJS は 2021年12月31日でサポートが終わり、後を継いだのは、作りなおされた別物の Angular です。Backbone と Knockout は、いまも新しい版が出ています。止まったものと、続いているもの。古い業務アプリを預かったときは、まずどれで書かれているかを確かめてください。

十

自分の PC で

手元に AngularJS の画面があれば、開発者ツールのコンソールで見張りの数を数えられます(ng-app の付いた部品から数えます)。

// AngularJS の画面で:見張り(watcher)の数
angular.element(document.querySelector('[ng-app]')).injector().get('$rootScope').$$watchersCount

CSP の下で文字から関数を作れるかは、どのページでも次の1行で試せます。厳しい CSP のページではエラーになります。

// 文字から関数を作れるか(CSP で 'unsafe-eval' が許されていなければエラー)
new Function('return 1')()

$$ で始まる名前は AngularJS の内部のもので、調べるためだけに使ってください。

結 — 一枚まとめと、次回

  1. 2010年、Backbone・Knockout・AngularJS がほぼ同時に出た。どれも本当の値を画面の外に置く。
  2. Backbone は値の変化をイベントで知らせ、画面は自分で書く。Knockout は値と画面を結び、AngularJS は全部を見張って比べる。
  3. 同じ8つの操作で、件数のずれは3つとも0回。1文字ごとの画面の変化は 7・1・1。
  4. 5000 行で1文字 1.6・3.9・16.4 ms と、React の 83 ms より速い。描くだけで 132 ms かかるのはブラウザの分。
  5. 厳しい CSP では、文字から式を作る書き方が止まる。AngularJS は ng-csp で動き、2021年にサポートを終えた。

新井速くて、ずれなくて、しかも小さい。なんで React に変わったんですか?

古井この表は10行の練習だからね。画面が百も千もの部品になると、両方向のつながりが、どこから来た変更なのかを追いにくくした。

新井つながりすぎて、からまった?

古井次回は歴史②。React が「片方向」を持ちこんでから、Vue、Angular、Svelte、Solid、そしてサーバーで画面を作る話まで。

参考

記録は、筆者がクラウド上の Linux で、2026年11月1日に取ったものです。ページは練習用のサーバー(Node.js 22)から配り、Playwright で動かした Chromium 153.0.8010.12 で操作と測定をしました。D は jQuery 4.0.0・Underscore 1.13.8・Backbone 1.6.1、E は Knockout 3.5.3、F は AngularJS 1.8.3、見た目はどれも Bootstrap 5.3.8 です。時間は、同じ欄に値を入れて input のイベントを送り、戻るまで(JavaScript の時間)と次の描画のあとまでを 25 回測り、最初の 5 回を除いた 20 回の中央値を、さらに3回くり返した中央値です。公開の年は各プロジェクトの記録と参考の資料、最新の版は npm の記録によります。ほかの PC では数字が変わります。

いにしえのフロントエンド入門 フレームワーク編 第2回 — 歴史①:jQuery から MVC へ / 2026.11

← 記事一覧へ