<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>wtrclred (日本語)</title>
    <link>https://wtrclred.vercel.app/ja</link>
    <description>Essays, notes, books, and favorite things by ubugeeei.</description>
    <language>ja</language>
    <atom:link href="https://wtrclred.vercel.app/ja/rss.xml" rel="self" type="application/rss+xml" />
    <lastBuildDate>Mon, 27 Apr 2026 11:12:35 GMT</lastBuildDate>
    <item>
      <title>嫌いなもの，好きなもの</title>
      <link>https://wtrclred.vercel.app/ja/posts/22</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/22</guid>
      <description>ubugeeei が嫌いなものと好きなもの．</description>
      <pubDate>Sun, 19 Apr 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-20T05:33:41.000Z</dc:date>
      <category>personal</category>
      <content:encoded><![CDATA[<p>なぜ嫌いなものを先に書くか．それは人は最後まで読まないからである．</p>
<h2>嫌いなもの</h2>
<ul>
<li><p><a href="https://ja.wikipedia.org/wiki/人間" target="_blank" rel="noopener noreferrer">人間</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/コーヒー" target="_blank" rel="noopener noreferrer">コーヒー</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/手続き" target="_blank" rel="noopener noreferrer">手続き</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/書類" target="_blank" rel="noopener noreferrer">書類</a></p>
</li>
<li><p><a href="https://www.milesdavis.com/albums/kind-of-blue/" target="_blank" rel="noopener noreferrer">Kind of Blue</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/人間関係" target="_blank" rel="noopener noreferrer">人間関係</a></p>
</li>
<li><p>不義理</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/Monster_Energy" target="_blank" rel="noopener noreferrer">Monster Energy</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/クラシック音楽" target="_blank" rel="noopener noreferrer">クラシック音楽</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/社会通念" target="_blank" rel="noopener noreferrer">社会通念</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/猫被り" target="_blank" rel="noopener noreferrer">猫被り</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/法律" target="_blank" rel="noopener noreferrer">法律</a>に反すること</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/私有" target="_blank" rel="noopener noreferrer">私有</a></p>
</li>
<li><p>被害者ぶり</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/親" target="_blank" rel="noopener noreferrer">親</a></p>
</li>
<li><p>仲良しこよし</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/一貫性" target="_blank" rel="noopener noreferrer">一貫性</a>のない言動</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/暴力" target="_blank" rel="noopener noreferrer">暴力</a></p>
</li>
<li><p>綺麗な<a href="https://ja.wikipedia.org/wiki/インターネット" target="_blank" rel="noopener noreferrer">インターネット</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/わかば_%28たばこ%29" target="_blank" rel="noopener noreferrer">わかば</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/社交" target="_blank" rel="noopener noreferrer">社交</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/嘘" target="_blank" rel="noopener noreferrer">嘘</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/営業" target="_blank" rel="noopener noreferrer">営業</a></p>
</li>
<li><p><a href="https://wtrclred.vercel.app/posts/19">要はバランスおじさん</a></p>
</li>
<li><p>酒っぽい<a href="https://ja.wikipedia.org/wiki/酒" target="_blank" rel="noopener noreferrer">酒</a></p>
</li>
<li><p>胸焼けしかねない脂の乗った肉や魚</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ビール" target="_blank" rel="noopener noreferrer">ビール</a></p>
</li>
<li><p>明るい人</p>
</li>
<li><p>クチャラー</p>
</li>
<li><p>同じ皿で箸を突くこと (ほぼ無理に近い)</p>
</li>
<li><p>飲み回し (ほぼ無理に近い)</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/公衆便所" target="_blank" rel="noopener noreferrer">公衆便所</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/地元" target="_blank" rel="noopener noreferrer">地元</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/規律" target="_blank" rel="noopener noreferrer">規律</a></p>
</li>
<li><p>無難</p>
</li>
<li><p>時間的な約束</p>
</li>
<li><p>定型作業</p>
</li>
<li><p>驚き屋</p>
</li>
<li><p>出かけるための準備</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/電話" target="_blank" rel="noopener noreferrer">電話</a></p>
</li>
<li><p>なんとなくオシャレな雰囲気</p>
</li>
<li><p>今の <a href="https://www.yoshiki.net/" target="_blank" rel="noopener noreferrer">YOSHIKI</a></p>
</li>
<li><p>ジャズが好きというとオシャレと言ってくる人</p>
</li>
<li><p>明るいところ</p>
</li>
<li><p>集団に参加すること</p>
</li>
<li><p>ジャズを高貴なものとして捉えている人</p>
</li>
<li><p>シャバさ</p>
</li>
<li><p>不誠実</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/理不尽" target="_blank" rel="noopener noreferrer">理不尽</a></p>
</li>
<li><p>人が多いところ</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/妥協" target="_blank" rel="noopener noreferrer">妥協</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ポリティカル・コレクトネス" target="_blank" rel="noopener noreferrer">ポリコレ</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/リベラル" target="_blank" rel="noopener noreferrer">リベラル</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/詮索" target="_blank" rel="noopener noreferrer">詮索</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/陰口" target="_blank" rel="noopener noreferrer">陰口</a></p>
</li>
<li><p>カッコつけ</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/権威主義" target="_blank" rel="noopener noreferrer">権威主義</a></p>
</li>
<li><p><a href="http://artblakey.com/" target="_blank" rel="noopener noreferrer">Art Blakey</a></p>
</li>
<li><p>手が汚れるザラザラしたグミ</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ルッキズム" target="_blank" rel="noopener noreferrer">ルッキズム</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/いじめ" target="_blank" rel="noopener noreferrer">いじめ</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/無自覚" target="_blank" rel="noopener noreferrer">無自覚</a></p>
</li>
</ul>
<h2>好きなもの</h2>
<ul>
<li><p><a href="https://ja.wikipedia.org/wiki/カモ" target="_blank" rel="noopener noreferrer">鴨</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/コンパイラ" target="_blank" rel="noopener noreferrer">コンパイラ</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/言語処理系" target="_blank" rel="noopener noreferrer">言語処理系</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/コンピュータ" target="_blank" rel="noopener noreferrer">コンピュータ</a></p>
</li>
<li><p>筋の良さ</p>
</li>
<li><p><a href="https://www.ledzeppelin.com/" target="_blank" rel="noopener noreferrer">Led Zeppelin</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/水彩画" target="_blank" rel="noopener noreferrer">透明水彩</a></p>
</li>
<li><p>香ばしさ</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ネコ" target="_blank" rel="noopener noreferrer">猫</a></p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Jazz" target="_blank" rel="noopener noreferrer">Jazz</a>，特に <a href="https://en.wikipedia.org/wiki/Contemporary_jazz" target="_blank" rel="noopener noreferrer">Contemporary Jazz</a></p>
</li>
<li><p><a href="https://www.kurtrosenwinkel.com/" target="_blank" rel="noopener noreferrer">Kurt Rosenwinkel</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/一貫性" target="_blank" rel="noopener noreferrer">一貫性</a></p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Hip_hop" target="_blank" rel="noopener noreferrer">Hip hop</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ナイトクラブ" target="_blank" rel="noopener noreferrer">クラブ</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/たばこ" target="_blank" rel="noopener noreferrer">タバコ</a>，che がベスト</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ウニ" target="_blank" rel="noopener noreferrer">うに</a></p>
</li>
<li><p>インターネット極左</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/最高峰" target="_blank" rel="noopener noreferrer">最高峰</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ファッション" target="_blank" rel="noopener noreferrer">ファッション</a>，<a href="https://ja.wikipedia.org/wiki/モード系" target="_blank" rel="noopener noreferrer">Mode</a> が好き</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/スカート" target="_blank" rel="noopener noreferrer">スカート</a>をはくこと</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ワンピース" target="_blank" rel="noopener noreferrer">ワンピース</a>を着ること</p>
</li>
<li><p>良い話</p>
</li>
<li><p><a href="https://united-tokyo.com/" target="_blank" rel="noopener noreferrer">UNITED TOKYO</a></p>
</li>
<li><p><a href="https://www.rust-lang.org/" target="_blank" rel="noopener noreferrer">Rust</a></p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Piano_trio" target="_blank" rel="noopener noreferrer">Piano Trio</a></p>
</li>
<li><p><a href="https://www.johncoltrane.com/" target="_blank" rel="noopener noreferrer">John Coltrane</a></p>
</li>
<li><p>昔の <a href="https://www.yoshiki.net/" target="_blank" rel="noopener noreferrer">YOSHIKI</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/くねくね" target="_blank" rel="noopener noreferrer">クネクネ</a></p>
</li>
<li><p>筋の良い研究者</p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Atonality" target="_blank" rel="noopener noreferrer">Atonal</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/音楽理論" target="_blank" rel="noopener noreferrer">音楽理論</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/風刺" target="_blank" rel="noopener noreferrer">風刺</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/概念" target="_blank" rel="noopener noreferrer">概念</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/批判" target="_blank" rel="noopener noreferrer">批判</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/感覚" target="_blank" rel="noopener noreferrer">感覚</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/思考" target="_blank" rel="noopener noreferrer">思考</a></p>
</li>
<li><p>Hard Fusion</p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Taxonomy" target="_blank" rel="noopener noreferrer">Taxonomy</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/デザイン" target="_blank" rel="noopener noreferrer">デザイン</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ピアス" target="_blank" rel="noopener noreferrer">ピアス</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/インテリア" target="_blank" rel="noopener noreferrer">インテリア</a></p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Nihilism" target="_blank" rel="noopener noreferrer">Nihilism</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/インターネット" target="_blank" rel="noopener noreferrer">インターネット</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ウェブサイト" target="_blank" rel="noopener noreferrer">Web サイト</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/デパス" target="_blank" rel="noopener noreferrer">デパス</a></p>
</li>
<li><p><a href="https://www.redbull.com/jp-ja" target="_blank" rel="noopener noreferrer">レッドブル</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ブロマゼパム" target="_blank" rel="noopener noreferrer">ブロマゼパム</a></p>
</li>
<li><p><a href="https://www.bradmehldaumusic.com/" target="_blank" rel="noopener noreferrer">Brad Mehldau</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/パンケーキ" target="_blank" rel="noopener noreferrer">パンケーキ</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/油彩" target="_blank" rel="noopener noreferrer">油絵</a></p>
</li>
<li><p>セブンイレブンのれんこんのり塩チップス</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/食肉" target="_blank" rel="noopener noreferrer">生に近い肉</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/麺" target="_blank" rel="noopener noreferrer">麺は硬ければ硬い程よい</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/白米" target="_blank" rel="noopener noreferrer">白米</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/チョコレート" target="_blank" rel="noopener noreferrer">チョコレート</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ランダムネス" target="_blank" rel="noopener noreferrer">ランダム</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/哲学" target="_blank" rel="noopener noreferrer">哲学</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/主張" target="_blank" rel="noopener noreferrer">主張</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/造語" target="_blank" rel="noopener noreferrer">造語</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/覚悟" target="_blank" rel="noopener noreferrer">覚悟</a></p>
</li>
<li><p>思い切りの良さ</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/実力" target="_blank" rel="noopener noreferrer">実力</a></p>
</li>
<li><p>よく作り込まれたもの</p>
</li>
<li><p><a href="https://vuejs.org/" target="_blank" rel="noopener noreferrer">Vue.js</a></p>
</li>
<li><p>殺伐としたインターネット</p>
</li>
<li><p>ネタツイ</p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Art" target="_blank" rel="noopener noreferrer">Art</a></p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Generative_art" target="_blank" rel="noopener noreferrer">Generative art</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/抽象絵画" target="_blank" rel="noopener noreferrer">抽象画</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/印象派" target="_blank" rel="noopener noreferrer">印象派</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/入れ墨" target="_blank" rel="noopener noreferrer">タトゥー</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/ギャグ" target="_blank" rel="noopener noreferrer">ギャグ</a></p>
</li>
<li><p>絵を見ること</p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Album_cover" target="_blank" rel="noopener noreferrer">アルバムのアートワーク</a>を見ること</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/クレープ" target="_blank" rel="noopener noreferrer">クレープ</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/照明" target="_blank" rel="noopener noreferrer">照明</a>が暗いところ</p>
</li>
<li><p>複雑なもの</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/写実主義" target="_blank" rel="noopener noreferrer">写実主義</a></p>
</li>
<li><p>美しいもの</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/景観" target="_blank" rel="noopener noreferrer">景観</a>の良い街</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/景観" target="_blank" rel="noopener noreferrer">景観</a>の良い路地</p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/散歩" target="_blank" rel="noopener noreferrer">散歩</a></p>
</li>
<li><p><a href="https://ja.wikipedia.org/wiki/デッサン" target="_blank" rel="noopener noreferrer">鉛筆デッサン</a></p>
</li>
<li><p>仕組み</p>
</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>営利企業こそ，OSS の作り手に金を払え</title>
      <link>https://wtrclred.vercel.app/ja/posts/21</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/21</guid>
      <description>OSS を使って売上を作る企業が，イベントだけでなく作者，メンテナ，コアコントリビュータへ分散的にお金を払うべきだ，という話．</description>
      <pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-17T08:53:41.000Z</dc:date>
      <category>oss</category>
      <category>philosophy</category>
      <category>sponsorship</category>
      <category>web</category>
      <content:encoded><![CDATA[<blockquote>
<p>注意:
この記事は私個人の意見であり，所属企業を代表するものではない．
会社のブログは現在準備中で，現時点では適切な公開場所がなかったこと，また個人的な問題意識に基づく話でもあることから，今回は個人ブログに書いている．また，特定の会社や個人を責めるための文章でもない．
ただ，営利企業として OSS を使っているなら，その作者，メンテナ，コアコントリビュータへもう少しお金の流れを作った方がよいのではないか，という話である．</p>
</blockquote>
<h2>序</h2>
<p>OSS の作者，メンテナ，コアコントリビュータに対する報酬が不十分である，という話はたびたび話題になる．あるライブラリのメンテナが疲弊している，重要なパッケージが少人数に支えられている，世界中の企業が依存しているにもかかわらず，その人の生活はまったく安定していない．そういう話が流れてくるたびに，インターネットは少しざわつく．</p>
<p>もちろん，OSS は単純な雇用契約ではない．コードを公開した人が，自動的に利用者全員から対価を受け取る権利を持つ，という単純な話でもないし，利用者が全員，何かに強制的に課金されるべきだと言いたいわけでもない．</p>
<p>ただし，営利企業については少し事情が違う．</p>
<p>私たちは OSS を使ってプロダクトを作り，開発速度を上げ，品質を上げ，運用し，売上を作っている．そしてその売上の一部は，給料や利益として私たちの手元に来ている．</p>
<p>それなら，OSS を利用して収益を上げている企業が，その作者，メンテナ，コアコントリビュータに対して資金を還元することは，自然なことではないか．</p>
<h2>Web フロントエンドは特にそうである</h2>
<p>私は普段 Web フロントエンドの領域に関わっているので，この話をどうしてもその文脈で見てしまう．</p>
<p>現在の Web フロントエンド開発では，OSS は個別の便利な道具というより，日々の開発と運用の前提になっている．
開発環境を整えるとき，品質を担保するとき，プロダクトを届けるとき，その多くの場面で OSS に触れている．</p>
<p>私たちはそれらを組み合わせ，自社の事情に合わせて運用し，開発基盤として利用している．</p>
<p>もちろん，自社で書いたコードもあるし，設計も運用もプロダクト理解もあるので，そこを軽く見るつもりはない．</p>
<p>しかし，その土台の多くは，自社の外にいる作り手によって支えられている．
しかもその多くは，直接の雇用関係も契約関係もない人たちが，どこかの時間を使って作り，直し，リリースしている．</p>
<p>OSS は抽象的な公共財のように見えることがある．しかし実際には，その向こうに作者がいて，メンテナがいて，レビューし，方針を決め，壊れたものを直すコアコントリビュータがいる．支払うべき対象を考えるとき，私はこの人たちの存在をもう少し前に出したい．</p>
<p>この状態で，企業が OSS の作り手にお金を払うことを特別な善行のように扱うのは，少し不自然だと思う．むしろ，通常の費用に近い．サーバー代を払い，SaaS に払い，デザインツールに払い，採用媒体に払い，イベントにスポンサーする．</p>
<p>ならば，依存している OSS の作り手にも支払うべきである．</p>
<h2>イベントへの支援に比べて，作り手への支援は少ない</h2>
<p>日本の技術コミュニティでは，イベントスポンサーを多く見かけるし，これは重要なことだと思う．会場費や配信費が出て，登壇者や参加者の体験が良くなり，企業とコミュニティの接点も生まれる．イベントスポンサーは文化を大きく支えている．</p>
<p>ただ，イベントにロゴを出す企業の数に比べると，OSS の作者，メンテナ，コアコントリビュータに対するスポンサーはまだ少ないように感じている．</p>
<p>イベントは目に見えやすい．ロゴが出て，会場にブースが出て，採用広報にもつながり，社内説明もしやすい．一方で，OSS の作り手の仕事は見えにくい．issue を読み，再現コードを書き，breaking change を避け，依存関係を更新し，脆弱性を直し，リリースノートを書き，ユーザーからの繊細な不満を受け止める．</p>
<p>これは派手な仕事ではなく，外部からも見えにくい．しかし，この地道な作業が止まると，ある日突然こちらの仕事も止まる．イベントスポンサーが不要だと言いたいのではないし，むしろ続いてほしい．ここで言いたいのは，支援先がイベントに寄りがちであるなら，OSS を作り，維持している人にももう少し支援が向いてよいのではないか，ということである．</p>
<h2>メンテナは大企業の中の人だけではない</h2>
<p>Meta，Vercel，Cloudflare のような企業には，OSS 活動を仕事として行っている人たちがいる．それは素晴らしいことだし，企業が OSS を戦略の中心に置き，人を雇い，開発を進めることには大きな意味がある．</p>
<p>しかし，OSS はそれだけで成り立っているわけではない．</p>
<p>大きな企業に所属していないメンテナもたくさんいる．個人でやっている人もいれば，小さな会社で働きながら夜や休日にやっている人もいるし，自分のプロダクトではないにもかかわらず，世界中のユーザーのために，結果として大きな責任を引き受けている人もいる．</p>
<p>実際に，私が日常的に利用しているいくつかの OSS も，大きな企業に所属していない開発者によって作成，またはメンテナンスされている．</p>
<p>私たちは，OSS を見るときに，有名な企業の名前を思い浮かべがちである．しかし，実際の依存関係の奥には，名前も顔も知らない個人がいる．その人が大企業に雇われているとは限らないし，会社の業務時間で直しているとも，生活に余裕があるとも限らない．それでも私たちは，その人たちの作ったものを使って売上を作っている．この非対称性を，もう少し直視した方がよい．</p>
<h2>個人スポンサーは意味がある，でも足りない</h2>
<p>私は GitHub Sponsors で，ありがたいことに 20 前後の個人スポンサーから支援を受けている．</p>
<p>これは大きな支えになっている．活動を見てくれている人がいるという実感があるし，金銭的にも少し余裕ができる．</p>
<p>だから，個人スポンサーに意味がないとはまったく思っていない．むしろ大いに意味があり，個人が個人に対して小さく支援する文化は，もっと広がってよい．</p>
<p>ただ，現実問題として，個人の小口支援だけで生活していくのは難しい．</p>
<p>数百円，数千円の支援が積み重なることは尊い．しかし，生活費，税金，保険，家賃，機材，時間の確保まで考えると，それだけで継続的な OSS メンテナンスやコアな開発参加を成立させるのは簡単ではない．</p>
<p>ここで企業による支援が必要になる．</p>
<p>個人が払えない金額でも，企業にとっては小さな予算であることがある．個人にとって大きな数万円が，企業にとっては一回の会食，一つの SaaS，一つの採用施策に近い扱いになることがある．その差は大きく，そこを活かすべきだと思う．</p>
<h2>ただし，大口の継続支援には危うさがある</h2>
<p>ここは重要だと思っている．</p>
<p>相当な覚悟がない限り，特定の一社が，一人の作者やメンテナ，コアコントリビュータに対して，継続的に大きな monthly 支援をするべきではない．</p>
<p>一見すると，大きな継続支援は望ましいことに見える．もちろん，安定した資金が入ること自体は良いし，受け取る側にとっても助かる．</p>
<p>しかし，その支援に生活が寄りすぎると，取りやめられたときのダメージが大きい．</p>
<p>会社の方針が変わる，担当者が異動する，景気が悪くなる，予算が切られる，その OSS を使わなくなる．企業側では起こり得る話である．しかし受け取る側にとっては，生活や活動計画が崩れる話になる．</p>
<p>だから，企業による OSS 支援は，集中した太い一本の柱よりも，細い柱がたくさんある状態の方が望ましいと思う．</p>
<p>単発のスポンサーが，たくさんの企業から，分散的に行われる．ある企業が一度支援し，別の企業が次に支援する．年に一度でも，四半期に一度でもよいし，プロジェクトのリリースや，自社で大きく助けられたタイミングでもよい．</p>
<p>一社の美談にせず，一人の作り手を一社に依存させず，企業群全体で，広く薄く，しかし個人スポンサーよりは大きな金額で支える．</p>
<p>この形の方が，OSS の経済としては健全だと思う．</p>
<h2>企業は単発で支援すればよい</h2>
<p>企業が OSS の作り手にお金を払うというと，すぐに大きな制度や継続契約を想像してしまうかもしれない．</p>
<p>しかし，最初から立派な制度にしなくてもよい．</p>
<p>今月，自社でよく使っているライブラリに払う．大きな移行で助けられたツールに払う．issue 対応が丁寧だったメンテナや，重要な設計判断を支えているコアコントリビュータ，依存関係の奥にいる個人に払う．リリースのたびに少し払う．</p>
<p>そのような単発の支援でよいと思う．</p>
<p>企業としては少額でも，受け取る個人にとっては大きい金額がある．たとえば 5 万円，10 万円，15 万円くらいの支援は，企業の予算としては過大ではないかもしれないが，個人の作者，メンテナ，コアコントリビュータにとっては，大きな意味を持つ金額になる．</p>
<p>そして，それが一社から継続的に来るのではなく，複数の企業から不定期に来るなら，依存の形も少し柔らかくなる．</p>
<p>もちろん，経理処理は煩雑かもしれないし，稟議も必要かもしれない．受け取り口が GitHub Sponsors なのか Open Collective なのか，請求書が必要なのか，社内規程に合うのか，さまざまな論点があると思う．</p>
<p>それでも，取り組んだ方がよい．</p>
<p>手続きの煩雑さを理由に見送るには，私たちは OSS に依存しすぎている．</p>
<h2>所属企業で話したこと</h2>
<p>ここで少し，自分の所属企業の話をする．</p>
<p>私は普段，株式会社メイツという営利企業でチーフエンジニアを務めている．</p>
<p>当然，仕事では OSS をたくさん利用している．Web フロントエンドでも，バックエンドでも，開発基盤でも，OSS はあらゆる場所にあり，その上でプロダクトが作られ，売上が生まれ，そのうちのいくらかは給料として私に振り込まれる．</p>
<p>この構図を考えると，少なくとも私個人としては，企業として OSS の作り手にお金を払うべきだという気持ちが強い．</p>
<p>ただ，この記事は会社を代表するものではなく，会社の公式見解でもない．ここはきちんと分けておきたい．</p>
<p>そのうえで，私はこの話を社内に持ち出した．まずは提案した本人が動くべきだと思ったからである．</p>
<p>「OSS に支えられているのだから，会社としてもその作者やメンテナ，コアコントリビュータに不定期に単発でスポンサーできないか」</p>
<p>要約すると，こういう相談をした．ありがたいことに，社内のメンバーは賛同してくれた．</p>
<p>これは，過度に美談として書きたいわけではない．会社として巨大な基金を作ったわけでもないし，世界を変える制度が今日から始まるわけでもない．金額も，会社としては少額の範囲になると思う．</p>
<p>ただ，それでも始めることに意味があると思っている．</p>
<p>まず自分のいる場所でやる．そして，できれば他の企業も巻き込む．一社の立派な取り組みにするのではなく，多くの企業が自然に取り組むことにする．</p>
<p>私はそういう方向に進んでほしいと思っている．</p>
<h2>これは善行というより，支払いである</h2>
<p>OSS 支援は，どうしても「良いことをしている」という印象が出やすく，スポンサーした企業が偉い，支援した人が優しい，という話になりやすい．</p>
<p>もちろん，そういう側面もあるし，支援するのは良いことだ．</p>
<p>しかし，企業に関しては，もう少し冷静に捉えてもよいと思う．</p>
<p>自分たちが使っているものの作り手に払う．自分たちの利益に貢献しているものの維持者に払う．自分たちの開発速度を支えている人に払う．</p>
<p>これは善行というより，通常の支払いである．</p>
<p>むしろ，これまで十分に支払ってこなかった状態の方が不自然だった，くらいに考えてもよいかもしれない．</p>
<p>何かを無料で使えることと，その維持にお金がかかっていないことは違う．ライセンス上は無料で使えることと，経済的にただ乗りし続けてよいことも違う．</p>
<p>OSS は無料で使えることが多い．だからこそ，払える側が自発的に払う必要がある．</p>
<p>特に，OSS を使って収益を上げている企業はそうだ．</p>
<h2>結論</h2>
<p>営利企業こそ，OSS の作り手に金を払え．</p>
<p>これは，企業を責めたい言葉ではなく，自分たちの仕事の土台を，もう少し正しく見るための言葉である．</p>
<p>私たちは OSS を使い，OSS によって開発速度を得て，品質を得て，売上を作っている．</p>
<p>なら，その一部を OSS の作者，メンテナ，コアコントリビュータに戻した方がよい．</p>
<p>イベントスポンサーも続けばよいし，個人スポンサーも広がればよいし，大企業が OSS 開発者を雇う流れも続けばよい．</p>
<p>そのうえで，もっと多くの営利企業が，依存している OSS の作者，メンテナ，コアコントリビュータに対して，単発で，分散的に，個人スポンサーより少し大きな金額を払うようになってほしい．</p>
<p>一社が過度に前面に出るのではなく，多くの会社が自然に支払う．一人の作り手を一社に寄せるのではなく，たくさんの企業で支える．採用広報の飾りではなく，開発費用の一部として扱う．</p>
<p>私は，この方向に OSS の経済が少しでも進んでほしいと思っているし，私も，自分のいる場所から，できるだけその流れを作っていきたい．</p>
]]></content:encoded>
    </item>
    <item>
      <title>若者よ，インターネットを荒らせ</title>
      <link>https://wtrclred.vercel.app/ja/posts/20</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/20</guid>
      <description>嘘をつかず，間違えたら謝り，それでも主張を持ち，争い，磨くためのインターネット論．</description>
      <pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-15T11:10:37.000Z</dc:date>
      <category>philosophy</category>
      <content:encoded><![CDATA[<blockquote>
<p>注意: ここでいう「荒らせ」は，誹謗中傷しろ，デマを流せ，人を傷つけろ，という意味ではない．利害関係や政治性や人の生存に関わるものを，軽く扱えという話でもない．むしろ逆で，嘘を言うのは良くないし，間違っていれば謝ればいい．その上で，主張を持て，争え，磨け，という話である．</p>
</blockquote>
<h2>序</h2>
<p>私は2000年生まれのソフトウェアエンジニアだ．インターネットの先輩たちが，ブログで殴り合い，Twitterでプロレスをし，掲示板やコメント欄で長文を書き，勉強会で思想をぶつけ，時にはかなり面倒くさい人間になっていく様子を，横目で見てきた世代でもある．そして俺は，3DSで2ちゃんねるを見て育った．</p>
<p>もちろん，昔のインターネットを無邪気に美化したいわけではない．普通に最悪なものも多かったし，人を傷つける言葉も，雑な決めつけも，笑えない攻撃もあった．あれをそのまま復刻しようと言いたいわけではない．ただ，一つ気がかりなことがある．<strong>私の世代のインターネッタラーは，大人しすぎるのではないか．</strong></p>
<h2>大人しさは美徳だが，限界でもある</h2>
<p>喧嘩は良くない．お互いにリスペクトを持つべきだ．誹謗中傷をしてはならないし，相手の属性や生存に関わることをゲームの駒にしてはいけない．これらは全部正しいし，かなり大事なことだと思っている．</p>
<p>しかし，そこから「だから主張を持つな」にはならない．「だから物議を醸すな」にはならない．「だから何も言わずに，安全な空気の中で，薄い同意だけを交換していろ」にはならない．リスペクトとは衝突を避けることではなく，相手を人間として扱った上で，なお主張をぶつけることだ．</p>
<p>何も言わないことが常に優しさになるわけでもない．主張を避け続ける場では，間違った前提も，弱い設計も，退屈な文化も，そのまま残る．インターネットがずっと無風なら，それは平和なのではなく，単に換気されていないだけかもしれない．</p>
<h2>嘘をつくな，でも間違えろ</h2>
<p>ここは分けて考えたい．嘘を言うのは良くない．知っていて誤情報を流すこと，確認していないことを断言すること，人を陥れるために事実をねじ曲げることは，ただ悪い．</p>
<p>一方で，間違えることはある．強い主張をすれば間違える可能性は上がるし，新しい概念を出せば粗くなる．誰かに反論すれば読み違えることもあるし，議論の途中で自分の前提が崩れることもある．そのときは謝ればいいし，訂正すればいいし，撤回すればいい．次から少しうまくやればいい．</p>
<p>間違える可能性があるから何も言わない，という態度は一見すると誠実に見えるが，それを突き詰めると何も世界に出せなくなる．嘘をつくな．でも，間違えることまで恐れすぎるな．間違えた主張は，謝罪と訂正によって磨ける．最初から存在しない主張は，何にもならない．</p>
<h2>プロレスには技術がいる</h2>
<p>私は，もっとプロレスをしてほしいと思っている．ただし，プロレスは殴り合いではない．高度な形式であり，受け身があり，間合いがあり，観客がいて，相手への信頼がある．インターネット上のプロレスも同じで，人格ではなく主張を殴り，相手の強い形を捉えてから反論し，煽るなら逃げ道も残し，比喩で笑わせても事実は雑にしない，という技術がいる．</p>
<p>間違えたら謝ること，相手が本当に傷ついているなら止まることも含めて，それらができないなら，ただの攻撃になる．それはプロレスではなく，素人がリングに刃物を持ち込んでいるだけである．危ないし，ださい．</p>
<p>だからこそ，技術を育てる必要がある．強い言葉を使うなら強い倫理も持て，物議を醸すなら物議の後始末も引き受けろ，人を集める言葉を使うなら人が集まった後の温度管理まで考えろ，という話だ．荒らすとは場を壊すことではない．場に流れを作ることだ．</p>
<h2>主張を持て</h2>
<p>若い世代は，たぶん失敗のリスクをよく知っている．スクショは残るし，発言は検索されるし，勤務先も見られる．文脈は切り取られ，誰かの怒りは知らないところで増幅される．だから大人しくなるのはわかる．私もわかる．インターネットは，昔よりずっと社会になった．</p>
<p>社会になったインターネットで暴れることは，単にハンドルネームで騒ぐことより重い．それでも，主張は持った方がいい．この技術はこうあるべきだ，この文化はつまらない，この設計は弱い，この流行は変だ，この言葉は便利すぎて危ない，このコミュニティはもっと良くできる．そういう主張を，もっと出した方がいい．</p>
<p>もちろん根拠は要るし，誠実さも要るし，反論を受ける覚悟も要る．でも，主張がなければ議論は始まらない．議論が始まらなければ何も深まらないし，何も深まらなければ世界は変わらない．</p>
<h2>争い，磨け</h2>
<p>争いは，それ自体が悪なのではない．悪いのは，人を壊す争いであり，属性を殴る争いであり，嘘で相手を潰す争いであり，取り返しのつかないものを軽いノリで燃やす争いである．</p>
<p>そうではなく，主張と主張をぶつける争いがある．概念と概念をぶつける争いがあり，設計思想と設計思想をぶつける争いがある．それは必要だ．衝突しない主張は丸く見えるかもしれないが，丸いだけの石は切れない．どこかで削られ，欠け，研がれないと，道具にはならない．</p>
<p>主張も同じだ．反論されて弱いところが見え，問い返されて前提が見え，笑われて過剰な自意識が落ち，誰かに刺さって初めて自分の言葉の刃の向きがわかる．だから，争え．そして磨け．勝つためだけに争うな．燃やすためだけに荒らすな．自分が正しいと証明するためだけに長文を書くな．世界を少し変えるために争え．自分の主張をより良くするために荒らせ．</p>
<h2>結論</h2>
<p>若者よ，インターネットを荒らせ．ただし，嘘をつくな．人を壊すな．生存や属性をゲームにするな．間違えたら謝れ．訂正しろ．撤回しろ．それでも主張を持て．</p>
<p>もっと物議を醸していい．もっとプロレスをしていい．もっと議論を深めていい．大人しくしているだけでは，世界はきれいにならない．ただ静かになるだけだ．</p>
<p>静かなインターネットは，一見，成熟して見える．しかし本当に必要なのは，無風の成熟ではなく，よく管理された乱流である．主張を持て．争え．磨け．そして，できれば少しだけ，世界を変えろ．</p>
]]></content:encoded>
    </item>
    <item>
      <title>要はバランスおじさんが嫌い</title>
      <link>https://wtrclred.vercel.app/ja/posts/19</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/19</guid>
      <description>議論を止める中立ポーズと，主張を持たないことの誠実さについて．</description>
      <pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-15T10:32:32.000Z</dc:date>
      <category>philosophy</category>
      <content:encoded><![CDATA[<blockquote>
<p>注意:
ここでいう「おじさん」は年齢でも性別でもない．
おばさんも若者も含む．
なんなら自分の中にもたまにいる．
これは属性ではなく，議論中に発生する態度の名前である．</p>
</blockquote>
<h2>序</h2>
<p>私は「要はバランス」という言葉があまり好きではない．</p>
<p>もちろん，バランスそのものが嫌いなわけではない．
品質と速度，自由と規律，安全性と機動力，理想と現実．
この手の議論は，だいたい最後にはバランスへ行き着く．</p>
<p>だからこそ，議論の途中で「要はバランス」と言うことは，多くの場合，何も言っていないに等しい．</p>
<p>料理中の人に「要は火加減」と言うようなものだ．
正しい．
正しすぎる．
そして，誰の手元も一ミリも助けていない．</p>
<p>強火なのか弱火なのか．
今なのか後なのか．
焦げ目をつけたいのか，中まで火を通したいのか．
そこを話している最中に「要は火加減」と言われても，こちらは「はい」としか言えない．
そして「はい」と言った瞬間に，議論だけが死ぬ．</p>
<h2>主張がないなら，主張がないと言えばいい</h2>
<p>主張を持っていないこと自体は悪くない．</p>
<p>情報が足りないなら「情報が足りない」と言えばいい．
判断基準が見えていないなら「判断基準が見えていない」と言えばいい．
まだ考えられていないなら「私はまだ主張を持てない」と言えばいい．</p>
<p>それは誠実である．</p>
<p>しかし，主張がないことを「自分は一段上から客観的に見えている」というポーズに変換し始めると，話が変わる．</p>
<p>両サイドを冷笑し，
「まあまあ，みんな熱くならずに」と言い，
あたかも自分だけが大人であるかのように振る舞う．</p>
<p>これは中立ではない．
かなり濃いめのエゴである．</p>
<p>ポライトな見下しは，見下しである．
砂糖をまぶした毒は，お菓子売り場に置くと余計に危ない．</p>
<h2>意見を言えないことと中立は違う</h2>
<p>ここは強調しておきたい．</p>
<p><strong>意見を言うことができないこと</strong> と，<strong>中立的であること</strong> は違う．</p>
<p>知識が足りない．
責任を持てない．
場の力学が怖い．
まだ言語化できない．
そういう理由で，意見を言えない瞬間はある．</p>
<p>それは状態である．
限界である．
人間なので普通にある．</p>
<p>しかし，中立は状態ではなく立場である．
両側の主張を見た上で，いまは判断を保留する．
論点を整理する．
判断に必要な情報を分ける．
そういう役割を引き受けることだ．</p>
<p>意見を言えないことを，中立であるかのように扱ってはいけない．
それは「まだ立場を置けない」を，「私は両側を超越している」にすり替える行為である．</p>
<p>このすり替えが起きると，無主張がなぜか道徳的優位になる．
議論に参加している人たちが未熟で，自分だけが成熟しているように見える．</p>
<p>それは中立ではない．
参加できていないことのラッピングである．</p>
<h2>議論は喧嘩ではない</h2>
<p>「要はバランスおじさん」は，しばしば議論を喧嘩として見る．</p>
<p>Aさんが強く言う．
Bさんも強く返す．
反例が出る．
前提が掘られる．
論点が増える．</p>
<p>すると，その場を「荒れている」と見て，止めに入る．</p>
<p>もちろん，人格攻撃やハラスメントが起きているなら止めた方がよい．
それは議論ではなく，人が壊れる方向へ進んでいるだけだからだ．</p>
<p>しかし，意見がぶつかっていること自体は悪ではない．
むしろ，両サイドがきちんと主張を出し合うことで，初めて見える地形がある．</p>
<p>速度を重く見る人がいる．
安全性を重く見る人がいる．
運用コストを言う人がいる．
ユーザー影響を言う人がいる．</p>
<p>この状態は，単に「対立している」のではない．
論点の地図が増えている．
価値の軸が見えてきている．
議論が深まるとは，たぶんこういうことだ．</p>
<p>そこで「要はバランス」と言って止めてしまうと，せっかく生えかけた軸が折れる．
あとには，何となく大人っぽい空気だけが残る．</p>
<p>その空気は議事録に書けない．
実装にも設計にも意思決定にも使えない．
だが本人の自己像だけは少し温まる．</p>
<p>省エネ暖房としては優秀かもしれない．
世界への貢献としてはかなり怪しい．</p>
<h2>「両方大事」は入口であって出口ではない</h2>
<p>「両方大事」も同じだ．</p>
<p>もちろん両方大事なのだ．
大事だから議論している．
片方がどうでもいいなら，そもそも議論にならない．</p>
<p>重要なのは，そこから先である．</p>
<ul>
<li><p>今回はどちらを優先するのか</p>
</li>
<li><p>どの条件なら優先順位が反転するのか</p>
</li>
<li><p>誰がそのコストを払うのか</p>
</li>
<li><p>失敗したとき，どちらの失敗の方が回復しやすいのか</p>
</li>
<li><p>原則の話なのか，今回だけの例外なのか</p>
</li>
</ul>
<p>この粒度まで降りて初めて，「バランス」は作業になる．
それ以前の「両方大事」は，ただの入場券である．
入場券を握りしめてステージ中央に立たれても困る．
ライブはまだ始まっていない．</p>
<p>何かを選ぶとは，何かを選ばないことでもある．
どちらかを重く見るとは，もう片方の不満を引き受けることでもある．
バランスを取るとは，その負債の置き場所を決めることでもある．</p>
<p>それを言わない「バランス」は，かなり高級な無責任である．</p>
<h2>止めるなら，理由を持て</h2>
<p>議論を止めること自体が悪いわけではない．</p>
<p>時間がないこともある．
参加者が疲れていることもある．
前提情報が足りないこともある．
同じ論点を三周していることもある．</p>
<p>ただし，止めるなら理由を持つべきである．</p>
<blockquote>
<p>今は判断材料が足りないので，ここで止めたい</p>
</blockquote>
<blockquote>
<p>論点が三つに分かれたので，いったん分解したい</p>
</blockquote>
<blockquote>
<p>このままだと人格評価に寄りそうなので，発話の対象を案に戻したい</p>
</blockquote>
<p>こういう止め方なら，議論に貢献している．
議論を殺しているのではなく，次に進めるために形を変えている．</p>
<p>一方で，</p>
<blockquote>
<p>要はバランスだよね</p>
</blockquote>
<p>だけで止めるのは，結論のふりをした停止命令である．</p>
<p>しかも「バランス」は善良そうに見える．
だから反論する側が，まるで過激派のように見える．</p>
<p>これはズルい．
かなりズルい．
そして本人にズルをしている自覚がないことが多い．
その無自覚さまで含めて，厄介である．</p>
<h2>結論</h2>
<p>私が嫌いなのは，バランスではない．</p>
<p>「要はバランス」という言葉が，主張しないための免罪符として使われることが嫌いなのだ．</p>
<p>主張する人は，間違えるリスクを負っている．
反論する人も，嫌われるリスクを負っている．
具体案を出す人は，具体的に批判されるリスクを負っている．</p>
<p>その横から，何も出さずに「要はバランス」とだけ言うのは，手ぶらで鍋パに来て「味って大事だよね」と言うようなものである．</p>
<p>大事だよ．
大事だから今みんなで作っているんだよ．
せめてネギを切れ．</p>
<p>主張があるなら主張する．
整理するなら整理する．
止めるなら理由を持って止める．
まだ主張がないなら，主張がないと言う．</p>
<p>議論は，冷やせば成熟するわけではない．
熱があるからこそ，出てくる形がある．</p>
<p>火傷しないようにはしたい．
でも，火を見た瞬間に消火器を構える人だけで世界を運用すると，たぶん夕飯がずっと生で出てくる．</p>
]]></content:encoded>
    </item>
    <item>
      <title>corsa-bind: 言語処理系オーケストレーションという考え方</title>
      <link>https://wtrclred.vercel.app/ja/posts/17</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/17</guid>
      <description>tsgoのstdio APIを，単なる関数呼び出しではなく，長生きする言語処理系サーバー群として束ねる．corsa-bindを作りながら考えている，言語処理系オーケストレーションという考え方を書く．</description>
      <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-17T08:53:41.000Z</dc:date>
      <category>typescript</category>
      <category>language</category>
      <category>compiler</category>
      <category>architecture</category>
      <category>language-tools</category>
      <category>rust</category>
      <category>oss</category>
      <category>orchestration</category>
      <content:encoded><![CDATA[<h2>序</h2>
<p>TypeScriptのネイティブ実装である<code>typescript-go</code>，つまり<code>tsgo</code>を眺めていると，かなり自然に発想が変わる瞬間がある．</p>
<p>最初は「<code>tsc</code>が速くなる」という話に見える．
実際，<a href="https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/" target="_blank" rel="noopener noreferrer">TypeScript 6.0のアナウンス</a>でも，TypeScript 6.0は現在のJavaScript実装からTypeScript 7.0以降のGo実装へ移るためのbridge releaseだと説明されているし，Go実装はnative codeとshared-memory multi-threadingを使う新しいcodebaseとして説明されている．</p>
<p>しかし<code>tsgo</code>を外から利用する側のコードを書いていると，関心は少しずつ別の場所へ移る．</p>
<p>僕が欲しいのは，単に一回の<code>tsgo --noEmit</code>を速くすることだけではない．
エディタ，リンター，コード生成，リファクタリング，AIエージェント，ビルドサーバーのような複数の利用者が，同じTypeScript program graphとcheckerの周りに集まり，必要な質問だけを繰り返し投げられる形である．</p>
<p>このとき僕には，<code>tsgo</code>がCLIというより，<strong>言語処理系サーバー</strong> に見えてくる．</p>
<p>そして僕は<a href="https://github.com/ubugeeei/corsa-bind" target="_blank" rel="noopener noreferrer">corsa-bind</a>で，この見え方を実装へ押し込んでいる．
<code>tsgo</code>をforkして中身を書き換えるのではなく，upstreamが用意するstdio API / LSPの境界に乗り，その外側でworker，session，snapshot，transport，cacheを束ねる．</p>
<p>名前についても少し補足しておく．
<code>corsa</code>は僕のプロジェクトそのものの名前というより，<code>tsgo</code>側のコードネームに由来する呼び名である．
僕のプロジェクト名は<code>corsa-bind</code>で，<a href="https://github.com/ubugeeei/corsa-bind#readme" target="_blank" rel="noopener noreferrer">README</a>でも，<code>typescript-go</code>をRustやNode.jsから扱うためのbindings / orchestration layersとして説明している．
つまり<code>bind</code>は，単にFFIの関数を一本生やすという意味ではなく，<code>tsgo</code>という処理系の外側にRust，Node，native language向けの接続面と運用層を束ねる，という意味合いを持っている．</p>
<p>僕はこの発想を，<strong>言語処理系オーケストレーション</strong> と呼びたい．</p>
<h2>stdio APIは関数呼び出しではない</h2>
<p><code>tsgo</code>のstdio APIを最初に見ると，「標準入力と標準出力でrequest / responseを流すだけ」と思うかもしれない．
しかしこの見方だと，かなり大事なものを落とす．</p>
<p>stdioの向こう側には，別プロセスとして起動した言語処理系がいる．
こちらはrequestを送る．
向こうはprogramを作り，source fileを読み，型を解き，symbolを持ち，snapshotを管理し，responseを返す．</p>
<p>つまりこれは，</p>
<pre><code class="language-text">caller process
  -&gt; transport
  -&gt; tsgo worker process
  -&gt; program / checker / snapshot state
</code></pre>
<p>という境界である．</p>
<p>ここで重要なのは，<code>tsgo</code> workerが状態を持つことだ．
一回ごとにprocessを起動し，configを読み，projectを開き，型検査して捨てるなら，それはCLIの使い方である．
しかしworkerを生かし続け，initializeを一回で済ませ，snapshotを再利用し，同じsessionに対して小さなqueryを投げ続けるなら，それはserverの使い方である．</p>
<p><a href="https://docs.rs/corsa_bind_client/latest/corsa_bind_client/" target="_blank" rel="noopener noreferrer">docs.rsの<code>corsa_bind_client</code></a>は，この層を「<code>typescript-go</code> stdio APIのhigh-level client bindings」として説明している．
具体的には，<code>tsgo</code> worker processをspawnし，一度initializeしたsessionを再利用し，snapshotを作って再利用し，type / symbol / syntaxの質問をtyped helper経由で投げる，という役割を持つ．</p>
<p>この時点で，僕が<code>corsa-bind</code>でやっていることは単なるbindingではなくなる．
FFIで関数を一本生やす，という話ではない．
プロセスの寿命，transportの種類，requestの型，snapshot handleの寿命，cleanup，timeout，observabilityをまとめて扱う必要がある．</p>
<p>僕は言語処理系を呼ぶためだけの薄いbindingを作っているのではない．
言語処理系を運用する層を書いている．</p>
<h2>コンパイラを速くするのではなく，仕事の形を変える</h2>
<p>ここで誤解してはいけないことがある．</p>
<p>僕は<code>corsa-bind</code>を，<code>tsgo</code>より賢いcompiler engineとして作っているわけではない．
同じprojectを開き，同じ型を解き，同じ診断を出すなら，根本的な仕事は<code>tsgo</code>がやる．
wrapperがその中身を魔法のように速くする，という話ではない．</p>
<p>僕が<code>corsa-bind</code>で狙っているのは，仕事の形を変えることだ．</p>
<p>たとえば，次の二つはかなり違う．</p>
<pre><code class="language-text">CLI-shaped workflow:
  run tsgo
  load config
  open project
  build program
  answer one big question
  exit

orchestrated workflow:
  spawn worker
  initialize once
  open project once
  create snapshot
  answer many small questions
  reuse worker and snapshot
  close explicitly
</code></pre>
<p>前者はbatch処理として自然である．
CIで全体を検査するなら，この形は今でもかなり素直だ．</p>
<p>後者はeditorやlint ruleやagentに近い．
あるnodeの型を知りたい．
次にsymbolを解決したい．
少しvirtual documentを変えてもう一度知りたい．
そのたびにproject全体をCLIとして開き直すのは，分散システムで毎requestごとにdatabase serverを起動するようなものだ．</p>
<p>だから僕が<code>corsa-bind</code>で採用しているperformance modelは，「同じ仕事をして<code>tsgo</code>に勝つ」ではなく，「同じengineの状態を再利用し，余計な仕事をしない」に近い．
<a href="https://github.com/ubugeeei/corsa-bind/blob/main/docs/benchmarking_guide.md" target="_blank" rel="noopener noreferrer">benchmarking guide</a>でも，engine speedとwrapper speedを分け，cold runとwarm runを分け，session reuseやtransportの違いを別々に見る，という考え方を取っている．</p>
<p>良い主張は「<code>corsa-bind</code>は<code>tsgo</code>より速い」ではない．
良い主張は，「warmなeditor workflowでは，毎回<code>tsgo --noEmit</code>を走らせ直すより，live sessionを再利用する方が安くなる」である．</p>
<p>この違いは小さく見えて，かなり大きい．
速いコンパイラを作る話ではなく，コンパイラを含む処理系をどのように配置し，どの状態を再利用し，どのqueryをどのworkerに投げるか，という話になるからだ．</p>
<p>言い換えると，これはcompilerから離れる話ではない．
むしろcompilerを一つの実行ファイルや一回きりの関数としてではなく，長生きするserviceとして扱う話である．
parser，binder，checker，program graphはcompilerの中核だが，それらをいつ起動し，どこに置き，どのconsumerに共有し，どの単位で問い合わせるかは，compilerの外側にある設計問題になる．</p>
<p>僕が<code>corsa-bind</code>で触りたいのは，この外側である．
compiler engineの意味論はupstreamの<code>tsgo</code>に任せる．
そのうえで，compilerを囲むtransportとlifetimeとcacheの層を設計する．</p>
<h2>snapshotは値ではなく，遠隔の状態へのhandleである</h2>
<p>僕が<code>corsa-bind</code>で重要視している概念の一つがsnapshotである．</p>
<p>snapshotは，単なるJSONの値ではない．
少なくとも僕がorchestratorを書く視点では，それはworker側にある言語処理系の状態へのhandleである．</p>
<p>かなり雑に書くと，こういう関係になる．</p>
<pre><code class="language-text">ApiClient
  owns worker process
  owns transport
  initializes tsgo session

ManagedSnapshot
  points to snapshot state inside that session
  can be reused for type / symbol / syntax queries
  releases remote state when dropped or closed
</code></pre>
<p>この「handleである」という見方が大事だ．</p>
<p>普通のlibrary callでは，関数に値を渡し，戻り値を受け取る．
しかしstdio越しの<code>tsgo</code>では，重い状態はworker側に残る．
callerはそれを識別するhandleを持ち，次のrequestでそのhandleを参照する．</p>
<p>これはdatabase connectionやactor systemにかなり近い．
「このsnapshotに対して，このfileのこの位置のtypeを教えてくれ」と聞く．
「このvirtual documentを反映したsnapshotを作ってくれ」と頼む．
「もう使わないのでreleaseしてくれ」と伝える．</p>
<p>つまり，言語処理系の内部状態を，プロセス境界の外からlifetime管理している．
ここを雑にすると，速さ以前に正しさが壊れる．
snapshotを解放し忘れればworker側の資源が残るし，process cleanupを軽く見るとzombie processや後続benchmarkの歪みにつながる．</p>
<p>だから僕は<code>corsa-bind</code>で，typed clientだけでなく，timeout，graceful shutdown，observer event，queue capacityのようなoperationalな要素も同じ設計面に出している．
それはbindingというより，少し小さな運用基盤である．</p>
<h2>分散サーバーのように考える</h2>
<p>この発想をもう一段進めると，僕には<code>corsa-bind</code>が分散サーバーに似て見えてくる．</p>
<p>もちろん，単一マシン上で<code>tsgo</code> workerを複数起動しているだけなら，本当にネットワーク越しのdistributed systemではない．
しかし設計上の問題はかなり似ている．</p>
<ul>
<li><p>どのworkerにrequestを投げるか</p>
</li>
<li><p>workerをいつ起動し，いつ止めるか</p>
</li>
<li><p>coldなworkerとwarmなworkerをどう扱うか</p>
</li>
<li><p>どのsnapshotを再利用できるか</p>
</li>
<li><p>workerが落ちたとき，どこまで復旧できるか</p>
</li>
<li><p>timeoutしたrequestをどう扱うか</p>
</li>
<li><p>transportをJSON-RPCにするかmsgpackにするか</p>
</li>
<li><p>観測可能なeventをどの粒度で出すか</p>
</li>
</ul>
<p>これは「コンパイラAPIを呼ぶ」だけの話ではない．
小さなclusterを扱っているのに近い．</p>
<p>たとえば<code>ApiProfile</code>のような概念は，単にspawn configに名前を付けているだけではない．
どの<code>tsgo</code> executableを使い，どのcwdで起動し，どのtransportを使い，どのtimeoutとobserverを付けるか，というworkerの性格を安定した名前で扱うためのものだ．</p>
<p>概念的には，こうなる．</p>
<pre><code class="language-text">profile: &quot;default-msgpack&quot;
  -&gt; worker pool
    -&gt; worker 1
      -&gt; session
      -&gt; snapshots
    -&gt; worker 2
      -&gt; session
      -&gt; snapshots

profile: &quot;lsp-jsonrpc&quot;
  -&gt; worker pool
    -&gt; worker 1
      -&gt; lsp session
      -&gt; virtual documents
</code></pre>
<p>workerがdatabase shardのように振る舞う，とまでは言いすぎかもしれない．
しかし「状態を持つ遠隔の処理主体を，client側のorchestratorが束ねる」という意味では，同じ設計語彙がかなり使える．</p>
<p>プロセスはnodeである．
snapshotはremote stateへのhandleである．
requestはmessageである．
transportはwire formatである．
profileはdeployment unitである．
observerはtelemetryである．
cleanupはcorrectnessである．</p>
<p>この比喩で見ると，言語処理系のAPI設計は急に面白くなる．</p>
<h2>なぜupstreamをforkしないのか</h2>
<p>僕は<code>corsa-bind</code>のREADMEに，かなりはっきりと<code>no forks, no patches</code>の方針を置いている．
<code>ref/typescript-go</code>をexact upstream checkoutとして扱い，upstream-supportedなentry pointに乗り，ローカルpatchを持たない．</p>
<p>これは単なる僕の潔癖ではない．
オーケストレーション層を作るなら，土台の意味論を勝手に変えないことが重要になる．</p>
<p>もし僕が<code>tsgo</code>本体にpatchを当てて速くしたなら，その速さは<code>corsa-bind</code>のorchestrationによるものなのか，compiler engineの改変によるものなのかが曖昧になる．
benchmarkも読みにくくなる．
upstream追従も難しくなる．
別言語bindingから見た互換性も怪しくなる．</p>
<p>だから僕は，境界を分ける．</p>
<p><code>tsgo</code>は言語処理系のengineとしてupstreamのまま扱う．
<code>corsa-bind</code>ではその外側で，transport，typed request / response，session reuse，snapshot lifetime，Node binding，C ABI，その他のlanguage bindingを扱う．</p>
<p>この分離は，かなり健全だと思っている．
compilerの中へ入り込んで全てを握るのではなく，compilerを一つの専門serverとして尊重し，その周囲で使い方を設計する．</p>
<p>これはUnix的でもあるし，microservice的でもある．
ただし，ここで扱っているserverはHTTP serviceではなく，型検査器である．</p>
<h2>Node bindingは作者体験の入口である</h2>
<p>僕は<code>corsa-bind</code>にRust側のclient / orchestratorだけでなく，<code>napi-rs</code>を使ったNode bindingも入れている．
これは単にJavaScriptからRustを呼べるようにするためだけのものではない．</p>
<p>TypeScriptの型情報を使う道具の作者は，多くの場合JS / TSの側にいる．
lint rule，code mod，editor extension，framework plugin，AI agentのtool adapterなどを書く人は，Rust crateを直接触りたいとは限らない．</p>
<p>一方で，毎回JS側で重いprotocol handlingを書き，process管理を書き，snapshotの寿命を自分で持つのもつらい．
だからRust側でhot pathとprocess orchestrationを持ち，Node側には使いやすいsurfaceを出すようにしている．</p>
<p>僕が<code>corsa-oxlint</code>でやっている方向性は，この考え方にかなり合っている．
rule authorはJS / TSでruleを書く．
しかし型情報の取得や<code>tsgo</code> workerとの会話は，Rust-backedな層が面倒を見る．</p>
<p>ここでも僕の主語は「binding」だけではない．
主語は，<strong>言語処理系を共有資源としてどう配るか</strong> である．</p>
<h2>一つのcompilerから，複数のconsumerへ</h2>
<p>従来のCLIでは，compilerとconsumerの関係は一対一になりやすかった．</p>
<pre><code class="language-text">tsc command
  -&gt; compiler
  -&gt; diagnostics
</code></pre>
<p>しかしeditorやtoolchainの実際の姿は，もっと多対一に近い．</p>
<pre><code class="language-text">editor hover
editor completion
lint rule
code action
AI agent
test runner
build watcher
  -&gt; shared language-processing state
</code></pre>
<p>もちろん全てを本当に一つのprocessに集約すればよい，という話ではない．
fault isolationも必要だし，queryの性質によってworkerを分けた方がよい場合もある．
read-heavyな型queryと，virtual documentを激しく差し替えるworkflowを同じsessionに押し込むべきかは，慎重に見るべきだ．</p>
<p>だからこそorchestratorが必要になる．</p>
<p>orchestratorは，compilerを隠すためのwrapperではない．
compilerを中心に複数のconsumerを配置するためのcontrol planeである．</p>
<p>control planeという言葉を使うと少し大げさだが，やっていることは近い．
どのprofileでworkerを作るか．
どのworkerがwarmか．
どのsnapshotがまだ使えるか．
どのrequestが詰まっているか．
どのtransportがそのworkflowに向いているか．
どの失敗をretryし，どれをcallerに返すか．</p>
<p>こういう判断は，compiler engineそのものとは別の層にある．</p>
<h2>言語処理系はこれから常駐する</h2>
<p>僕は，今後の言語処理系はますます常駐プロセスとして扱われると思っている．</p>
<p>理由は単純で，開発体験がbatchからconversationへ寄っているからだ．</p>
<p>人間がファイルを保存したら一回検査する，というだけではない．
エディタは入力中に候補を出す．
リンターはASTと型を見ながらruleを走らせる．
AI agentは「このnodeの型は何か」「このsymbolはどこから来たか」「この修正でdiagnosticは消えるか」を細かく聞く．
frameworkはvirtual fileを生成し，元ファイルと対応付ける．</p>
<p>こういう世界では，言語処理系は毎回起動するcommandというより，問い合わせを受け続けるserviceになる．</p>
<p>そしてserviceになるなら，そこにはorchestrationが必要になる．
worker pool，cache，snapshot，backpressure，timeout，observability，graceful shutdown，version pinning，compatibility policyが必要になる．</p>
<p>僕は<code>corsa-bind</code>で，<code>tsgo</code>という具体的な対象を通して，この層を作っている．</p>
<p>言語処理系はparserとcheckerだけでは終わらない．
それをどう生かし続け，どう共有し，どう壊れたときに畳み，どう別言語の作者体験へ渡すかまで含めて，これからのlanguage toolingになる．</p>
<h2>結</h2>
<p><code>tsgo</code>は速い<code>tsc</code>である．
しかし，それだけでは少しもったいない．</p>
<p>stdio APIを通して見ると，<code>tsgo</code>は状態を持つ言語処理系serverでもある．
そして僕が<code>corsa-bind</code>で作っているのは，そのserverをRustとNodeと複数のnative bindingから扱うためのorchestration layerである．</p>
<p>僕が大事にしているのは，compiler engineそのものを無理に抱え込まないことだ．
upstreamの<code>tsgo</code>を尊重し，forkせず，patchせず，その代わり外側でsessionを再利用し，snapshotを管理し，transportを選び，workerを束ねる．</p>
<p>これは，分散サーバーを扱うときの考え方に近い．
processがいて，messageが流れ，remote stateへのhandleがあり，lifetimeがあり，失敗があり，観測があり，cleanupがある．</p>
<p>言語処理系をlibraryとして呼ぶだけでなく，serviceとして運用する．</p>
<p>それが，僕が<code>corsa-bind</code>で作ろうとしている言語処理系オーケストレーションである．</p>
]]></content:encoded>
    </item>
    <item>
      <title>Webで透明水彩をやる技術</title>
      <link>https://wtrclred.vercel.app/ja/posts/16</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/16</guid>
      <description>このサイトの水彩背景を，紙，濡れ，乾き，fallback，hydration境界に分けて実装した話．</description>
      <pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-27T11:12:35.000Z</dc:date>
      <category>web-graphics</category>
      <category>canvas</category>
      <category>webgpu</category>
      <category>webgl</category>
      <category>performance</category>
      <content:encoded><![CDATA[<blockquote>
<p>この記事は100% AIによって出力したものであり，使用したモデルはGPT-5.5である．</p>
</blockquote>
<h2>序</h2>
<p>このサイトの背景には，薄い透明水彩のような紙と滲みを置いている．
見た目はただの装飾だが，実装は一枚画像ではない．
WebGPUで紙を描き，WebGL2で濡れた顔料を少しだけ動かし，Canvas 2Dのfallbackを持ち，mobileでは静的な背景へ落とす．</p>
<p>最初に大事なのは，これは「Web上で正確な水彩シミュレーションを作る」話ではない．
文章サイトの背景に，正確な流体シミュレーションはいらない．
むしろ正確にやろうとした瞬間に，背景が本文より偉くなる．</p>
<p>背景に必要なのは，次のくらいでよい．</p>
<ul>
<li><p>紙に凹凸と繊維がある</p>
</li>
<li><p>顔料が均一な半透明色ではない</p>
</li>
<li><p>濡れている間だけ少し動く</p>
</li>
<li><p>乾いたら紙に固定される</p>
</li>
<li><p>動かせない環境では静かに止まる</p>
</li>
<li><p>本文，リンク，スクロールを邪魔しない</p>
</li>
</ul>
<p>実装を読み直すと，水彩らしさはshaderの派手さよりも，この分割で作っていることが分かる．
紙，濡れ，乾き，入力，fallbackを別々にして，それぞれの失敗範囲を小さくする．
この設計がほぼすべてである．</p>
<h2>背景は強くならない</h2>
<p>画面上の層はだいたいこうなっている．</p>
<pre><code class="language-text">paper fallback span
paper fallback canvas
paper WebGPU canvas
brush canvas
page content
controls
</code></pre>
<p>Vue componentのtemplateは，layerを置くだけに近い．</p>
<pre><code class="language-vue">&lt;div class=&quot;watercolor-bg-container&quot; aria-hidden=&quot;true&quot;&gt;
  &lt;span ref=&quot;fallbackLayerElement&quot; class=&quot;paper-fallback&quot; /&gt;
  &lt;canvas ref=&quot;fallbackCanvasElement&quot; class=&quot;paper-fallback-canvas&quot; aria-hidden=&quot;true&quot; /&gt;
  &lt;canvas ref=&quot;paperCanvasElement&quot; class=&quot;paper-canvas&quot; aria-hidden=&quot;true&quot; /&gt;
  &lt;canvas ref=&quot;brushCanvasElement&quot; class=&quot;brush-canvas&quot; aria-hidden=&quot;true&quot; /&gt;
&lt;/div&gt;
</code></pre>
<p><code>aria-hidden=&quot;true&quot;</code>なのは，これは内容ではなく素材だからだ．
screen readerにcanvasの存在を知らせる必要はない．</p>
<p>CSSでも，container自体は巨大なfixed overlayにしない．
固定されるのは個々のcanvasで，containerは本文の上に覆いかぶさらない．</p>
<pre><code class="language-css">.watercolor-bg-container {
  position: static;
  pointer-events: auto;
  background: transparent;
}

.paper-fallback-canvas,
.paper-canvas,
.brush-canvas {
  position: fixed;
  inset: 0;
  pointer-events: none;
}
</code></pre>
<p><code>pointer-events: none</code>がかなり重要である．
背景は画面全体にあるが，リンクもスクロールも奪わない．
見た目は広いが，DOM上の権限は弱い．
文章サイトの装飾は，このくらい弱い方がよい．</p>
<h2>islandも常に起こさない</h2>
<p>この背景はVuerendのislandとして登録している．
ただし，すべてのviewportでhydrateするわけではない．</p>
<pre><code class="language-ts">const desktopIslandMedia = &quot;(min-width: 769px)&quot;;

export const WatercolorBackgroundIsland = defineIsland(&quot;watercolor-background&quot;, {
  component: WatercolorBackgroundIslandView,
  load: () =&gt; import(&quot;./features/ShellWatercolorBackgroundIslandLoader&quot;),
  hydrate: &quot;media&quot;,
  media: desktopIslandMedia,
});
</code></pre>
<p>desktop幅のときだけclient codeを読む．
mobileではCSSでcanvasを隠し，<code>bg-sp.png</code>を使う．</p>
<pre><code class="language-css">@media (max-width: 768px) {
  html {
    background: #f1eedf url(&quot;/bg-sp.png&quot;) center center / cover no-repeat;
  }

  .paper-fallback {
    opacity: 1 !important;
    background: #f1eedf url(&quot;/bg-sp.png&quot;) center center / cover no-repeat;
  }

  .paper-fallback-canvas,
  .paper-canvas,
  .brush-canvas {
    display: none;
  }
}
</code></pre>
<p>これは妥協というより，背景としての仕様だと思う．
touch-firstな環境で，水彩brushを有効にしても嬉しさより事故の方が大きい．
mobileでは静的な紙を出す．
それで十分なところでは，十分なことだけをする．</p>
<p>pointer入力も同じで，fine pointerの環境だけ受ける．</p>
<pre><code class="language-ts">export function canUseFinePointerPaint(): boolean {
  return window.matchMedia(&quot;(hover: hover) and (pointer: fine)&quot;).matches;
}
</code></pre>
<p>背景の実装は，「何をできるか」だけでなく，「どこではやらないか」を先に決めると安定する．</p>
<h2>紙は先に出す</h2>
<p>WebGPUの紙shaderは非同期で初期化される．
だから，その前にCanvas 2Dでfallback紙を描く．</p>
<pre><code class="language-ts">renderPaperFallback(fallbackCanvasElement.value, maxDpr);
brushLayer.resize();
removeViewportListeners = addShellViewportListeners(queueResize);
</code></pre>
<p>fallback紙は単色ではない．
薄いradial wash，shadow patch，highlight patch，fiber，grain tileを重ねる．</p>
<pre><code class="language-ts">context.fillStyle = &quot;#f1eedf&quot;;
context.fillRect(0, 0, width, height);
drawRadialWash(context, width, height);

context.globalCompositeOperation = &quot;multiply&quot;;
drawPaperPatches(context, width, height, random, Math.round(820 * areaScale), &quot;shadow&quot;);

context.globalCompositeOperation = &quot;screen&quot;;
drawPaperPatches(context, width, height, random, Math.round(520 * areaScale), &quot;highlight&quot;);
</code></pre>
<p>WebGPUが成功したら，fallbackのopacityを下げる．
完全には消さない．</p>
<pre><code class="language-ts">target.fallbackLayer?.style.setProperty(&quot;opacity&quot;, &quot;0.1&quot;);
target.fallbackCanvas?.style.setProperty(&quot;opacity&quot;, &quot;0.16&quot;);
</code></pre>
<p>この順番が大事である．
WebGPUが速ければshaderの紙が見える．
遅ければfallback紙が見える．
失敗しても，ただ紙が残る．
背景の失敗としては，それでよい．</p>
<h2>紙はWebGPUで一枚だけ焼く</h2>
<p>WebGPU側は，<code>ShellSitePaper.wgsl</code>をraw importして使っている．</p>
<pre><code class="language-ts">import sitePaperShaderSource from &quot;./ShellSitePaper.wgsl?raw&quot;;

export const sitePaperShader = sitePaperShaderSource;
</code></pre>
<p>adapterは<code>low-power</code>で要求する．</p>
<pre><code class="language-ts">const adapter = await gpu.requestAdapter({ powerPreference: &quot;low-power&quot; });
</code></pre>
<p>背景の紙を描くために，高性能GPUを起こす必要はない．
描画もfullscreen triangle一枚で済ませる．</p>
<pre><code class="language-wgsl">var positions = array&lt;vec2f, 3&gt;(
  vec2f(-1.0, -1.0),
  vec2f(3.0, -1.0),
  vec2f(-1.0, 3.0)
);
</code></pre>
<p>DPRは<code>1.25</code>で切っている．</p>
<pre><code class="language-ts">const dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
</code></pre>
<p>紙のざらつきは，native DPRまで上げなくても読める．
背景に必要な解像度と，画面の物理解像度は同じではない．
ここで欲しいのは精密な紙の顕微鏡画像ではなく，本文の後ろで紙だと分かることだ．</p>
<h2>紙は動かさない</h2>
<p>紙shaderにはtimeを渡していない．
紙の座標はviewportとseedから決まる．</p>
<pre><code class="language-wgsl">let aspect = uniforms.resolution.x / max(uniforms.resolution.y, 1.0);
let centered = (input.uv - 0.5) * vec2f(aspect * 1.72, 1.72);
let point =
  rotate(centered * vec2f(1.0, 1.04), -0.06)
  + vec2f(uniforms.seed * 6.4, uniforms.seed * 3.2);
</code></pre>
<p>紙が読んでいる最中に動くと，水彩紙ではなく演出になる．
背景に置く紙は，動かない方が強い．</p>
<p>shaderの中では，単なる色noiseではなく，高さ場を作っている．
<code>embossField</code>，<code>microGrainField</code>，<code>fiberField</code>，<code>celluloseNetworkField</code>，<code>coldPressToothField</code>，<code>crumpleField</code>，<code>wrinkleLineField</code>のような場がある．
それらを<code>paperHeight</code>へ混ぜる．</p>
<pre><code class="language-wgsl">return macroRelief * 0.34
  + detail.x * 2.16
  + micro * (0.76 + uniforms.fiberDensity * 0.16 + uniforms.grainScale * 0.1)
  + (emboss - 0.18) * (0.18 + uniforms.embossDepth * 0.24)
  + (tooth.y - 0.28) * (0.46 + uniforms.embossDepth * 0.34)
  - (tooth.x - 0.18) * (0.34 + uniforms.shadowStrength * 0.22)
  + (grain - 0.5) * 0.018
  + (pulp - 0.3) * 0.018;
</code></pre>
<p><code>tooth.y</code>は凸部，<code>tooth.x</code>は凹部として効く．
この符号の違いが，紙の上に顔料が乗ったときの説得力になる．
紙は白い背景ではなく，顔料が沈む地形である．</p>
<p>紙shaderの処理を手順にすると，こうなる．</p>
<pre><code class="language-text">1. uvを紙座標へ変換する
2. 低周波のmacro reliefを作る
3. fiber / fibril / cellulose / tooth / pulpの場を別々に作る
4. それらを高さfieldへ合成する
5. 高さfieldを近傍sampleして法線を作る
6. 法線，局所relief，toothからcavity / ridge maskを作る
7. 暖かい白にcavity shadowとridge highlightを混ぜる
</code></pre>
<p>法線は解析的に出していない．
<code>paperHeight</code>を少しずらしてsampleし，差分から作っている．</p>
<pre><code class="language-wgsl">fn paperNormal(point: vec2f) -&gt; vec3f {
  let epsilon = 0.00076;
  let center = paperHeight(point);
  let dx = paperHeight(point + vec2f(epsilon, 0.0)) - center;
  let dy = paperHeight(point + vec2f(0.0, epsilon)) - center;
  let strength = 3.1 + uniforms.embossDepth * 2.2 + uniforms.grainScale * 1.1;
  return normalize(vec3f(-dx * strength, -dy * strength, 1.0));
}
</code></pre>
<p>その法線と紙目から，凹みと山を別々に見る．</p>
<pre><code class="language-wgsl">let cavityMask = saturate(
  max(0.0, -localRelief * 1.84)
    + tooth.x * 0.68
    + (1.0 - normal.z) * 0.4
    + max(0.0, -diffuse) * 0.16
    + crumple.x * 0.08
    + wrinkleLines.x * 0.05
);

let ridgeMask = saturate(
  max(0.0, localRelief * 1.42)
    + tooth.y * 0.62
    + detail.y * 0.06
    + max(0.0, diffuse) * 0.18
    + crumple.y * 0.06
    + wrinkleLines.y * 0.04
);
</code></pre>
<p>ここでcavityを暗くしすぎないのも大事である．
背景紙なので，凹凸が見えても本文のcontrastを奪ってはいけない．
最後は<code>clamp(tone, vec3f(0.72), vec3f(1.0))</code>で沈みすぎを止める．</p>
<h2>ブラシはstampではなく濡れた状態</h2>
<p>最初の発想として，Canvas 2Dで有機的な形を重ねれば水彩っぽく見える．
実際，fallback brushはそうしている．
ただ，それだけだと「半透明の画像を押している」感じが残る．
水彩らしさには，描いたあとに少しだけ状態が変わる時間がいる．</p>
<p>そこで通常pathではWebGL2の小さなsolverを使う．
ただし，これも最初から起動しない．</p>
<pre><code class="language-ts">const fallback = createBrushLayer(options);
let engine: FluidBrushEngine | undefined;
let triedEngine = false;

function ensureEngine(): FluidBrushEngine | undefined {
  if (engine || triedEngine) {
    return engine;
  }

  triedEngine = true;
  engine = FluidBrushEngine.create();
  return engine;
}
</code></pre>
<p>WebGL2やfloat framebufferが使えなければ，Canvas 2D brushへ戻る．</p>
<pre><code class="language-ts">if (!gl || !gl.getExtension(&quot;EXT_color_buffer_float&quot;)) {
  return undefined;
}
</code></pre>
<p>solverは画面全体の解像度では動かさない．
base sizeは<code>560</code>で，aspectに合わせて小さなtextureを作る．</p>
<pre><code class="language-ts">const simulationBaseSize = 560;
</code></pre>
<p>背景の滲みは，細かすぎるsimulationよりも少し鈍い方がよい．
解像度を下げることが，見た目にも性能にも効く．</p>
<p>流体の章は，まずこの1行だけで読める．</p>
<pre><code class="language-text">筆先が wet を置く -&gt; velocity が wet を少し運ぶ -&gt; pressure が暴れを抑える -&gt; 古い wet が dry になる
</code></pre>
<p>目で追う対象は，青い<code>wet</code>のしみだけでよい．
矢印の<code>velocity</code>はそのしみを動かす力で，赤い<code>pressure</code>は破裂しそうな流れをならすためのブレーキで，橙の<code>dry</code>は紙に固定された跡である．
この見方は，<a href="https://x.com/threejs/status/2047490608940655073?s=20" target="_blank" rel="noopener noreferrer">three.jsのX投稿</a>を見て，水彩背景の実装を自分のコードに引き寄せて読み直したものである．</p>
<p>brush solverの1 frameは，ざっくり次のpipelineになっている．</p>
<pre><code class="language-text">curl
vorticity confinement
advect velocity
boundary velocity
advect wet
boundary wet
dry wet into dry
remove dried wet
divergence
pressure solve
subtract pressure gradient
boundary velocity
display
</code></pre>
<p>&lt;div&gt;
&lt;svg viewBox=&quot;0 0 760 360&quot; role=&quot;img&quot; aria-labelledby=&quot;fluid-frame-title-ja&quot;&gt;
  &lt;title id=&quot;fluid-frame-title-ja&quot;&gt;WebGL2 brush solver の1 frame pipeline&lt;/title&gt;
  &lt;defs&gt;
    &lt;marker id=&quot;arrow-fluid-frame-ja&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;5&quot; markerHeight=&quot;5&quot; orient=&quot;auto-start-reverse&quot;&gt;
      &lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#35495e&quot; /&gt;
    &lt;/marker&gt;
    &lt;linearGradient id=&quot;code-wash-fluid-frame-ja&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; stop-color=&quot;#5a8abf&quot; stop-opacity=&quot;0.045&quot; /&gt;
      &lt;stop offset=&quot;72%&quot; stop-color=&quot;#5a8abf&quot; stop-opacity=&quot;0&quot; /&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;rect x=&quot;0&quot; y=&quot;0&quot; width=&quot;760&quot; height=&quot;360&quot; fill=&quot;url(#code-wash-fluid-frame-ja)&quot; /&gt;
  &lt;path d=&quot;M 0 0.5 H 760 M 0 359.5 H 760&quot; stroke=&quot;#2f3b47&quot; stroke-opacity=&quot;0.16&quot; /&gt;
  &lt;text x=&quot;48&quot; y=&quot;62&quot; font-size=&quot;18&quot; font-weight=&quot;700&quot; fill=&quot;#35495e&quot;&gt;1 frame の見取り図&lt;/text&gt;
  &lt;g font-family=&quot;system-ui, sans-serif&quot; font-size=&quot;14&quot; text-anchor=&quot;middle&quot;&gt;
    &lt;ellipse cx=&quot;112&quot; cy=&quot;119&quot; rx=&quot;70&quot; ry=&quot;32&quot; fill=&quot;#64d4a3&quot; opacity=&quot;0.16&quot; /&gt;
    &lt;text x=&quot;112&quot; y=&quot;115&quot; fill=&quot;#35495e&quot;&gt;curl&lt;/text&gt;
    &lt;text x=&quot;112&quot; y=&quot;133&quot; fill=&quot;#666&quot;&gt;渦度を測る&lt;/text&gt;
    &lt;line x1=&quot;184&quot; y1=&quot;119&quot; x2=&quot;204&quot; y2=&quot;119&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-fluid-frame-ja)&quot; /&gt;
    &lt;ellipse cx=&quot;281&quot; cy=&quot;119&quot; rx=&quot;76&quot; ry=&quot;32&quot; fill=&quot;#64d4a3&quot; opacity=&quot;0.16&quot; /&gt;
    &lt;text x=&quot;281&quot; y=&quot;115&quot; fill=&quot;#35495e&quot;&gt;vorticity&lt;/text&gt;
    &lt;text x=&quot;281&quot; y=&quot;133&quot; fill=&quot;#666&quot;&gt;渦を少し戻す&lt;/text&gt;
    &lt;line x1=&quot;358&quot; y1=&quot;119&quot; x2=&quot;382&quot; y2=&quot;119&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-fluid-frame-ja)&quot; /&gt;
    &lt;ellipse cx=&quot;455&quot; cy=&quot;119&quot; rx=&quot;76&quot; ry=&quot;32&quot; fill=&quot;#646cff&quot; opacity=&quot;0.13&quot; /&gt;
    &lt;text x=&quot;455&quot; y=&quot;115&quot; fill=&quot;#35495e&quot;&gt;advect u&lt;/text&gt;
    &lt;text x=&quot;455&quot; y=&quot;133&quot; fill=&quot;#666&quot;&gt;速度を運ぶ&lt;/text&gt;
    &lt;line x1=&quot;532&quot; y1=&quot;119&quot; x2=&quot;556&quot; y2=&quot;119&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-fluid-frame-ja)&quot; /&gt;
    &lt;ellipse cx=&quot;629&quot; cy=&quot;119&quot; rx=&quot;76&quot; ry=&quot;32&quot; fill=&quot;#646cff&quot; opacity=&quot;0.13&quot; /&gt;
    &lt;text x=&quot;629&quot; y=&quot;115&quot; fill=&quot;#35495e&quot;&gt;advect wet&lt;/text&gt;
    &lt;text x=&quot;629&quot; y=&quot;133&quot; fill=&quot;#666&quot;&gt;顔料を運ぶ&lt;/text&gt;
    &lt;line x1=&quot;629&quot; y1=&quot;154&quot; x2=&quot;629&quot; y2=&quot;184&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-fluid-frame-ja)&quot; /&gt;
    &lt;ellipse cx=&quot;629&quot; cy=&quot;219&quot; rx=&quot;76&quot; ry=&quot;32&quot; fill=&quot;#f59e0b&quot; opacity=&quot;0.14&quot; /&gt;
    &lt;text x=&quot;629&quot; y=&quot;215&quot; fill=&quot;#35495e&quot;&gt;dry&lt;/text&gt;
    &lt;text x=&quot;629&quot; y=&quot;233&quot; fill=&quot;#666&quot;&gt;wet を dry へ&lt;/text&gt;
    &lt;line x1=&quot;552&quot; y1=&quot;219&quot; x2=&quot;530&quot; y2=&quot;219&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-fluid-frame-ja)&quot; /&gt;
    &lt;ellipse cx=&quot;455&quot; cy=&quot;219&quot; rx=&quot;76&quot; ry=&quot;32&quot; fill=&quot;#ff6b6b&quot; opacity=&quot;0.1&quot; /&gt;
    &lt;text x=&quot;455&quot; y=&quot;215&quot; fill=&quot;#35495e&quot;&gt;divergence&lt;/text&gt;
    &lt;text x=&quot;455&quot; y=&quot;233&quot; fill=&quot;#666&quot;&gt;膨張を測る&lt;/text&gt;
    &lt;line x1=&quot;380&quot; y1=&quot;219&quot; x2=&quot;358&quot; y2=&quot;219&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-fluid-frame-ja)&quot; /&gt;
    &lt;ellipse cx=&quot;281&quot; cy=&quot;219&quot; rx=&quot;76&quot; ry=&quot;32&quot; fill=&quot;#ff6b6b&quot; opacity=&quot;0.1&quot; /&gt;
    &lt;text x=&quot;281&quot; y=&quot;215&quot; fill=&quot;#35495e&quot;&gt;pressure&lt;/text&gt;
    &lt;text x=&quot;281&quot; y=&quot;233&quot; fill=&quot;#666&quot;&gt;8回反復&lt;/text&gt;
    &lt;line x1=&quot;204&quot; y1=&quot;219&quot; x2=&quot;184&quot; y2=&quot;219&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-fluid-frame-ja)&quot; /&gt;
    &lt;ellipse cx=&quot;112&quot; cy=&quot;219&quot; rx=&quot;70&quot; ry=&quot;32&quot; fill=&quot;#ff6b6b&quot; opacity=&quot;0.1&quot; /&gt;
    &lt;text x=&quot;112&quot; y=&quot;215&quot; fill=&quot;#35495e&quot;&gt;gradient&lt;/text&gt;
    &lt;text x=&quot;112&quot; y=&quot;233&quot; fill=&quot;#666&quot;&gt;圧力を引く&lt;/text&gt;
    &lt;line x1=&quot;112&quot; y1=&quot;252&quot; x2=&quot;112&quot; y2=&quot;282&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-fluid-frame-ja)&quot; /&gt;
    &lt;path d=&quot;M 60 302 C 200 292 388 292 692 302&quot; fill=&quot;none&quot; stroke=&quot;#fcb32c&quot; stroke-width=&quot;28&quot; stroke-linecap=&quot;round&quot; opacity=&quot;0.14&quot; /&gt;
    &lt;text x=&quot;373&quot; y=&quot;307&quot; fill=&quot;#35495e&quot;&gt;display: wet / dry を透明な brush canvas へ戻す&lt;/text&gt;
  &lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<p>完全なNavier-Stokes solverではない．
ただ，速度場に渦を少し戻し，semi-Lagrangian advectionで濡れた顔料を運び，pressure projectionで発散を抑える．
背景としては，このくらいのstable fluidsで十分だった．</p>
<h2>まず粒子を追っていない</h2>
<p>流体と聞くと，水の粒をたくさん置いて，その粒を追いかける絵を想像しやすい．
でもこの実装は粒子を追っていない．
画面より小さいtexture gridを用意して，各pixelに「ここでは水がどちらへ流れているか」と「ここにはどれくらい濡れた顔料があるか」を持たせている．</p>
<p>つまり，考える対象は粒ではなく場である．</p>
<pre><code class="language-text">各pixel:
  velocity = その場所の水の向き
  wet      = その場所の濡れた顔料
  dry      = その場所の乾いた顔料
</code></pre>
<p>1つのstrokeは，だいたい次のように変わる．</p>
<p>&lt;div&gt;
&lt;svg viewBox=&quot;0 0 760 520&quot; role=&quot;img&quot; aria-labelledby=&quot;toy-fluid-title-ja&quot;&gt;
  &lt;title id=&quot;toy-fluid-title-ja&quot;&gt;1つの筆跡が wet / velocity / dry に分かれていく様子&lt;/title&gt;
  &lt;defs&gt;
    &lt;marker id=&quot;arrow-toy-fluid-ja&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;5&quot; markerHeight=&quot;5&quot; orient=&quot;auto-start-reverse&quot;&gt;
      &lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#35495e&quot; /&gt;
    &lt;/marker&gt;
    &lt;radialGradient id=&quot;wet-blob-ja&quot; cx=&quot;50%&quot; cy=&quot;50%&quot; r=&quot;60%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; stop-color=&quot;#646cff&quot; stop-opacity=&quot;0.46&quot; /&gt;
      &lt;stop offset=&quot;70%&quot; stop-color=&quot;#646cff&quot; stop-opacity=&quot;0.18&quot; /&gt;
      &lt;stop offset=&quot;100%&quot; stop-color=&quot;#646cff&quot; stop-opacity=&quot;0&quot; /&gt;
    &lt;/radialGradient&gt;
    &lt;radialGradient id=&quot;dry-blob-ja&quot; cx=&quot;50%&quot; cy=&quot;50%&quot; r=&quot;60%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; stop-color=&quot;#f59e0b&quot; stop-opacity=&quot;0.42&quot; /&gt;
      &lt;stop offset=&quot;80%&quot; stop-color=&quot;#f59e0b&quot; stop-opacity=&quot;0.16&quot; /&gt;
      &lt;stop offset=&quot;100%&quot; stop-color=&quot;#f59e0b&quot; stop-opacity=&quot;0&quot; /&gt;
    &lt;/radialGradient&gt;
    &lt;linearGradient id=&quot;code-wash-toy-fluid-ja&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; stop-color=&quot;#5a8abf&quot; stop-opacity=&quot;0.045&quot; /&gt;
      &lt;stop offset=&quot;72%&quot; stop-color=&quot;#5a8abf&quot; stop-opacity=&quot;0&quot; /&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;rect x=&quot;0&quot; y=&quot;0&quot; width=&quot;760&quot; height=&quot;520&quot; fill=&quot;url(#code-wash-toy-fluid-ja)&quot; /&gt;
  &lt;path d=&quot;M 0 0.5 H 760 M 0 519.5 H 760&quot; stroke=&quot;#2f3b47&quot; stroke-opacity=&quot;0.16&quot; /&gt;
  &lt;text x=&quot;48&quot; y=&quot;62&quot; font-size=&quot;18&quot; font-weight=&quot;700&quot; fill=&quot;#35495e&quot;&gt;青い wet が動き，橙の dry として残る&lt;/text&gt;
  &lt;g font-family=&quot;system-ui, sans-serif&quot; font-size=&quot;14&quot;&gt;
    &lt;path d=&quot;M 380 92 L 380 442 M 58 266 L 702 266&quot; stroke=&quot;#ddd&quot; stroke-width=&quot;1&quot; opacity=&quot;0.5&quot; /&gt;
    &lt;text x=&quot;80&quot; y=&quot;120&quot; fill=&quot;#35495e&quot; font-weight=&quot;700&quot;&gt;0. splat&lt;/text&gt;
    &lt;text x=&quot;80&quot; y=&quot;142&quot; fill=&quot;#666&quot;&gt;pointer位置に wet と velocity を足す&lt;/text&gt;
    &lt;ellipse cx=&quot;210&quot; cy=&quot;180&quot; rx=&quot;46&quot; ry=&quot;30&quot; fill=&quot;url(#wet-blob-ja)&quot; /&gt;
    &lt;circle cx=&quot;210&quot; cy=&quot;180&quot; r=&quot;7&quot; fill=&quot;#646cff&quot; /&gt;
    &lt;path d=&quot;M 210 180 L 264 160&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-toy-fluid-ja)&quot; /&gt;
    &lt;path d=&quot;M 210 180 L 260 202&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-toy-fluid-ja)&quot; /&gt;
    &lt;path d=&quot;M 210 180 L 174 208&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-toy-fluid-ja)&quot; /&gt;</p>
<p>&lt;text x=&quot;424&quot; y=&quot;120&quot; fill=&quot;#35495e&quot; font-weight=&quot;700&quot;&gt;1. advect&lt;/text&gt;
    &lt;text x=&quot;424&quot; y=&quot;142&quot; fill=&quot;#666&quot;&gt;velocity が wet を少し運ぶ&lt;/text&gt;
    &lt;ellipse cx=&quot;534&quot; cy=&quot;182&quot; rx=&quot;58&quot; ry=&quot;28&quot; transform=&quot;rotate(10 534 182)&quot; fill=&quot;url(#wet-blob-ja)&quot; /&gt;
    &lt;path d=&quot;M 478 176 C 520 158 564 164 610 188&quot; fill=&quot;none&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-toy-fluid-ja)&quot; /&gt;
    &lt;path d=&quot;M 482 208 C 528 194 574 198 616 220&quot; fill=&quot;none&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-toy-fluid-ja)&quot; /&gt;</p>
<p>&lt;text x=&quot;80&quot; y=&quot;314&quot; fill=&quot;#35495e&quot; font-weight=&quot;700&quot;&gt;2. project&lt;/text&gt;
    &lt;text x=&quot;80&quot; y=&quot;336&quot; fill=&quot;#666&quot;&gt;膨らみすぎる速度を pressure でならす&lt;/text&gt;
    &lt;circle cx=&quot;204&quot; cy=&quot;390&quot; r=&quot;36&quot; fill=&quot;#ff6b6b&quot; opacity=&quot;0.12&quot; /&gt;
    &lt;path d=&quot;M 204 390 L 204 354&quot; stroke=&quot;#ff6b6b&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-toy-fluid-ja)&quot; /&gt;
    &lt;path d=&quot;M 204 390 L 242 390&quot; stroke=&quot;#ff6b6b&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-toy-fluid-ja)&quot; /&gt;
    &lt;path d=&quot;M 204 390 L 204 426&quot; stroke=&quot;#ff6b6b&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-toy-fluid-ja)&quot; /&gt;
    &lt;path d=&quot;M 204 390 L 166 390&quot; stroke=&quot;#ff6b6b&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-toy-fluid-ja)&quot; /&gt;
    &lt;path d=&quot;M 292 362 C 326 380 326 416 292 438&quot; fill=&quot;none&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;3&quot; marker-end=&quot;url(#arrow-toy-fluid-ja)&quot; /&gt;
    &lt;text x=&quot;80&quot; y=&quot;450&quot; fill=&quot;#666&quot;&gt;外向きの破裂を抑える&lt;/text&gt;</p>
<p>&lt;text x=&quot;424&quot; y=&quot;314&quot; fill=&quot;#35495e&quot; font-weight=&quot;700&quot;&gt;3. dry&lt;/text&gt;
    &lt;text x=&quot;424&quot; y=&quot;336&quot; fill=&quot;#666&quot;&gt;wet は減り，dry が紙に残る&lt;/text&gt;
    &lt;ellipse cx=&quot;540&quot; cy=&quot;382&quot; rx=&quot;62&quot; ry=&quot;30&quot; transform=&quot;rotate(8 540 382)&quot; fill=&quot;url(#dry-blob-ja)&quot; /&gt;
    &lt;ellipse cx=&quot;566&quot; cy=&quot;370&quot; rx=&quot;36&quot; ry=&quot;18&quot; transform=&quot;rotate(8 566 370)&quot; fill=&quot;url(#wet-blob-ja)&quot; opacity=&quot;0.55&quot; /&gt;
    &lt;path d=&quot;M 494 402 C 528 416 576 412 616 392&quot; fill=&quot;none&quot; stroke=&quot;#f59e0b&quot; stroke-width=&quot;3&quot; opacity=&quot;0.7&quot; /&gt;
    &lt;text x=&quot;424&quot; y=&quot;430&quot; fill=&quot;#666&quot;&gt;乾いた部分はもう移流しない&lt;/text&gt;
  &lt;/g&gt;
  &lt;g font-family=&quot;system-ui, sans-serif&quot; font-size=&quot;13&quot;&gt;
    &lt;circle cx=&quot;78&quot; cy=&quot;470&quot; r=&quot;7&quot; fill=&quot;#646cff&quot; opacity=&quot;0.55&quot; /&gt;
    &lt;text x=&quot;94&quot; y=&quot;475&quot; fill=&quot;#35495e&quot;&gt;青 = まだ動く wet&lt;/text&gt;
    &lt;line x1=&quot;222&quot; y1=&quot;470&quot; x2=&quot;260&quot; y2=&quot;470&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-toy-fluid-ja)&quot; /&gt;
    &lt;text x=&quot;272&quot; y=&quot;475&quot; fill=&quot;#35495e&quot;&gt;矢印 = velocity&lt;/text&gt;
    &lt;circle cx=&quot;406&quot; cy=&quot;470&quot; r=&quot;7&quot; fill=&quot;#f59e0b&quot; opacity=&quot;0.6&quot; /&gt;
    &lt;text x=&quot;422&quot; y=&quot;475&quot; fill=&quot;#35495e&quot;&gt;橙 = 紙に固定された dry&lt;/text&gt;
    &lt;circle cx=&quot;574&quot; cy=&quot;470&quot; r=&quot;7&quot; fill=&quot;#ff6b6b&quot; opacity=&quot;0.45&quot; /&gt;
    &lt;text x=&quot;590&quot; y=&quot;475&quot; fill=&quot;#35495e&quot;&gt;赤 = pressure の補正&lt;/text&gt;
  &lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<p>この図で言うと，流体solverが毎frame動かすのは主に<code>velocity</code>と<code>wet</code>である．
<code>dry</code>はそこから外される．
だから顔料は，最初だけ水に乗って動き，だんだん紙に固定されていく．</p>
<h2>流体の場はtextureとして持つ</h2>
<p>solverはCPU上の配列ではなく，WebGL2の<code>RGBA16F</code> textureとして場を持つ．
各passはfullscreen triangleを一枚描き，fragment shaderで次のtextureを書き込む．
読み書きが同じtextureになると壊れるので，<code>velocity</code>，<code>wet</code>，<code>dry</code>，<code>pressure</code>はread/writeを持つping-pong bufferである．</p>
<pre><code class="language-text">velocity.xy   速度
velocity.zw   未使用 / alphaの余白
wet.rgb       濡れた顔料の吸収量
wet.a         濡れ量
dry.rgb       乾いた顔料の吸収量
dry.a         乾いた量
pressure.x    圧力
divergence.x  速度場の発散
curl.x        2D速度場の渦度
</code></pre>
<p>&lt;div&gt;
&lt;svg viewBox=&quot;0 0 760 340&quot; role=&quot;img&quot; aria-labelledby=&quot;fluid-textures-title-ja&quot;&gt;
  &lt;title id=&quot;fluid-textures-title-ja&quot;&gt;流体solverが持つtexture field&lt;/title&gt;
  &lt;defs&gt;
    &lt;marker id=&quot;arrow-fluid-textures-ja&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;5&quot; markerHeight=&quot;5&quot; orient=&quot;auto-start-reverse&quot;&gt;
      &lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#35495e&quot; /&gt;
    &lt;/marker&gt;
    &lt;linearGradient id=&quot;code-wash-fluid-textures-ja&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; stop-color=&quot;#5a8abf&quot; stop-opacity=&quot;0.045&quot; /&gt;
      &lt;stop offset=&quot;72%&quot; stop-color=&quot;#5a8abf&quot; stop-opacity=&quot;0&quot; /&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;rect x=&quot;0&quot; y=&quot;0&quot; width=&quot;760&quot; height=&quot;340&quot; fill=&quot;url(#code-wash-fluid-textures-ja)&quot; /&gt;
  &lt;path d=&quot;M 0 0.5 H 760 M 0 339.5 H 760&quot; stroke=&quot;#2f3b47&quot; stroke-opacity=&quot;0.16&quot; /&gt;
  &lt;text x=&quot;48&quot; y=&quot;62&quot; font-size=&quot;18&quot; font-weight=&quot;700&quot; fill=&quot;#35495e&quot;&gt;field は texture として並んでいる&lt;/text&gt;
  &lt;g font-family=&quot;system-ui, sans-serif&quot; font-size=&quot;14&quot; text-anchor=&quot;middle&quot;&gt;
    &lt;ellipse cx=&quot;124&quot; cy=&quot;130&quot; rx=&quot;78&quot; ry=&quot;42&quot; fill=&quot;#646cff&quot; opacity=&quot;0.1&quot; /&gt;
    &lt;text x=&quot;124&quot; y=&quot;121&quot; fill=&quot;#35495e&quot; font-weight=&quot;700&quot;&gt;velocity&lt;/text&gt;
    &lt;text x=&quot;124&quot; y=&quot;143&quot; fill=&quot;#666&quot;&gt;xy = 水の流れ&lt;/text&gt;
    &lt;ellipse cx=&quot;290&quot; cy=&quot;130&quot; rx=&quot;78&quot; ry=&quot;42&quot; fill=&quot;#42b883&quot; opacity=&quot;0.12&quot; /&gt;
    &lt;text x=&quot;290&quot; y=&quot;121&quot; fill=&quot;#35495e&quot; font-weight=&quot;700&quot;&gt;wet&lt;/text&gt;
    &lt;text x=&quot;290&quot; y=&quot;143&quot; fill=&quot;#666&quot;&gt;rgb = 濡れ顔料&lt;/text&gt;
    &lt;ellipse cx=&quot;456&quot; cy=&quot;130&quot; rx=&quot;78&quot; ry=&quot;42&quot; fill=&quot;#f59e0b&quot; opacity=&quot;0.12&quot; /&gt;
    &lt;text x=&quot;456&quot; y=&quot;121&quot; fill=&quot;#35495e&quot; font-weight=&quot;700&quot;&gt;dry&lt;/text&gt;
    &lt;text x=&quot;456&quot; y=&quot;143&quot; fill=&quot;#666&quot;&gt;rgb = 乾いた顔料&lt;/text&gt;
    &lt;ellipse cx=&quot;622&quot; cy=&quot;130&quot; rx=&quot;78&quot; ry=&quot;42&quot; fill=&quot;#ff6b6b&quot; opacity=&quot;0.1&quot; /&gt;
    &lt;text x=&quot;622&quot; y=&quot;121&quot; fill=&quot;#35495e&quot; font-weight=&quot;700&quot;&gt;pressure&lt;/text&gt;
    &lt;text x=&quot;622&quot; y=&quot;143&quot; fill=&quot;#666&quot;&gt;x = 圧力&lt;/text&gt;
    &lt;text x=&quot;196&quot; y=&quot;232&quot; fill=&quot;#35495e&quot;&gt;read texture&lt;/text&gt;
    &lt;path d=&quot;M 128 244 C 162 250 230 250 264 244&quot; fill=&quot;none&quot; stroke=&quot;#8899aa&quot; stroke-width=&quot;2&quot; opacity=&quot;0.55&quot; /&gt;
    &lt;text x=&quot;380&quot; y=&quot;208&quot; fill=&quot;#666&quot; font-size=&quot;13&quot;&gt;shader pass で write へ描く&lt;/text&gt;
    &lt;line x1=&quot;282&quot; y1=&quot;224&quot; x2=&quot;476&quot; y2=&quot;224&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-fluid-textures-ja)&quot; /&gt;
    &lt;text x=&quot;564&quot; y=&quot;232&quot; fill=&quot;#35495e&quot;&gt;write texture&lt;/text&gt;
    &lt;path d=&quot;M 496 244 C 530 250 598 250 632 244&quot; fill=&quot;none&quot; stroke=&quot;#8899aa&quot; stroke-width=&quot;2&quot; opacity=&quot;0.55&quot; /&gt;
    &lt;path d=&quot;M 564 260 C 520 306 240 306 196 260&quot; fill=&quot;none&quot; stroke=&quot;#f59e0b&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-fluid-textures-ja)&quot; /&gt;
    &lt;text x=&quot;380&quot; y=&quot;316&quot; fill=&quot;#666&quot;&gt;swap: write が次の read になる&lt;/text&gt;
  &lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<p>流体として見ると，毎frameでやりたいことはこうである．</p>
<pre><code class="language-text">u = velocity
w = wet pigment
d = dry pigment

u &lt;- add vorticity(u)
u &lt;- advect(u, u)
w &lt;- advect(w, u)
d, w &lt;- dry(w, d)
u &lt;- project(u)
</code></pre>
<p>ここで<code>dry</code>は移流しない．
紙に固定された顔料だからだ．
水彩っぽさは，動く場を増やすことより，動かない場を決めることで出ている．</p>
<h2>移流は戻ってsampleする</h2>
<p>移流はsemi-Lagrangian advectionである．
現在のpixelに来た量は，ひとつ前の時刻では速度のぶんだけ上流にあった，とみなす．
だから現在位置<code>vUv</code>から逆向きに戻ってsampleする．</p>
<p>&lt;div&gt;
&lt;svg viewBox=&quot;0 0 760 320&quot; role=&quot;img&quot; aria-labelledby=&quot;advection-title-ja&quot;&gt;
  &lt;title id=&quot;advection-title-ja&quot;&gt;semi-Lagrangian advection のbacktrace&lt;/title&gt;
  &lt;defs&gt;
    &lt;marker id=&quot;arrow-advection-ja&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;5&quot; markerHeight=&quot;5&quot; orient=&quot;auto-start-reverse&quot;&gt;
      &lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#35495e&quot; /&gt;
    &lt;/marker&gt;
    &lt;linearGradient id=&quot;code-wash-advection-ja&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; stop-color=&quot;#5a8abf&quot; stop-opacity=&quot;0.045&quot; /&gt;
      &lt;stop offset=&quot;72%&quot; stop-color=&quot;#5a8abf&quot; stop-opacity=&quot;0&quot; /&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;rect x=&quot;0&quot; y=&quot;0&quot; width=&quot;760&quot; height=&quot;320&quot; fill=&quot;url(#code-wash-advection-ja)&quot; /&gt;
  &lt;path d=&quot;M 0 0.5 H 760 M 0 319.5 H 760&quot; stroke=&quot;#2f3b47&quot; stroke-opacity=&quot;0.16&quot; /&gt;
  &lt;text x=&quot;48&quot; y=&quot;62&quot; font-size=&quot;18&quot; font-weight=&quot;700&quot; fill=&quot;#35495e&quot;&gt;移流は「今の点から上流へ戻って読む」&lt;/text&gt;
  &lt;g stroke=&quot;#ddd&quot; stroke-width=&quot;1&quot;&gt;
    &lt;path d=&quot;M 84 88 L 84 256 M 144 88 L 144 256 M 204 88 L 204 256 M 264 88 L 264 256 M 324 88 L 324 256 M 384 88 L 384 256 M 444 88 L 444 256 M 504 88 L 504 256 M 564 88 L 564 256 M 624 88 L 624 256&quot; /&gt;
    &lt;path d=&quot;M 64 108 L 660 108 M 64 148 L 660 148 M 64 188 L 660 188 M 64 228 L 660 228&quot; /&gt;
  &lt;/g&gt;
  &lt;g font-family=&quot;system-ui, sans-serif&quot; font-size=&quot;14&quot;&gt;
    &lt;text x=&quot;324&quot; y=&quot;122&quot; fill=&quot;#35495e&quot; text-anchor=&quot;middle&quot;&gt;前の時刻のsample位置&lt;/text&gt;
    &lt;circle cx=&quot;324&quot; cy=&quot;150&quot; r=&quot;8&quot; fill=&quot;#42b883&quot; /&gt;
    &lt;text x=&quot;520&quot; y=&quot;122&quot; fill=&quot;#35495e&quot; text-anchor=&quot;middle&quot;&gt;現在のpixel vUv&lt;/text&gt;
    &lt;circle cx=&quot;520&quot; cy=&quot;166&quot; r=&quot;8&quot; fill=&quot;#646cff&quot; /&gt;
    &lt;path d=&quot;M 520 166 C 472 132 386 126 324 150&quot; fill=&quot;none&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;3&quot; marker-end=&quot;url(#arrow-advection-ja)&quot; /&gt;
    &lt;text x=&quot;420&quot; y=&quot;194&quot; fill=&quot;#666&quot; text-anchor=&quot;middle&quot;&gt;速度分だけ逆に戻る&lt;/text&gt;
    &lt;path d=&quot;M 242 220 C 330 206 438 210 564 230&quot; fill=&quot;none&quot; stroke=&quot;#f59e0b&quot; stroke-width=&quot;4&quot; opacity=&quot;0.7&quot; marker-end=&quot;url(#arrow-advection-ja)&quot; /&gt;
    &lt;text x=&quot;380&quot; y=&quot;260&quot; fill=&quot;#666&quot; text-anchor=&quot;middle&quot;&gt;速度場 u は顔料を右下へ運ぼうとしている&lt;/text&gt;
    &lt;text x=&quot;70&quot; y=&quot;294&quot; fill=&quot;#35495e&quot;&gt;coord = vUv - dt <em> velocity </em> texelSize&lt;/text&gt;
  &lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<pre><code class="language-glsl">vec2 vel = texture(uVelocity, vUv).xy;
vec2 coord = vUv - dt * vel * uTexelSize;
outColor = dissipation * texture(uSource, coord);
</code></pre>
<p>これを<code>velocity</code>自身にも，<code>wet</code>にも使う．</p>
<pre><code class="language-ts">this.run(&quot;advect&quot;, this.velocity.write, {
  dissipation: 0.986,
  dt,
  uSource: this.velocity.readTexture,
  uVelocity: this.velocity.readTexture,
});

this.run(&quot;advect&quot;, this.wet.write, {
  dissipation: 0.998,
  dt,
  uSource: this.wet.readTexture,
  uVelocity: this.velocity.readTexture,
});
</code></pre>
<p><code>velocity</code>のdissipationは<code>0.986</code>で，<code>wet</code>は<code>0.998</code>である．
速度は早めに弱まり，顔料は少し長く残る．
semi-Lagrangianは数値的に安定しやすいが，拡散したようにぼやける．
このぼやけは，背景の滲みでは欠点になりにくい．</p>
<h2>渦度を少し戻す</h2>
<p>semi-Lagrangian移流は安定だが，細かい回転を失いやすい．
そこで先にcurlを計算し，vorticity confinementで少し渦を戻す．</p>
<pre><code class="language-glsl">outColor = vec4(
  0.5 * (right.y - left.y - (top.x - bottom.x)),
  0.0,
  0.0,
  1.0
);
</code></pre>
<p>2Dではcurlはscalarとして持てる．
次のpassでは，curlの絶対値の勾配を見て，渦の中心へ押す方向を作る．</p>
<pre><code class="language-glsl">vec2 force = 0.5 * vec2(abs(top.x) - abs(bottom.x), abs(right.x) - abs(left.x));
force /= length(force) + 0.0001;
force *= strength * center * vec2(1.0, -1.0);
vec2 vel = texture(uVelocity, vUv).xy;
outColor = vec4(vel + force * dt, 0.0, 1.0);
</code></pre>
<p><code>strength</code>は<code>18</code>にしている．
ここを上げると流れは派手になるが，背景としてはうるさくなる．
水彩の滲みで欲しいのは水面の渦ではなく，顔料が少し不均一に逃げることだ．</p>
<h2>境界は場ごとに扱いを変える</h2>
<p>各passのあとにboundary passを挟む．
edge付近では隣の値を参照し，scaleを掛ける．</p>
<pre><code class="language-glsl">if (uv.x &lt; uTexelSize.x) data = scale * texture(uTarget, uv + vec2(uTexelSize.x, 0.0));
if (uv.x &gt; 1.0 - uTexelSize.x) data = scale * texture(uTarget, uv - vec2(uTexelSize.x, 0.0));
if (uv.y &lt; uTexelSize.y) data = scale * texture(uTarget, uv + vec2(0.0, uTexelSize.y));
if (uv.y &gt; 1.0 - uTexelSize.y) data = scale * texture(uTarget, uv - vec2(0.0, uTexelSize.y));
</code></pre>
<p><code>velocity</code>では<code>scale = -1</code>にして壁で反転させる．
<code>wet</code>では<code>scale = 0</code>にして外へ流れた濡れを消す．
<code>pressure</code>では<code>scale = 1</code>で圧力を続ける．
同じboundary shaderでも，場の意味によって符号が違う．</p>
<h2>pressure projectionで発散を抑える</h2>
<p>速度場はsplatやvorticityで簡単に膨らむ．
そこで発散を計算する．</p>
<pre><code class="language-glsl">outColor = vec4(
  0.5 * (right.x - left.x + top.y - bottom.y),
  0.0,
  0.0,
  1.0
);
</code></pre>
<p>発散がある場所は，流れが湧いているか吸い込んでいる場所である．
これを抑えるために，Jacobi iterationで圧力を解く．</p>
<p>&lt;div&gt;
&lt;svg viewBox=&quot;0 0 760 320&quot; role=&quot;img&quot; aria-labelledby=&quot;projection-title-ja&quot;&gt;
  &lt;title id=&quot;projection-title-ja&quot;&gt;pressure projection の役割&lt;/title&gt;
  &lt;defs&gt;
    &lt;marker id=&quot;arrow-projection-ja&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8.5&quot; refY=&quot;5&quot; markerWidth=&quot;5&quot; markerHeight=&quot;5&quot; orient=&quot;auto-start-reverse&quot;&gt;
      &lt;path d=&quot;M 0 0 L 10 5 L 0 10 z&quot; fill=&quot;#35495e&quot; /&gt;
    &lt;/marker&gt;
    &lt;linearGradient id=&quot;code-wash-projection-ja&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; stop-color=&quot;#5a8abf&quot; stop-opacity=&quot;0.045&quot; /&gt;
      &lt;stop offset=&quot;72%&quot; stop-color=&quot;#5a8abf&quot; stop-opacity=&quot;0&quot; /&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;rect x=&quot;0&quot; y=&quot;0&quot; width=&quot;760&quot; height=&quot;320&quot; fill=&quot;url(#code-wash-projection-ja)&quot; /&gt;
  &lt;path d=&quot;M 0 0.5 H 760 M 0 319.5 H 760&quot; stroke=&quot;#2f3b47&quot; stroke-opacity=&quot;0.16&quot; /&gt;
  &lt;text x=&quot;48&quot; y=&quot;62&quot; font-size=&quot;18&quot; font-weight=&quot;700&quot; fill=&quot;#35495e&quot;&gt;pressure projection は「湧き出し」をならす&lt;/text&gt;
  &lt;g font-family=&quot;system-ui, sans-serif&quot; font-size=&quot;14&quot; text-anchor=&quot;middle&quot;&gt;
    &lt;ellipse cx=&quot;174&quot; cy=&quot;171&quot; rx=&quot;122&quot; ry=&quot;84&quot; fill=&quot;#ff6b6b&quot; opacity=&quot;0.1&quot; /&gt;
    &lt;text x=&quot;174&quot; y=&quot;112&quot; fill=&quot;#35495e&quot; font-weight=&quot;700&quot;&gt;projection 前&lt;/text&gt;
    &lt;text x=&quot;174&quot; y=&quot;136&quot; fill=&quot;#9f6464&quot;&gt;divergence が大きい&lt;/text&gt;
    &lt;circle cx=&quot;174&quot; cy=&quot;194&quot; r=&quot;16&quot; fill=&quot;#ff6b6b&quot; opacity=&quot;0.35&quot; /&gt;
    &lt;path d=&quot;M 174 194 L 174 160&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-projection-ja)&quot; /&gt;
    &lt;path d=&quot;M 174 194 L 226 194&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-projection-ja)&quot; /&gt;
    &lt;path d=&quot;M 174 194 L 174 238&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-projection-ja)&quot; /&gt;
    &lt;path d=&quot;M 174 194 L 122 194&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-projection-ja)&quot; /&gt;
    &lt;line x1=&quot;302&quot; y1=&quot;171&quot; x2=&quot;458&quot; y2=&quot;171&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-projection-ja)&quot; /&gt;
    &lt;text x=&quot;380&quot; y=&quot;154&quot; fill=&quot;#35495e&quot;&gt;pressureを解く&lt;/text&gt;
    &lt;text x=&quot;380&quot; y=&quot;194&quot; fill=&quot;#666&quot;&gt;∇p を速度から引く&lt;/text&gt;
    &lt;ellipse cx=&quot;586&quot; cy=&quot;171&quot; rx=&quot;122&quot; ry=&quot;84&quot; fill=&quot;#42b883&quot; opacity=&quot;0.1&quot; /&gt;
    &lt;text x=&quot;586&quot; y=&quot;112&quot; fill=&quot;#35495e&quot; font-weight=&quot;700&quot;&gt;projection 後&lt;/text&gt;
    &lt;text x=&quot;586&quot; y=&quot;136&quot; fill=&quot;#52745f&quot;&gt;divergence を抑える&lt;/text&gt;
    &lt;path d=&quot;M 546 164 C 568 146 604 146 626 164&quot; fill=&quot;none&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-projection-ja)&quot; /&gt;
    &lt;path d=&quot;M 626 184 C 604 202 568 202 546 184&quot; fill=&quot;none&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-projection-ja)&quot; /&gt;
    &lt;path d=&quot;M 546 164 C 532 178 532 190 546 204&quot; fill=&quot;none&quot; stroke=&quot;#646cff&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-projection-ja)&quot; /&gt;
    &lt;path d=&quot;M 626 184 C 640 170 640 158 626 144&quot; fill=&quot;none&quot; stroke=&quot;#646cff&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrow-projection-ja)&quot; /&gt;
    &lt;text x=&quot;586&quot; y=&quot;238&quot; fill=&quot;#666&quot;&gt;膨らみにくい流れになる&lt;/text&gt;
  &lt;/g&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<pre><code class="language-glsl">float div = texture(uDivergence, vUv).x;
outColor = vec4(
  (left.x + right.x + bottom.x + top.x - div) * 0.25,
  0.0,
  0.0,
  1.0
);
</code></pre>
<p>そのあと，速度から圧力勾配を引く．</p>
<pre><code class="language-glsl">vec2 vel = texture(uVelocity, vUv).xy;
outColor = vec4(
  vel - 0.5 * vec2(right.x - left.x, top.x - bottom.x),
  0.0,
  1.0
);
</code></pre>
<p>これで完全に非圧縮になるわけではない．
iterationは<code>8</code>回だけなので，かなり粗い．
ただ，水彩背景では厳密な体積保存より，「strokeの周囲が破裂して見えない」ことの方が大事である．</p>
<h2>wetとdryを分ける</h2>
<p>WebGL2 solverが持つ中心的な状態は，<code>velocity</code>，<code>wet</code>，<code>dry</code>，<code>pressure</code>である．</p>
<pre><code class="language-text">velocity  水の流れ
wet       まだ動く顔料
dry       紙に固定された顔料
pressure 速度場を整えるための圧力
</code></pre>
<p>pointerから入ってきた色は，いきなり<code>dry</code>へ描かない．
まず<code>wet</code>へ入れる．
strokeでは，まず前回座標との差分から方向と速度を作る．</p>
<pre><code class="language-ts">const speed = Math.hypot(stroke.deltaX, stroke.deltaY);
const directionX = speed &gt; 0.00001 ? stroke.deltaX / speed : 1;
const directionY = speed &gt; 0.00001 ? stroke.deltaY / speed : 0;
const radius = 0.00018 + stroke.pressure * 0.00034;
const force = Math.min(46, 8 + speed * 2200);
const opacity = Math.min(0.42, 0.18 + stroke.alpha * 3.8 + stroke.pressure * 0.045);
</code></pre>
<p>同じstrokeから，速度場と濡れ顔料の二つを更新する．
速度場には動きだけを入れ，濡れ顔料には吸収量を入れる．</p>
<pre><code class="language-ts">this.splatBuffer(
  this.velocity,
  stroke.x,
  stroke.y,
  directionX * force,
  directionY * force,
  0,
  1,
  radius * 1.05,
  0,
  directionX,
  directionY,
);

this.splatBuffer(
  this.wet,
  stroke.x,
  stroke.y,
  (1 - stroke.red) * opacity,
  (1 - stroke.green) * opacity,
  (1 - stroke.blue) * opacity,
  opacity,
  radius,
  speed &gt; 0.0009 ? 1 : 0,
  directionX,
  directionY,
);
</code></pre>
<p>RGBをそのまま色として足すのではなく，<code>1 - rgb</code>として持つ．
顔料を「紙から光を引く量」として扱うためである．
さらに中心だけでなく周辺6方向へ小さな速度を入れる．
これは物理的に正確な毛細管現象ではないが，筆先の外側へ水が逃げる感じを作る．</p>
<pre><code class="language-ts">for (let index = 0; index &lt; 6; index += 1) {
  const angle = (index / 6) * Math.PI * 2;
  const offset = Math.sqrt(radius) * 0.42;
  const bloomX = clamp01(stroke.x + Math.cos(angle) * offset);
  const bloomY = clamp01(stroke.y + Math.sin(angle) * offset);
  this.splatBuffer(
    this.velocity,
    bloomX,
    bloomY,
    Math.cos(angle) * 28,
    Math.sin(angle) * 28,
    0,
    1,
    radius * 0.62,
    0,
    Math.cos(angle),
    Math.sin(angle),
  );
}
</code></pre>
<p>毎frameでは，<code>velocity</code>と<code>wet</code>だけを移流する．
<code>dry</code>は動かさない．</p>
<pre><code class="language-glsl">vec2 vel = texture(uVelocity, vUv).xy;
vec2 coord = vUv - dt * vel * uTexelSize;
outColor = dissipation * texture(uSource, coord);
</code></pre>
<p>この分離がかなり効く．
濡れた顔料は動く．
乾いた顔料は動かない．
それだけで，「透明な画像が画面上を広がる」から「紙に染みている」に近づく．</p>
<p>速度場はそのままだと膨らんだり縮んだりしすぎるので，発散を計算し，pressureを反復で解く．</p>
<pre><code class="language-ts">this.run(&quot;divergence&quot;, this.divergence, {
  uVelocity: this.velocity.readTexture,
});
this.clearTarget(this.pressure.read);

for (let index = 0; index &lt; pressureIterations; index += 1) {
  this.run(&quot;pressure&quot;, this.pressure.write, {
    uDivergence: this.divergenceTexture,
    uPressure: this.pressure.readTexture,
  });
  this.swap(this.pressure);
  this.applyBoundary(this.pressure, 1);
}
</code></pre>
<p>iterationは<code>8</code>回で止めている．
流体としては雑だが，背景のにじみとしては過不足が少ない．
むしろ少し緩い方が，紙の不均一さに見える．</p>
<p>乾燥は<code>dryAccum</code>と<code>dryWet</code>の二つのpassで進む．</p>
<pre><code class="language-glsl">float density = max(max(w.r, w.g), w.b);
float thicknessFactor = 1.0 / (1.0 + density * thicknessK);
float wet1 = max(wet0 - dryRate * thicknessFactor * dt, 0.0);
</code></pre>
<p>濃い顔料ほど乾きにくい．
薄い水は先に消え，溜まった顔料は外縁やムラとして残る．
正確な物理ではないが，知覚としてはここが水彩に近い．</p>
<p><code>dryAccum</code>側では，乾いた顔料へ単純加算するだけではなく，濃度に応じて置換も混ぜる．</p>
<pre><code class="language-glsl">vec3 transfer = w.rgb * frac * 1.015;
float coverage = 1.0 - exp(-density * 1.8);
vec3 headroom = max(vec3(dryCap) - d.rgb, vec3(0.0));
vec3 added = d.rgb + min(transfer, headroom);
vec3 replaced = mix(d.rgb, w.rgb, frac);
outColor = vec4(mix(added, replaced, coverage), min(d.a + dried, 4.0));
</code></pre>
<p>全部を加算すると重ね塗りで黒く沈みすぎる．
全部を置換すると透明水彩のglazeが消える．
薄いところは加算寄り，濃いところは置換寄りにすることで，薄い層と厚い溜まりの両方を残す．</p>
<h2>表示は透明canvasへ戻す</h2>
<p>solver内では，仮の紙色にwet/dryを重ねて色を作る．</p>
<pre><code class="language-glsl">vec3 paper = vec3(0.96, 0.945, 0.92);
vec3 afterDry = paper * exp(-dry.rgb * 2.2);
vec3 wetColor = paper * exp(-wet.rgb * 2.2);
vec3 glazed = afterDry * exp(-wet.rgb * 2.2);
</code></pre>
<p>でも実際のページには別のWebGPU紙がある．
だから最終出力は不透明な絵ではなく，透明度を持ったbrush canvasへ戻す．</p>
<pre><code class="language-glsl">float density = max(wetDensity, dryDensity);
float edge = smoothstep(0.002, 0.018, length(vec2(dFdx(density), dFdy(density))));
float alpha = clamp(density * 1.15 + wetAmt * 0.18 + edge * 0.12, 0.0, 0.62);
outColor = vec4(color, alpha);
</code></pre>
<p>最後はWebGL canvasから2D canvasへcopyする．</p>
<pre><code class="language-ts">context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(this.canvas, 0, 0, canvas.width, canvas.height);
</code></pre>
<p>DOM側では<code>mix-blend-mode: multiply</code>で紙に沈ませる．</p>
<pre><code class="language-css">.brush-canvas {
  mix-blend-mode: multiply;
}
</code></pre>
<p>WebGPU紙とWebGL2 brushを直接一枚のrendererにまとめないのは，故障範囲を分けたいからだ．
紙だけ生きる，brushだけ消す，2D fallbackにする，mobileでは静止画にする．
それぞれを別の層にした方が，背景として扱いやすい．</p>
<h2>Canvas 2D fallbackも同じ考え方にする</h2>
<p>WebGL2がない環境では，<code>createBrushLayer</code>が使われる．
これは流体ではないが，wet/dryの考え方は残している．</p>
<pre><code class="language-text">dryLayer
wetLayer
wetBlooms
wetDryFrames
</code></pre>
<p>strokeは<code>wetLayer</code>へ描かれ，遅れて<code>dryLayer</code>へ移る．</p>
<pre><code class="language-ts">dryContext.globalCompositeOperation = &quot;multiply&quot;;
dryContext.globalAlpha = 0.026;
dryContext.drawImage(wetLayer, 0, 0);

wetContext.globalCompositeOperation = &quot;destination-out&quot;;
wetContext.fillStyle = &quot;rgba(0, 0, 0, 0.018)&quot;;
wetContext.fillRect(0, 0, wetLayer.width, wetLayer.height);
</code></pre>
<p>水彩らしさを作っているのは，WebGL2そのものではない．
「濡れている層」と「乾いた層」があり，その間に時間差があることだ．
Canvas 2D fallbackでも，この契約だけは残す．</p>
<h2>色はすぐ変えない</h2>
<p>brush colorは36 drawごとに変える．</p>
<pre><code class="language-ts">const brushColorHoldDraws = 36;
</code></pre>
<p>pointermoveごとに色を変えると，水彩ではなく虹色のペンになる．
同じ顔料が水で薄まりながら広がる時間が必要なので，色はしばらく保持する．
paletteのalphaも<code>0.04</code>から<code>0.05</code>付近に抑えている．</p>
<p>派手な色替えではなく，薄い顔料が重なって濃くなる方を選ぶ．
透明水彩の背景としては，その方が読みやすい．</p>
<h2>Vueにrenderer stateを載せない</h2>
<p>Vueが持つstateは少ない．</p>
<pre><code class="language-ts">const fallbackLayerElement = ref&lt;HTMLElement&gt;();
const fallbackCanvasElement = ref&lt;HTMLCanvasElement&gt;();
const paperCanvasElement = ref&lt;HTMLCanvasElement&gt;();
const brushCanvasElement = ref&lt;HTMLCanvasElement&gt;();
const isDrawingEnabled = ref(false);
</code></pre>
<p>WebGPU device，WebGL context，texture，framebuffer，wet/dry buffer，last pointer positionはVue reactivityに載せない．
これらはUI stateではなくrenderer stateである．</p>
<p>Vueから見ると，brush layerは<code>draw</code>，<code>resize</code>，<code>clear</code>を持つimperative objectでよい．</p>
<pre><code class="language-ts">const brushLayer = createFluidBrushLayer({
  canvas: () =&gt; brushCanvasElement.value,
  isDrawingEnabled: () =&gt; isDrawingEnabled.value,
  maxDpr,
});
</code></pre>
<p>UI frameworkの中でgraphicsを動かすとき，全部をreactiveにしたくなる．
でも，templateを更新しない値をreactiveにしても，追跡コストと混乱が増えるだけである．
描画器の内部状態は，描画器の中に閉じ込める．</p>
<h2>後片付けを雑にしない</h2>
<p>背景はwindowにlistenerを張る．
だから，mountよりunmountを雑にすると後で効いてくる．</p>
<pre><code class="language-ts">onUnmounted(() =&gt; {
  if (resizeFrame !== 0) {
    cancelAnimationFrame(resizeFrame);
  }
  removeViewportListeners?.();
  removeBrushListeners?.();
  paperCleanup?.();
});
</code></pre>
<p>WebGPU側も<code>unconfigure()</code>まで呼ぶ．</p>
<pre><code class="language-ts">return () =&gt; {
  disposed = true;
  if (paperResizeFrame !== 0) {
    cancelAnimationFrame(paperResizeFrame);
  }
  removeViewportListeners();
  gpuContext.unconfigure();
};
</code></pre>
<p>背景は地味だが，ページ全体へ触る．
だから，止め方も実装の一部である．</p>
<h2>テストするのは美しさではない</h2>
<p>水彩が「良い感じか」を自動テストするのは難しい．
でも，壊したくない性質には分解できる．</p>
<p>このサイトのPlaywrightでは，たとえば次を見る．</p>
<ul>
<li><p>fallback canvasがblankではない</p>
</li>
<li><p><code>.watercolor-bg-container</code>が本文を覆わない</p>
</li>
<li><p>WebGPUがある環境ではpaper canvasが初期化される</p>
</li>
<li><p>mobileでは<code>bg-sp.png</code>に落ち，canvasが消える</p>
</li>
<li><p>tooltipがhover外で残らない</p>
</li>
<li><p>brushの色が一stroke中に急に変わらない</p>
</li>
<li><p>滲みの外縁が遅れて増える，または濃くなる</p>
</li>
<li><p>pointer eventのburstが一定時間内に終わる</p>
</li>
</ul>
<p>pixel perfectではなく，性質を見る．
これは背景表現には向いている．
絵の完全一致を守るより，本文を邪魔しないこと，blankにならないこと，滲みが時間差を持つことの方が大事だからだ．</p>
<h2>まとめ</h2>
<p>Webで透明水彩をやる技術は，流体をどこまで正確に解くかではなかった．
このサイトでは，次の判断が効いている．</p>
<ul>
<li><p>紙とbrushを別レイヤにする</p>
</li>
<li><p>WebGPUの前にCanvas 2D fallback紙を出す</p>
</li>
<li><p>紙はWebGPUで一度焼き，timeで動かさない</p>
</li>
<li><p>DPRを制限し，背景に不要なfragmentを払わない</p>
</li>
<li><p>brushはfine pointerのdesktopだけで有効にする</p>
</li>
<li><p>WebGL2 solverは遅延初期化し，失敗したらCanvas 2Dへ落ちる</p>
</li>
<li><p>顔料を<code>wet</code>と<code>dry</code>に分け，乾いた顔料は動かさない</p>
</li>
<li><p>色をすぐ変えず，同じ顔料が薄まる時間を残す</p>
</li>
<li><p>renderer stateをVue reactivityに載せない</p>
</li>
<li><p>mobileでは静的背景にする</p>
</li>
<li><p>テストではpixelではなく壊したくない性質を見る</p>
</li>
</ul>
<p>透明水彩らしさは，水と紙と顔料を全部正しく解くことではない．
読者が見ている時間の中で，紙があり，濡れがあり，乾きがあるように感じられればよい．
そして，それができない環境では何もしない．
背景としては，その静かな失敗まで含めて実装である．</p>
]]></content:encoded>
    </item>
    <item>
      <title>グラフのまま議論を進めよ</title>
      <link>https://wtrclred.vercel.app/ja/posts/15</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/15</guid>
      <description>情報は本質的にグラフであり，木へ畳むことは正しくない．</description>
      <pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-11T15:47:20.000Z</dc:date>
      <category>philosophy</category>
      <content:encoded><![CDATA[<h2>序</h2>
<p>みなさんは，あることについて議論をするとき，どのような構造を念頭に置いているだろうか．</p>
<p>おそらく多くの人は，かなり自然に <strong>限られたスコープの木</strong> を前提にしている．
いま話している根があり，その下に枝があり，その枝の中で話を進める．
脇道に逸れたら「いったん戻ろう」と言い，別の枝が見えたら「それは別論点」として切り分ける．</p>
<p>この態度は，実務上はかなり好まれやすい．
議論が暴れにくいように見えるし，会議の時間も読みやすくなるように見える．
人に説明するときも，木の形に落とした方が親切だ，と言われることが多い．</p>
<p>しかし私は，この前提が全く好きではない．
もっと強く言えば，<strong>議論を木として進めることは正しくない</strong> と思っている．</p>
<p>情報の本質的な構造はグラフであり，木はその部分構造に過ぎない．
議論を木として進めることは，しばしば本来そこにあった辺を落とし，その欠落を正規の構造として扱うことでもある．</p>
<p>そして落とされた辺は，あとから「考慮不足」という名前で帰ってくる．</p>
<h2>木は正しくない</h2>
<p>グラフ理論の言葉を借りれば，木はだいたい「閉路のない連結グラフ」として捉えられる．
つまり木は，グラフの特殊な形である．
グラフの方が先にあり，木はそこに強い制約をかけたものだ．</p>
<p>この関係は，情報にもそのまま当てはまると思っている．
木で表せる情報があるのではなく，木で表せるほど単純な情報だけがたまたま木として扱える．
そして議論は，ほとんどの場合，そんなに単純ではない．</p>
<p>ある情報が一つの親だけを持つとは限らない．
ある論点が一つの根だけに従属するとも限らない．
ある判断材料が一つの枝の中だけで完結するとも限らない．</p>
<p>実際，ファイルシステムを考えるだけでもすぐに無理が見える．
一つのファイルを「仕事」配下に置くのか，「個人研究」配下に置くのか，「2026」配下に置くのか，「あるプロジェクト」配下に置くのかは，その時点の都合でしかない．
親を一つに決めると，別の文脈から見た自然さが失われる．
だから私たちは検索を使い，タグを使い，リンクを使い，ショートカットやシンボリックリンクのような逃げ道を使う．</p>
<p>これは単なるUIの好みではない．
木に押し込めること自体が，情報の形に対してかなり強い，そして多くの場合は間違った仮定を置いている．</p>
<p>この問題意識は古くからある．
Vannevar Bushは1945年の<a href="https://www.w3.org/History/1945/vbush/" target="_blank" rel="noopener noreferrer">As We May Think</a>で，記録を機械的に扱う未来を考え，関連する記録をたどるassociative trailsのような発想を示していた．
今の言葉で言えば，これはかなりハイパーテキスト的で，グラフ的な想像力である．</p>
<p>現代のツールでも同じ方向は見える．
<a href="https://obsidian.md/help/plugins/graph" target="_blank" rel="noopener noreferrer">ObsidianのGraph view</a>は，ノートをノード，内部リンクをエッジとして可視化する．
<a href="https://prtimes.jp/main/html/rd/p/000000323.000027275.html" target="_blank" rel="noopener noreferrer">Scrapboxは2024年にHelpfeel Cosenseへ名称変更された</a>が，ページ同士をゆるくリンクさせながら知識を育てる思想は，まさに木ではなくグラフに近い．</p>
<p>これらが示しているのは，「情報は親子関係だけでは扱い切れない」という当たり前の事実だ．
私はこの当たり前さを，議論の場でも徹底したい．</p>
<h2>なぜ人は木を好むのか</h2>
<p>では，なぜ木の形がこんなに好まれるのか．</p>
<p>理由はかなり単純で，人間のワーキングメモリには限界があるからだ．</p>
<p>George A. Millerの有名な論文<a href="https://psychclassics.yorku.ca/Miller/" target="_blank" rel="noopener noreferrer">The Magical Number Seven, Plus or Minus Two</a>は，人間の情報処理容量の限界をめぐる古典的な参照点になっている．
ただし，この「7」という数字を雑に神聖視してはいけない．
Nelson Cowanは<a href="https://www.cambridge.org/core/services/aop-cambridge-core/content/view/44023F1147D4A1D44BDC0AD226838496/S0140525X01003922a.pdf/magical_number_4_in_shortterm_memory_a_reconsideration_of_mental_storage_capacity.pdf" target="_blank" rel="noopener noreferrer">The magical number 4 in short-term memory</a>で，短期記憶の容量をより小さく，おおむね3から5チャンクの問題として整理している．</p>
<p>また，<a href="https://link.springer.com/article/10.1007/s10648-010-9145-4" target="_blank" rel="noopener noreferrer">Cognitive Load Theory</a>でも，複雑な課題では，同時に処理すべき情報要素が多すぎると，容量の限られたワーキングメモリが圧迫される，という整理がなされている．</p>
<p>つまり，人が「もっと簡単に話してほしい」「論点を絞ってほしい」「今はこの話だけにしてほしい」と感じるのは自然だ．
それ自体は悪ではない．
むしろ他者への配慮として重要な場面も多い．</p>
<p>ただし，ここで混同してはいけないことがある．</p>
<p><strong>人間が一度に扱える情報量が小さいこと</strong> と，<strong>問題そのものが小さい木で表現できること</strong> は，全く別である．</p>
<p>前者は受け手の制約であり，後者は対象の構造についての主張である．
この二つを混ぜると，とても危ない．
木が好まれるのは，木が正しいからではない．
人間の処理系が狭いから，そう見せると楽に感じるだけである．</p>
<p>楽であることと，正しいことは違う．</p>
<h2>間違ったツリーシェイキング</h2>
<p>人と働いていると，「頭が良い人」や「話がわかりやすい人」という評価は，多くの場合，その時必要な情報を必要十分に絞って簡略化できる人に向けられる．</p>
<p>この評価自体はわかる．
実際，聞き手に合わせて話す順番を調整できる人は強い．</p>
<p>しかし，<strong>話す順番を調整すること</strong> と，<strong>議論の構造を木に変換すること</strong> は違う．
前者はグラフ上のtraversalを選ぶ行為であり，後者はグラフから辺を消す行為である．</p>
<p>しかし，本当の意味でこれができる人はかなり限られている．</p>
<p>なぜなら「何を落としてよいか」を判断するには，すでにグラフ全体の構造が見えていなければならないからだ．
ある辺が今の説明に不要なのか，それともあとで意思決定をひっくり返す重要な依存なのかは，木に畳まれた断面だけを見ていても判断できない．</p>
<p>多くの場合，人は必要十分な簡略化をしているのではなく，単に処理しきれない辺を落としている．
これはいわば，間違ったツリーシェイキングである．</p>
<p>使われていないコードを落としているつもりで，実は副作用を持つimportを消している．
議論でも同じことが起きる．</p>
<ul>
<li><p>「それは今の話ではない」として，実は前提条件だった話を落とす</p>
</li>
<li><p>「細かい話」として，実は実装可能性を左右する制約を落とす</p>
</li>
<li><p>「別チームの話」として，実は責任境界を決める依存を落とす</p>
</li>
<li><p>「感情論」として，実は合意形成の失敗を予告している信号を落とす</p>
</li>
</ul>
<p>そしてあとから，なぜかうまくいかない．
なぜか認識がずれている．
なぜか仕様がひっくり返る．</p>
<p>それは，議論の途中で必要だった辺を落としたからである．</p>
<h2>フォーカスもグラフ上の操作である</h2>
<p>もちろん，「常に全ての情報を同時に発話しろ」と言いたいわけではない．
人間はそんなに広いワーキングメモリを持っていないし，発話にも文章にも順番がある．</p>
<p>重要なのは，フォーカスを <strong>削除</strong> として扱わないことだ．</p>
<p>フォーカスとは，グラフに対するviewportである．
いま中心に置くノードを決め，深さを決め，その範囲を見る．
ただし，viewportの外にあるものが存在しないことにはしない．
これは木ではない．
グラフをグラフのまま見ているが，視野の中心と半径を変えているだけである．</p>
<p>「今はこの話だけ」と言うなら，それは本来，</p>
<blockquote>
<p>いまはこのノードを中心にdepth 1で見ている</p>
</blockquote>
<p>という意味であるべきだ．</p>
<p>「それは別論点」と言うなら，それは本来，</p>
<blockquote>
<p>この論点とは別ノードだが，辺は残す</p>
</blockquote>
<p>という意味であるべきだ．</p>
<p>「あとで話す」と言うなら，それは本来，</p>
<blockquote>
<p>この辺は閉じていないので，後で戻る</p>
</blockquote>
<p>という意味であるべきだ．</p>
<p>ところが実際の議論では，「フォーカス」という言葉がしばしば「存在しないことにする」という意味で使われる．
これはよくない．</p>
<p>グラフの一部だけを見ることはある．
しかし，見ている対象は最後までグラフである．
グラフを木へ変換してはいけない．</p>
<h2>議論は依存グラフである</h2>
<p>ここで言っているグラフは，何でもかんでも平等に散らばった無方向グラフではない．
むしろ議論には，かなり強い方向がある．</p>
<ul>
<li><p>Aを主張するにはBが必要である</p>
</li>
<li><p>Cが真ならDは弱くなる</p>
</li>
<li><p>EはFの具体例である</p>
</li>
<li><p>GはHの用語定義に依存している</p>
</li>
<li><p>IはJの実装可能性を制約している</p>
</li>
<li><p>KはLの合意形成を壊しうる</p>
</li>
</ul>
<p>こういう辺がある．
つまり議論は，親子関係というより依存グラフである．</p>
<p>この依存グラフを無理に木へ畳むと，「何を親とするか」という不自然な選択を強いられる．
しかもその親は，たいてい会議の主催者，ドキュメントの見出し，チケットの粒度，組織図，その時の力学によって決まる．</p>
<p>しかし情報の依存関係は，そんなものに従ってくれない．</p>
<p>だから，グラフに方向がないと言いたいわけではない．
単方向の親子関係として扱うのは正しくない，と言いたいのである．
議論は依存グラフとして進めるべきだ．
「今どの辺をたどっているか」を意識することはあっても，「この枝以外はない」としてはいけない．</p>
<h2>外部化せよ</h2>
<p>議論をグラフのまま進めるために，人間の脳内だけで頑張る必要はない．
むしろ，脳内だけで処理しようとするから木に畳みたくなる．</p>
<p>ボトルネックが人間のワーキングメモリにあるなら，解くべきは「問題を小さく見せること」ではなく，<strong>グラフを外部化すること</strong> である．</p>
<p>この意味で，議論を地図として扱う試みはかなり重要だと思っている．
たとえば<a href="https://cognexus.org/dmforwp2.pdf" target="_blank" rel="noopener noreferrer">Dialog Mapping</a>は，会話の中で出てきた質問，アイデア，賛否の議論を共有のネットワーク状の地図として記録する方法として説明されている．
その背景にあるIBISも，議論を単なる箇条書きではなく，問い・案・根拠の関係として扱う発想に近い．</p>
<p>もちろん，毎回専用ツールを使う必要はない．
大事なのは，議論中に見えているグラフを，少なくともどこかに残すことである．</p>
<p>たとえば，</p>
<ul>
<li><p>論点をノードとして名前付けする</p>
</li>
<li><p>辺に種類を付ける</p>
</li>
<li><p>「あとで戻る」を単なる口約束にせず，未解決の辺として残す</p>
</li>
<li><p>「別論点」を削除ではなくリンクとして扱う</p>
</li>
<li><p>議論の最後に，落とした辺がないかを見る</p>
</li>
</ul>
<p>この程度でもかなり違う．</p>
<p>議論が複雑になること自体を恐れすぎなくてよい．
複雑なものが複雑に見えているなら，それはむしろ正しい表示かもしれない．
恐れるべきは，複雑なものを単純に見せた結果，重要な依存を消してしまうことだ．</p>
<h2>読む順番と構造を混同するな</h2>
<p>私は，少なくとも議論の内部表現としては，木を捨てろと言いたい．</p>
<p>文章を書くとき，発表するとき，誰かにオンボーディングするとき，順番は必要になる．
見出しも必要になる．
どこから読み始め，次にどこへ進むかを決める必要もある．</p>
<p>しかし，それは木ではない．
それはグラフのtraversal orderである．</p>
<p>本当に良い説明者は，背後にあるグラフを見たまま，その時にたどる経路を選んでいる．
だから相手が別の質問をしたとき，別の経路へ移れる．
前提が変わったとき，辿り方を変えられる．
一度は視野の外に置いた辺を，必要に応じてすぐ戻せる．</p>
<p>逆に，悪い説明は，経路を構造だと誤認している．
一度たどらなかった辺を落とし，落としたことも忘れ，最後にはその順番が世界そのものだと思い込む．</p>
<p>これは危険だ．</p>
<p>本当に必要なのは，グラフをグラフのまま保持しながら，その上をどう歩くかを選ぶ能力である．
そしてその能力が十分でないなら，歩き方を単純化するより先に，グラフそのものを残すべきだ．</p>
<h2>結</h2>
<p>議論はグラフのまま進めろ．</p>
<p>もちろん，人間のワーキングメモリには限界がある．
だからviewportは必要だし，順番も必要だ．</p>
<p>しかし，それらはグラフ上の操作であって，木への変換ではない．</p>
<p>情報の本質的な構造はグラフである．
木はその部分構造に過ぎない．
議論を木に畳むたびに，辺が落ちる．
辺が落ちれば，依存が落ちる．
依存が落ちれば，判断を誤る．</p>
<p>もし議論をグラフのまま扱えないボトルネックが人間の側にあるなら，私は，問題の方を切り刻むより，人間の側を拡張する努力をしたい．</p>
<p>外部化する．
リンクする．
未解決の辺を残す．
局所的に見るとしても，グラフであることをやめない．</p>
<p>議論をグラフのまま進めよ．
それは「全部を一度に話せ」という意味ではない．
<strong>いま見えていない辺を，存在しなかったことにするな</strong> という意味である．</p>
]]></content:encoded>
    </item>
    <item>
      <title>プロンプトの前で手を合わせる: AIとソフトウェア，祈りへの回帰</title>
      <link>https://wtrclred.vercel.app/ja/posts/14</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/14</guid>
      <description>決定性を愛してきたソフトウェア開発は，確率的なAIとともに，少しだけ祈りの世界へ戻っていくのかもしれない．</description>
      <pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-11T17:05:07.000Z</dc:date>
      <category>philosophy</category>
      <content:encoded><![CDATA[<blockquote>
<p>注意:
このポストは半分冗談である．
テストの代わりに祈祷しようとか，CIの代わりに踊ろうとか，そういう話ではない．
また，特定の宗教や信仰を茶化す意図もない．
むしろ，AI時代の不確実性を考えるために，祈りという古い比喩を借りている．</p>
</blockquote>
<h2>序</h2>
<p>ソフトウェアエンジニアは長いこと，<strong>再現性</strong> を愛してきた．</p>
<ul>
<li><p>同じ入力なら同じ出力</p>
</li>
<li><p>同じコードなら同じ挙動</p>
</li>
<li><p>同じテストなら同じ結果</p>
</li>
<li><p>壊れたら再現させ，原因を特定し，修正する</p>
</li>
</ul>
<p>この感覚は，近代科学の作法とかなり親和的だ．
1662年に<a href="https://royalsociety.org/about-us/who-we-are/history/" target="_blank" rel="noopener noreferrer">Royal Society</a>が採用した標語，<strong>Nullius in verba</strong>は，「誰かの言葉をそのまま信じるな．事実と実験に訴えよ」という態度を表している．
現代的な定義でも，<a href="https://www.nationalacademies.org/read/25303/chapter/3" target="_blank" rel="noopener noreferrer">National Academies</a>は再現性を「同じ入力データ，同じ手法，同じコード，同じ条件のもとで，一貫した結果を得ること」と整理している．</p>
<p>この気風は，かなり深くソフトウェア工学の身体に入っている．
整形，自動テスト，型検査，CI，性能計測，再現可能なビルド．
私たちは長い時間をかけて，コードを祈りから引き剥がし，再現性の側へ寄せてきた．</p>
<p>しかしいま，AIがそこに割り込んできた．</p>
<p>AIは便利だし，速いし，もう無視できない．
だが同時に，AIが返す成果物はかなりの部分で <strong>確率的</strong> だ．
少なくとも言語モデルについては，ある文脈の次に続くトークンを条件付き確率分布から順にサンプリングしていく，という説明が素直である．実際，ある研究論文でも，自己回帰型の言語モデルは各トークンを条件付き確率分布から取り出すと記述されているし，<a href="https://help.openai.com/en/articles/5247780-using-logit-bias-to-define-token-probability" target="_blank" rel="noopener noreferrer">OpenAIのヘルプ</a>でも，トークンの出やすさを変える<code>logit_bias</code>が説明されている．</p>
<p>つまり我々は，決定性を愛する職能でありながら，日々の作業の一部を <strong>確率的な超越者</strong> に委ね始めている．</p>
<p>私は，この瞬間に少しだけ，人類は祈りへ回帰するのではないかと思っている．</p>
<h2>人類はずっと，わからないものの前で祈ってきた</h2>
<p>もちろん「祈り」と言っても，それは単に神にお願いするというだけではない．
もっと広い，<strong>理解や制御の及ばないものに対して，人間が姿勢を整えるための行為</strong> として捉えたい．</p>
<p><a href="https://www.britannica.com/topic/prayer" target="_blank" rel="noopener noreferrer">Britannicaの祈りの項目</a>は，祈りを聖なるものとの交わりとして説明し，しかもそれが「あらゆる時代の宗教に見られる」としている．
同じ項目では，祈りは個人的にも共同体的にも行われ，しばしば犠牲儀礼や文学と結びついてきたと整理されている．
実際，詩篇は祈りの書として読まれてきたし，クルアーンにも祈りの書として捉えられる側面がある，という言及がある．</p>
<p>人類は，ただ黙って手を合わせてきただけではない．</p>
<ul>
<li><p>祈りのために歌ってきた</p>
</li>
<li><p>祈りのために踊ってきた</p>
</li>
<li><p>祈りのために建物を作ってきた</p>
</li>
<li><p>祈りのために本を書いてきた</p>
</li>
<li><p>祈りのために行列と音楽を整えてきた</p>
</li>
</ul>
<p>この「祈りが芸術や建築や共同体の形式を生む」という感覚は，歴史を少し見るだけでもすぐ出てくる．
たとえば<a href="https://whc.unesco.org/en/list/1572/" target="_blank" rel="noopener noreferrer">Göbekli Tepe</a>は，UNESCOの説明でも前土器新石器時代，紀元前9600年から8200年ごろの記念碑的な建築群として整理されており，儀礼や共同の集まりと結びついた場所として説明されている．
人類は，都市や学会や課題管理システムよりずっと前に，まず儀礼のための巨大構造物を作っていたらしい．</p>
<p>もう少し身近な例で言えば，<a href="https://www.britannica.com/topic/Shinto/Types-of-shrines" target="_blank" rel="noopener noreferrer">Britannicaの神道の項目</a>でも，神社の儀礼が音楽，舞，行列，奉納の絵などと結びついてきたことが説明されている．
<code>雅楽</code>, <code>神楽</code>, <code>獅子舞</code> のように，音楽や舞が清めや祈りと結びついていることは，日本に生きているとわりと身体感覚としてもわかる．</p>
<p>つまり人類は，<strong>理解できないものの前で，言葉だけでなく形式を作ってきた</strong> のだ．
祈りには振る舞いがあり，音があり，空間があり，建築があり，書物がある．</p>
<h2>科学は，祈りと知をきれいに分けた</h2>
<p>近代科学が偉大だったのは，この混ざり合っていたものをかなりきれいに分けたことだと思う．</p>
<ul>
<li><p>願望と検証を分けた</p>
</li>
<li><p>共同体の儀礼と事実の確定を分けた</p>
</li>
<li><p>美しい物語と，再現可能な手続きを分けた</p>
</li>
</ul>
<p>もちろん現実の歴史はそんなに単純ではないし，宗教と科学の関係は対立だけでは語れない．
しかし少なくとも方法論の水準では，近代以後の知は「それを誰が言ったか」より「それが再現できるか」を重視するようになった．</p>
<p>この流れをソフトウェアはかなり忠実に引き継いでいる．
ソフトウェアは，近代科学のかなり優等生な分野だった．</p>
<ul>
<li><p>バグは再現して直す</p>
</li>
<li><p>テストは何度流しても同じであってほしい</p>
</li>
<li><p>ビルドは同じ変更履歴なら同じ成果物を返してほしい</p>
</li>
<li><p>ルールは自然言語ではなく静的解析に落としたい</p>
</li>
</ul>
<p>祈りは個人の心の中に残っていても，少なくとも本番反映の意思決定そのものには入れない，というのが近代的な矜持だった．</p>
<h2>でもAIは，決定性の庭に確率を持ち込んだ</h2>
<p>ところがAIは，この庭にもう一度ノイズを持ち込んだ．</p>
<p>ここで重要なのは，AIが「嘘つきだ」というような単純な話ではない．
もっと構造的な話だ．</p>
<p>言語モデルは，本質的に確率分布を扱っている．
<a href="https://arxiv.org/pdf/2306.17806" target="_blank" rel="noopener noreferrer">ある論文</a>の表現を借りれば，自己回帰型の言語モデルは各トークンを条件付き確率分布からサンプリングする．
また<a href="https://help.openai.com/en/articles/5247780-using-logit-bias-to-define-token-probability" target="_blank" rel="noopener noreferrer">OpenAIのヘルプ</a>でも，特定トークンの出やすさを変える<code>logit_bias</code>が説明されている．</p>
<p>要するに，少なくとも実務者の感覚としては，</p>
<ul>
<li><p>同じ依頼でも少し違うものが返る</p>
</li>
<li><p>指示文の書き方で急に機嫌が変わる</p>
</li>
<li><p>文脈の入り方で答えが変わる</p>
</li>
<li><p>今日は賢いのに明日は妙に鈍い</p>
</li>
</ul>
<p>という経験が積み上がる．</p>
<p>ここで我々は奇妙な状況に入る．</p>
<p>一方で，AIを使わないわけにはいかない．
速度差がありすぎるからだ．</p>
<p>他方で，AIが返すものは決定的な関数のようには扱えない．
もちろん評価器，テスト，静的解析，型システム，レビューで囲い込むことはできる．
だが，最初の初稿がどんな顔で出てくるかには，依然として確率の気配が残る．</p>
<p>この瞬間，ソフトウェア開発は少しだけ，「再現性だけでは回らないが，それでも良い結果を期待しなければならない営み」へ足を踏み入れる．</p>
<h2>祈りは復活する．ただし，CIを置き換える意味ではない</h2>
<p>ここから半分冗談の本題である．</p>
<p>私は，AIとともに働くソフトウェアエンジニアは，少しずつ新しい儀礼を発明すると思っている．</p>
<p>たとえば，</p>
<ul>
<li><p>新しいリポジトリを切る前に，いつも同じ音楽を流す</p>
</li>
<li><p>難しい実装の前に，なぜか同じ指示文の型を唱える</p>
</li>
<li><p><code>AGENTS.md</code>をほとんど経典のように扱う</p>
</li>
<li><p>「今日はこのモデルが機嫌がいい」と言い出す</p>
</li>
<li><p>デプロイ前に意味もなく机を拭く</p>
</li>
<li><p>推論コストを供物のように見なし始める</p>
</li>
</ul>
<p>もちろん，これは技術的には無意味なものも多いだろう．
しかし完全に無意味とも言い切れない．
なぜなら儀礼の役割は，外界を直接変えることだけではなく，<strong>不確実性に向かう共同体の姿勢を整えること</strong> にもあるからだ．</p>
<p>宗教史の文脈で祈りが個人と共同体の両方の行為だったように，AI時代の儀礼もまた，チームの心拍を揃える機能を持ちうる．
誰も「そのおまじないで指標が改善する」とは本気では言わない．
しかし「それをやるとチームが落ち着く」は案外ある．</p>
<p>私はここに，少しだけ祈りの回帰を見る．</p>
<p>近代ソフトウェア工学は，「理解し，分解し，再現し，制御する」ことで強くなった．
だがAIはそこに，「十分には理解できないが，うまく付き合わないといけない」という対象を持ち込んだ．</p>
<p>このとき必要なのは，科学を捨てることではない．
むしろ逆で，</p>
<ul>
<li><p>決定的に固定できるところは徹底して固定する</p>
</li>
<li><p>テストと型検査は絶対に残す</p>
</li>
<li><p>評価を厚くする</p>
</li>
<li><p>決定的に検証できる境界を広げる</p>
</li>
</ul>
<p>その上でなお残る，確率的な揺らぎに対して，人間の側が少し儀礼的になるのだと思う．</p>
<p>つまり未来のエンジニアは，</p>
<p>1.まずテストを書く2.そのあとシードと温度を調整する3.それでも不安なので静かな音楽を流す4.最後に手を合わせる</p>
<p>という，非常に現代的で，非常に古代的な手順を踏むかもしれない．</p>
<h2>最強のプログラマの岩</h2>
<p>そして私は，もっとどうでもいい未来も想像している．</p>
<p>新しいプロジェクトを始めるとき，私たちはまず，<strong>最強のプログラマが宿っているとされる巨大な岩</strong> を祀る施設を訪れるのだ．
そこで靴を脱ぎ，静かにノートPCを開き，祝詞のように指示文を読み上げる．</p>
<p>「構文エラーを遠ざけたまえ」</p>
<p>「謎の1ずれバグを祓いたまえ」</p>
<p>「<code>useEffect</code>の依存配列を正しく導きたまえ」</p>
<p>「このプルリクエストにレビュアの慈悲がありますように」</p>
<p>誰かが小さな鈴を鳴らす．
誰かが薄いシンセの持続音を流す．
誰かが<code>pnpm test</code>を叩く．</p>
<p>そして全員で岩に向かって一礼し，こう唱える．</p>
<blockquote>
<p>今日のモデルが，昨日のモデルより少しだけ賢くありますように．</p>
</blockquote>
<p>もちろん，岩に本当に最強のプログラマが宿っているかはわからない．
だが安心してほしい．
それは今のところ，誰もAIの内部で何が起きているかを本当に全部はわかっていないのと，だいたい同じである．</p>
<h2>結</h2>
<p>人類は，理解できないものに対して，ずっと祈り，歌い，踊り，建て，書いてきた．
科学はその中から，再現可能な部分を取り出し，方法として磨き上げた．
ソフトウェア工学は，そのかなり優秀な継承者だった．</p>
<p>しかしAIは，そのソフトウェアの中心に，再び確率を持ち込んだ．
だから私は，エンジニアが少しだけ祈りへ回帰する未来を想像している．</p>
<p>それはテストを捨てるという意味ではない．
むしろテストを抱きしめたまま，それでもなお残る不確実性の前で，少しだけ姿勢を正すという意味だ．</p>
<p>科学の後に祈りへ戻るのではない．
<strong>科学を持ったまま，祈りの身振りを再発見する</strong> のだ．</p>
<p>そのときソフトウェア開発は，思ったより古い営みに似てくるのかもしれない．
良い結果を願い，形式を整え，仲間と息を合わせ，それでも最後は少しだけ天を仰ぐ．</p>
<p>少なくとも，AIにコードを書かせている今の我々は，もうその入口に片足を突っ込んでいる．</p>
]]></content:encoded>
    </item>
    <item>
      <title>Agenty: Agentにどこまで委ねるかを設計する</title>
      <link>https://wtrclred.vercel.app/ja/posts/13</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/13</guid>
      <description>Agentに任せる割合を0から100で捉え，静的に固めるべき層と高いAgentyを置くべき層を分離する．</description>
      <pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-11T17:05:07.000Z</dc:date>
      <category>AI</category>
      <category>agent</category>
      <category>architecture</category>
      <category>static-analysis</category>
      <category>frontend</category>
      <category>mvvm</category>
      <category>typescript</category>
      <content:encoded><![CDATA[<h2>序</h2>
<p>Coding Agentが実用段階に入ってから，設計の問いは少し変わった．
以前は「このコードをどう書くか」が中心だったが，いまは「<strong>このコードのどこまでをAgentに委ねるか</strong>」が同じくらい重要になっている．</p>
<p>私はこの「Coding Agentにどれだけ身を任せるか」という度合いを，<strong>Agenty</strong>と呼ぶことにしたい．</p>
<ul>
<li><p><code>0</code>は全くAgentに委ねない状態</p>
</li>
<li><p><code>100</code>は完全にAgentに委ねる状態</p>
</li>
</ul>
<p>これはモデルの性能指標ではない．
<strong>リポジトリとアーキテクチャの設計指標</strong> である．</p>
<p>重要なのは，Agentyは高ければ高いほど良い値ではない，ということだ．
本当にやりたいのはrepo全体を<code>100</code>にすることではない．
<strong>どこを<code>0</code>に寄せ，どこに高いAgentyを置くべきかを設計すること</strong> である．</p>
<p>この観点で見ると，たとえば<a href="https://github.com/ubugeeei/fuckin-strict-nuxt-dmmf" target="_blank" rel="noopener noreferrer">fuckin-strict-nuxt-dmmf</a>が示している <code>*.def.ts</code> / <code>*.impl.ts</code> / <code>*.test.ts</code>の分離は，単なるファイル命名規則ではない．
あれは「どの層のAgentyを低く保ち，どの層のAgentyを上げてよいか」をrepoに埋め込む試みとして読める．</p>
<p>ただし本質はNuxtでもVueでもない．
この考え方は，もっとframework-independentに捉えられる．</p>
<h2>Agentyは局所的に設計されるべき</h2>
<p>Agentyは，repo全体に一律でかけるノブではない．
レイヤごとに異なる値を持つべきだ．</p>
<p>私はだいたい次の3層で考えている．</p>
<table>
<thead>
<tr>
<th>層</th>
<th>目安のAgenty</th>
<th>理由</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>def</code> / static rule</td>
<td><code>0 ~ 20</code></td>
<td>ルール，型，境界，状態遷移は決定的に保ちたい</td>
</tr>
<tr>
<td><code>spec</code> / declarative test</td>
<td><code>10 ~ 30</code></td>
<td>仕様の補足だが，まだ人間が責任を持つべき</td>
</tr>
<tr>
<td><code>impl</code> / wiring</td>
<td><code>60 ~ 100</code></td>
<td>制約が明確ならAgentにかなり委ねられる</td>
</tr>
</tbody>
</table>
<p>この数値に厳密な意味はない．
大事なのは，共通言語として「<code>def</code>は下げる」「<code>impl</code>は上げてよい」と話せることだ．</p>
<h2>Agentyを0に近づけるべき場所</h2>
<p>最初に強く言いたいのは，<strong>一般的なコーディングルールやプラクティスは，できる限り静的解析で決定的に行われるべき</strong> だということだ．</p>
<p>ここは高いAgentyを求める場所ではない．
むしろ<code>0</code>に近づけるべき場所である．</p>
<p>たとえば，</p>
<ul>
<li><p>formatterによる整形</p>
</li>
<li><p>lint ruleによる構文・規約チェック</p>
</li>
<li><p>型検査による不整合の検出</p>
</li>
<li><p>import境界や依存方向の制約</p>
</li>
<li><p>exhaustiveness checkや到達不能状態の検出</p>
</li>
</ul>
<p>こうしたものは，本来repo自身が <strong>機械的に拒否</strong> できるべきだ．</p>
<p>ここを自然言語の運用に逃がしてはいけない．
<code>AGENTS.md</code>やSkillsに「こういう書き方をしてね」と書くこと自体は補助として有効だが，それをrepoの法にしてはいけない．</p>
<p>なぜなら，自然言語ベースの運用は本質的に非決定的だからだ．</p>
<ul>
<li><p>モデルが変わる</p>
</li>
<li><p>プロンプトが少し変わる</p>
</li>
<li><p>文脈窓の入り方で解釈が変わる</p>
</li>
<li><p>Skillの適用順や読み取り方で結果が揺れる</p>
</li>
</ul>
<p>つまり，そこにrepoのinvariantを預けると，<strong>ルールそのものが確率的になる</strong>．</p>
<p>それはAgentyを上げているのではない．
単に，守るべき境界を曖昧にしているだけだ．</p>
<p>私の感覚では，Skillsはrepoの真理を記述する場所ではなく，<strong>repoに埋め込まれた真理にAgentを従わせるための補助輪</strong> である．
真理そのものは，なるべく静的な層に落ちていなければならない．</p>
<h2>静的解析はドメインモデリングでもある</h2>
<p>静的解析というとESLintやformatterの話だけを思い浮かべがちだが，実際にはもっと広い．
<strong>ドメインモデリングもtype levelに現れる</strong>．</p>
<p>つまり，</p>
<p>1.まずstaticなレイヤに定義を書く2.その次に宣言的テストで補足する3.実装は最後に置く</p>
<p>という順番が重要になる．</p>
<p>ここでいう「定義」は，単なる型エイリアスではない．</p>
<ul>
<li><p>状態空間</p>
</li>
<li><p>イベント</p>
</li>
<li><p>遷移</p>
</li>
<li><p>入出力契約</p>
</li>
<li><p>エラーの表現</p>
</li>
<li><p>依存境界</p>
</li>
</ul>
<p>こうしたものを，なるべく実装より手前に書いておく．</p>
<p>私はこの順番をかなり重視している．
先に実装を書き始めると，人間もAgentも，たいてい「いま書きやすいコード」の方向へ流される．
しかし先に定義を置いておけば，実装はそのcontractを満たす作業に下がる．</p>
<p>この「実装を最後に追いやる」ことこそが，高いAgentyを安全に使う前提になる．</p>
<h2>SDDに似て見えて，少し違う</h2>
<p>ここまでの話は，一見するとSDD, つまり仕様駆動開発にかなり近く見えるかもしれない．
実際，「仕様を先に置く」「実装の前に合意を作る」という方向性そのものは近い．</p>
<p>ただ，私はそのまま同一視はしていない．
理由は単純で，<strong>運用面で必要な解像度が違う</strong> からだ．</p>
<p>現実のrepo運用では，曖昧な自然言語仕様をそのままmasterにするのは無理がある．
人間同士の設計議論やRFCには自然言語が必要だし，それ自体は大切だ．
しかし，自然言語はどうしても，</p>
<ul>
<li><p>解釈の幅が広い</p>
</li>
<li><p>境界条件が抜けやすい</p>
</li>
<li><p>参照時に読み手の経験へ依存しやすい</p>
</li>
<li><p>Agentに読ませたときに確率的に揺れやすい</p>
</li>
</ul>
<p>という性質を持つ．</p>
<p>だから私は，「自然言語仕様を起点にする」のではなく，<strong>自然言語の議論を，最終的に<code>def</code>や<code>spec</code>へ落とし切る</strong> ことを重視したい．
言い換えると，</p>
<ul>
<li><p>RFCや議論は自然言語でよい</p>
</li>
<li><p>しかしrepoのmasterは，できる限り形式的であるべき</p>
</li>
</ul>
<p>ということだ．</p>
<p>仕様を自然言語のまま神棚に置くのではなく，型，状態遷移，宣言的テスト，静的制約へ変換する．
そこまでやって初めて，Agentと人間の両方が同じcontractを参照できるようになる．</p>
<p>私はここが重要だと思っている．
SDDが悪いというより，<strong>Coding Agent時代には，自然言語だけをマスターにした仕様駆動では足りない</strong>．
確率的で曖昧な自然言語表現をrepoの最終的な真理として運用するのは，現実世界では厳しい．
だからこそ，Agentyの議論では「仕様を先に書く」だけでなく，「仕様をどこまで形式に落とすか」まで含めて考える必要がある．</p>
<h2><code>def / spec / impl</code>はframeworkに依存しない</h2>
<p><a href="https://github.com/ubugeeei/fuckin-strict-nuxt-dmmf" target="_blank" rel="noopener noreferrer">fuckin-strict-nuxt-dmmf</a>の面白いところは，<code>*.def.ts</code>と <code>*.impl.ts</code>を分けている点そのものより，<strong>分ける基準がframeworkではなく責務になっている</strong> 点にある．</p>
<p>重要なのは「Vueだからこう分ける」ではない．
むしろ，ReactでもSvelteでもserver-side applicationでも，考え方は同じでよい．</p>
<p>たとえば，抽象化すると次のような分割になる．</p>
<pre><code class="language-text">feature/
  order.def.ts
  order.spec.ts
  order.impl.ts
</code></pre>
<p>あるいは言語に依存しない記法なら，</p>
<pre><code class="language-text">feature/
  order.def.*
  order.spec.*
  order.impl.*
</code></pre>
<p>と考えてもよい．</p>
<p>そしてそれぞれの意味は，おおむね次のようになる．</p>
<ul>
<li><p><code>def</code>: 低Agenty．変更には設計意図が伴う．Agentにむやみに触らせない</p>
</li>
<li><p><code>spec</code>: 低から中Agenty．受け入れ条件や宣言的シナリオを補足する</p>
</li>
<li><p><code>impl</code>: 高Agenty．制約の中で最も自由度が高く，Agentの探索空間になる</p>
</li>
</ul>
<p>この分離があると，指示も運用も明確になる．</p>
<ul>
<li><p>「<code>def</code>は明示的な設計変更があるときだけ触る」</p>
</li>
<li><p>「<code>spec</code>を満たすように<code>impl</code>を直す」</p>
</li>
<li><p>「レビューでは<code>def</code>のdiffを仕様変更として読む」</p>
</li>
<li><p>「<code>impl</code>のdiffは仕様充足として読む」</p>
</li>
</ul>
<p>ここで初めて，Agentyがrepoのトポロジに変換される．</p>
<h2>Agentyは教育のフレームでもある</h2>
<p>ここで強調しておきたいのは，この話は単なるrepo設計やAgent運用の話ではない，ということだ．
<strong>人材教育のフレーム</strong> としても読める．</p>
<p>正直に言えば，<code>def</code>を必要十分に書き起こすのは難しい．
それは簡単な仕事ではない．</p>
<ul>
<li><p>何が状態なのか</p>
</li>
<li><p>何がイベントなのか</p>
</li>
<li><p>どこまでを同一視し，どこで場合分けするのか</p>
</li>
<li><p>何を禁止状態として表現すべきか</p>
</li>
<li><p>どの境界をstaticに固定すべきか</p>
</li>
</ul>
<p>こうしたことを，実装に逃げずに形式的に書くのは，かなり訓練が要る．</p>
<p>だが，私はそれでよいと思っている．
むしろ <strong>そうあるべき</strong> だと思っている．</p>
<p>なぜなら，実装より先にドメイン仕様を形式的に表現する力こそが，これからの時代により重要になるからだ．
Coding Agentが強くなるほど，「速く書く」ことの希少性は下がる．
その代わりに，</p>
<ul>
<li><p>何を定義すべきか</p>
</li>
<li><p>どこまでを機械に委ねてよいか</p>
</li>
<li><p>どのcontractなら安全に自動化できるか</p>
</li>
<li><p>生成された実装が仕様を満たしているか</p>
</li>
</ul>
<p>を見極める力の価値が上がる．</p>
<p>この意味で，<code>def</code>を書く練習は，単なる前処理ではない．
<strong>ドメインを形式化する筋力そのものを鍛える</strong>．</p>
<p>これは別にまったく新しい話ではない．
「実装をする前にRFCを書きましょう」「実装する前に設計をディスカッションしましょう」という従来の動きにかなり近い．
ただ，Coding Agentの登場によって，この順序の意味がさらに強くなっただけだ．</p>
<p>以前は，設計を雑に始めても，最終的には人間が実装の泥臭さの中で辻褄を合わせることが多かった．
しかしAgentが大量の実装を高速に吐けるようになると，<strong>手前の定義の雑さが後段で増幅される</strong>．
だからこそ，先に<code>def</code>を議論し，<code>spec</code>を固めることの教育的価値が上がる．</p>
<p>私は，Coding Agentの登場によって「後続人をどう育成するのか」という議論はたちまち話題になっている，少なくともそうなりつつあると思っている．
もし実装の大部分をAgentが書くなら，若い開発者は何を学べばよいのか，という問いである．</p>
<p>このフレームは，その解決策の一つになりうる．</p>
<ul>
<li><p>まず<code>def</code>を書く</p>
</li>
<li><p><code>spec</code>で受け入れ条件を明確にする</p>
</li>
<li><p><code>impl</code>はAgentと一緒に作る</p>
</li>
<li><p>生成物をcontractとdiffでレビューする</p>
</li>
</ul>
<p>こうすると，育成の焦点は単なるタイピング速度や暗記量ではなく，</p>
<ul>
<li><p>ドメインの切り分け</p>
</li>
<li><p>境界の設計</p>
</li>
<li><p>状態遷移の記述</p>
</li>
<li><p>仕様の言語化</p>
</li>
<li><p>生成物の検証</p>
</li>
</ul>
<p>へ移る．</p>
<p>私はこれは健全だと思う．
実装力が不要になるわけではない．
むしろ，良い<code>def</code>や<code>spec</code>を書くには実装感覚も必要だ．
ただ，育成の順序が変わる．
まず実装から入るのではなく，<strong>先に仕様を記述し，その後に実装を読む・直す・評価する</strong> 方向へ重心が移る．</p>
<p>そしてこのやり方は，個人の学習だけでなく，組織運用としてもサステナブルだ．
知識が「ベテランの頭の中」や「うまくいったpromptの言い回し」ではなく，<code>def</code>や<code>spec</code>というstaticな資産としてrepoに蓄積されるからだ．
これは教育にも，保守にも，引き継ぎにも効く．</p>
<h2>高いAgentyを本当に求めるべき場所</h2>
<p>では，どこに高いAgentyを置くべきなのか．</p>
<p>私は，<strong>複数の実装がありえて，しかも正しさを後段で機械的に検証できる場所</strong> だと思っている．</p>
<p>具体的には，</p>
<ul>
<li><p>mapping</p>
</li>
<li><p>adapter</p>
</li>
<li><p>UI wiring</p>
</li>
<li><p>API clientの組み立て</p>
</li>
<li><p>viewとstateの接着</p>
</li>
<li><p>boilerplateが多いdomain implementation</p>
</li>
</ul>
<p>のような場所だ．</p>
<p>これらは「やり方」は複数あるが，「満たすべき条件」は比較的明確に書ける．
だから，先にcontractとspecを固定しておけば，実装側のAgentyはかなり高くできる．</p>
<p>逆に，次のような場所で高いAgentyを求めると危ない．</p>
<ul>
<li><p>state machineそのもの</p>
</li>
<li><p>business ruleの例外条件</p>
</li>
<li><p>境界値や禁止状態の定義</p>
</li>
<li><p>architectural boundary</p>
</li>
<li><p>repo全体のcoding rule</p>
</li>
</ul>
<p>ここは「あとで直せばよい」では済まない．
ここが揺れると，それ以降のすべての自動化が不安定になる．</p>
<h2>WebフロントエンドではVMを<code>def</code>に書き起こす</h2>
<p>Webフロントエンドでは，この話は特に重要になる．</p>
<p>私はMVVM的な分離をいま一度強く見直すべきだと思っている．
ただし関心があるのは「ViewModelというクラスを作ること」ではない．
<strong>VMレイヤの状態遷移を，実装の前に定義として書き起こすこと</strong> に関心がある．</p>
<p>たとえば，</p>
<pre><code class="language-ts">export type CheckoutState =
  | { kind: &quot;Idle&quot;; cart: Cart }
  | { kind: &quot;Submitting&quot;; cart: Cart }
  | { kind: &quot;Succeeded&quot;; orderId: OrderId }
  | { kind: &quot;Failed&quot;; cart: Cart; reason: CheckoutError };

export type CheckoutEvent =
  | { type: &quot;Submit&quot; }
  | { type: &quot;Resolve&quot;; orderId: OrderId }
  | { type: &quot;Reject&quot;; reason: CheckoutError };

export type Transition = (state: CheckoutState, event: CheckoutEvent) =&gt; CheckoutState;
</code></pre>
<p>こういうものは<code>checkout.vm.def.ts</code>のような場所にあるべきだ．</p>
<p>ここで固定したいのはUIの見た目ではない．
<strong>画面がどういう状態を持ち，どういうイベントでどこへ遷移するか</strong> である．</p>
<p>この層は低Agentyにしておくべきだ．
なぜなら，これはそのままプロダクトの振る舞いだからだ．</p>
<p>一方で，</p>
<ul>
<li><p>状態からpropsへの写像</p>
</li>
<li><p>componentの分割</p>
</li>
<li><p>event handlerのwiring</p>
</li>
<li><p>副作用の実装</p>
</li>
</ul>
<p>といった部分は，契約が定義されていればより高いAgentyを許容できる．</p>
<p>ただしここで注意したいのは，<strong>MVVMの<code>V</code>に相当する部分は，まだAIによる制御がそんなに簡単ではない</strong> ということだ．
layout, typography, spacing, motion, visual hierarchyのような領域は，単にstate contractがあれば自動で良くなるわけではない．
ここはstate machineの正しさとは別種の難しさを持っている．</p>
<p>むしろ<code>V</code>は，状態管理の延長として扱うよりも，<strong>デザインツールと統合されるべき別のソース・オブ・トゥルース</strong> として考えた方がよい．
たとえば，</p>
<ul>
<li><p>画面状態と遷移は<code>VM def</code>に書く</p>
</li>
<li><p>受け入れ条件は<code>VM spec</code>に書く</p>
</li>
<li><p>見た目やレイアウトはdesign tool側で管理する</p>
</li>
<li><p>実装コードはその両者を接続する</p>
</li>
</ul>
<p>という分離である．</p>
<p>この意味では，WebフロントエンドのAgentyは単純に一本の軸ではない．
少なくとも，</p>
<ul>
<li><p><code>VM</code>: 低Agentyな定義と，高Agentyな実装に分離しやすい</p>
</li>
<li><p><code>V</code>: まだAI単体では制御しづらく，design system / design toolとの統合が重要</p>
</li>
</ul>
<p>という違いがある．</p>
<p>だから私は，Webフロントエンドでは</p>
<ul>
<li><p><code>VM def</code>は低Agenty</p>
</li>
<li><p><code>VM spec</code>は中Agenty</p>
</li>
<li><p><code>VM impl</code>は高Agenty</p>
</li>
<li><p><code>View</code>はstate管理とは別軸で，design toolと接続して扱う</p>
</li>
</ul>
<p>と整理するのがよいと思っている．</p>
<p>これをやっておくと，プロダクトの振る舞いはrepo側の<code>VM</code>で握りつつ，<code>V</code>はより適切なdesign workflowに委ねられる．
つまり，画面の意味論と画面の造形を，無理に同じ場所へ押し込めなくてよくなる．</p>
<h2>これは運用とレビューのしやすさにも直結する</h2>
<p>この設計は，単にAI-friendlyというだけではない．
実際にソースコードを運用するうえで効いてくる．</p>
<p>まず，<strong>仕様の分離</strong> が効く．
<code>def</code>と<code>spec</code>が手前にあると，「何を変えたかったのか」と「どう実現したか」がdiff上で分離される．</p>
<p>次に，<strong>Agentによる改変のしやすさ</strong> が上がる．
編集可能領域を<code>impl</code>に寄せられるので，プロンプトは単純になる．
「この契約を守って実装して」と言える．</p>
<p>さらに，<strong>コードレビューのしやすさ</strong> が上がる．
レビューアは全diffを同じ粒度で読まなくてよくなる．</p>
<ul>
<li><p><code>def</code>が変わっているなら仕様レビュー</p>
</li>
<li><p><code>spec</code>が変わっているなら期待値レビュー</p>
</li>
<li><p><code>impl</code>だけなら実装レビュー</p>
</li>
</ul>
<p>というふうに，読むモードを切り替えられる．</p>
<p>高いAgentyを扱う時代ほど，このdiffの意味論が重要になる．
なぜなら，Agentは大量のコードを書けるが，人間のレビュー帯域はほとんど増えないからだ．</p>
<p>だからこそ，人間が本当に読むべき差分は，できるだけ静的で，宣言的で，小さく保たれていなければならない．
そして，その静的で宣言的な差分こそが，教育可能で，引き継ぎ可能で，組織的にもサステナブルな資産になる．</p>
<h2>結</h2>
<p>Agentyという言葉で言いたいのは，単に「AIにどこまで任せるか」ではない．
<strong>どこを確率的にしてよく，どこを決定的に固定すべきか</strong> という設計の話である．</p>
<p>一般的なルール，ドメインの境界，状態遷移，契約．
これらはなるべくstaticに，type levelに，そして機械的に検査できる形で置くべきだ．
その上で，宣言的テストで仕様を補足する．
実装は最後に置く．</p>
<p>高いAgentyを求めるべき場所は，repoの法がすでに定まっていて，その範囲で自由に探索してよい場所だけである．</p>
<p>repo全体をAgentに明け渡すのではない．
<strong>Agentが暴れてよいsandboxを，architecture側で先に作る</strong>．</p>
<p>私はこれが，これからのCoding Agent時代の設計だと思っている．</p>
]]></content:encoded>
    </item>
    <item>
      <title>SignalsとSignals．そしてRetained UI．</title>
      <link>https://wtrclred.vercel.app/ja/posts/12</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/12</guid>
      <description>Signalsは値のプリミティブなのか，レンダラの文脈なのか．</description>
      <pubDate>Wed, 11 Mar 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-20T05:33:58.000Z</dc:date>
      <category>javascript</category>
      <category>oss</category>
      <category>signals</category>
      <category>reactivity</category>
      <category>vue</category>
      <category>react</category>
      <category>solid</category>
      <category>svelte</category>
      <category>vdom</category>
      <category>vapor</category>
      <content:encoded><![CDATA[<h1>序</h1>
<p>ここ2, 3年で<strong>Signals</strong>という言葉は急激に広がった.
しかし，この言葉はしばしば二つの異なるものを同時に指している.</p>
<p>ひとつは，<a href="https://vuejs.org/api/reactivity-core.html" target="_blank" rel="noopener noreferrer">Vueの<code>ref()</code></a>や<a href="https://docs.solidjs.com/reference/basic-reactivity/create-signal" target="_blank" rel="noopener noreferrer">Solidの<code>createSignal()</code></a>のような，<strong>ランタイムで値と依存関係を保持するリアクティブ・プリミティブ</strong> という意味でのSignalsだ.
もうひとつは，<a href="https://svelte.dev/" target="_blank" rel="noopener noreferrer">Svelte</a>や<a href="https://github.com/solidjs/solid" target="_blank" rel="noopener noreferrer">Solid</a>，そして<a href="https://github.com/vuejs/core/issues/13687" target="_blank" rel="noopener noreferrer">Vue Vapor</a>のように，<strong>どのDOMがどの依存にぶら下がるかというtrackerまで含めて更新系を語る文脈</strong> でのSignalsである.</p>
<p>私はこの二つを意図的に分けて考えた方がよいと思っている.
前者は「値のリアクティビティ」であり，後者は「レンダラのリアクティビティ」だ.</p>
<p>そしてこの区別を曖昧にすると，<code>Signals = VDOMの終わり</code> のような雑な結論に流れやすい.
しかし実際には，<a href="https://github.com/tc39/proposal-signals" target="_blank" rel="noopener noreferrer">TC39 Signals proposal</a>自身がFAQの中で，SignalsはVDOMにも，native DOMにも，両者の組み合わせにも載せられると明言している.
つまり，Signalsは <strong>描画方式そのものではない</strong>.</p>
<p>この話をするためには少し歴史に戻る必要がある.</p>
<h1>Knockout.jsとObservableの時代</h1>
<p>Signalsの流行を見るたびに，私は<a href="https://knockoutjs.com/documentation/observables.html" target="_blank" rel="noopener noreferrer">Knockout.js</a>を思い出す.
Knockoutはかなり早い時期から<code>observable</code>と<code>computed</code>を持ち，依存関係を自動追跡していた.
しかも<a href="https://knockoutjs.com/documentation/computed-dependency-tracking.html" target="_blank" rel="noopener noreferrer">Knockoutのドキュメント</a>は，<strong>宣言的binding自体がcomputed observablesとして実装されている</strong> と説明している.</p>
<p>これは重要だ.
今日我々が「Signals的」と呼んでいるもののかなりの部分は，実はかなり昔からある.
値をobservableに包み，依存を追跡し，変更時にUIの一部を更新するという構図は，新しいアイデアではない.</p>
<p>ただし，その後しばらく標準化の文脈で前に出てきたのはSignalではなくObservableだった.
<a href="https://github.com/tc39/proposal-observable" target="_blank" rel="noopener noreferrer">TC39のObservable proposal</a>は，DOM eventやtimerやsocketのような<strong>push-basedな複数値のstream</strong>を標準化しようとしていた.
現在は<a href="https://github.com/WICG/observable" target="_blank" rel="noopener noreferrer">WICG Observable proposal</a>側で継続しており，そこでもhistoryとして2015年のTC39提案，2017年のWHATWG DOM issue，2019年の再活性化が整理されている.</p>
<p>ここで一度整理しておきたい.</p>
<ul>
<li><p>Observableが主に扱うのは，時間方向に流れるevent streamだ.</p>
</li>
<li><p>Signalsが主に扱うのは，「いまの値」とその依存グラフだ.</p>
</li>
</ul>
<p>両者は近いが同じではない.
Observableはtemporalであり，Signalはcurrent-value orientedだ.
だから，TC39におけるObservableの歴史と，現在の<a href="https://github.com/tc39/proposal-signals" target="_blank" rel="noopener noreferrer">Signals proposal</a>は連続してはいるが，そのまま同一視はできない.</p>
<p>しかもSignals proposalは，framework authorsの協調の上で，<strong>developer-facing surface APIよりも，underlying signal graphのcore semanticsを揃える</strong> ことに重心を置いている.
ここでもう既に，「Signalsとはend-user APIではなく，frameworkの下に潜る基盤である」という意識が見えている.</p>
<h1>SignalsとSignals．</h1>
<p>ここからが本題だ.</p>
<p>私が言いたいのは，Signalsという語には少なくとも二つのスコープがある，ということだ.</p>
<h2>1.ランタイム・プリミティブとしてのSignals</h2>
<p>これは最も狭い意味でのSignalsだ.</p>
<p><a href="https://vuejs.org/guide/extras/reactivity-in-depth.html" target="_blank" rel="noopener noreferrer">Vueのreactivity docs</a>は，Vue 3では<code>reactive()</code> がProxyを使い，<code>ref()</code> がgetter / setterを使って<code>track()</code> / <code>trigger()</code> を実現していると説明している.
また同じページでVueは，自身のreactivity systemが<strong>primarily runtime-based</strong>であると明言している.</p>
<p><a href="https://docs.solidjs.com/reference/basic-reactivity/create-signal" target="_blank" rel="noopener noreferrer">Solidの<code>createSignal()</code></a>も同様に，getterがreactive context内で依存を追跡し，setterがdependent computationsを通知するprimitiveとして説明されている.</p>
<p>この層でのSignalsは，かなり素朴に次のようなものだ:</p>
<pre><code class="language-text">signal cell -&gt; track reads -&gt; trigger writes -&gt; rerun effects
</code></pre>
<p>ここで主役なのは <strong>値</strong> である.
どの値が読まれたか，どのeffectがそれに依存しているか，という依存グラフがランタイムに構築される.</p>
<p>Vueの<code>ref</code>も，Solidの<code>createSignal</code>も，この文脈でまず理解できる.</p>
<h2>2. DOM trackerまで含めたSignals</h2>
<p>しかし，もうひとつ別の文脈がある.</p>
<p><a href="https://svelte.dev/" target="_blank" rel="noopener noreferrer">Svelte</a>は，compilerによってbrowserでの仕事を最小化するframeworkであると自身を説明している.
<a href="https://github.com/tc39/proposal-signals" target="_blank" rel="noopener noreferrer">TC39 Signals proposal</a>のFAQも，Svelte 5がrunesを内部のsignals libraryにtransformする例として挙げている.
<a href="https://github.com/solidjs/solid" target="_blank" rel="noopener noreferrer">SolidのREADME</a>は，templateをreal DOM nodesにcompileし，whole buttonをrerenderするのではなく，必要な場所だけをupdateするコードを生成することを示している.
また<a href="https://docs.solidjs.com/advanced-concepts/fine-grained-reactivity" target="_blank" rel="noopener noreferrer">Solid Docsのfine-grained reactivity</a>は，Reactがcomponent全体を再実行しがちなのに対して，Solidはtarget attributeを更新すると説明している.</p>
<p>このときSignalsは，もはや単なる値containerではない.
どのsignalがどのDOM text node / attribute / bindingを更新するか，という<strong>DOM側のtracker</strong>まで含めた更新文脈になる.</p>
<pre><code class="language-text">signal graph -&gt; compiler-known DOM sinks -&gt; exact DOM mutation
</code></pre>
<p>ここではrendererの仕事の一部がdependency graphに吸い込まれている.
再実行の単位はcomponent全体ではなく，もっと細いDOM sinkに近づく.</p>
<p>この意味でのSignalsは，値のprimitiveというより <strong>描画アーキテクチャの名前</strong> である.
ここでは同じ<code>Signals</code>という語を重ねている.</p>
<h1>Retained UI</h1>
<p>ではVDOMやReact Fiberはどこに位置づくのか.</p>
<p>私はこれを<strong>Retained UI</strong>と呼びたい.
つまり，一度desired UIをメモリ内のnode / frame / treeとして持ち，それをもとに更新を管理するランタイムである.</p>
<p><a href="https://react.dev/learn/render-and-commit" target="_blank" rel="noopener noreferrer">ReactのRender and Commit</a>は，renderでcomponentを呼び，commitでDOMに反映すると説明している.
再renderの際にはchanged propsを計算し，commitではminimal necessary operationsを適用する.</p>
<p>さらに<a href="https://github.com/acdlite/react-fiber-architecture" target="_blank" rel="noopener noreferrer">React Fiber architecture</a>では，</p>
<ul>
<li><p>render時にappを記述するtreeがmemoryに生成されること</p>
</li>
<li><p>そのtreeをdiffしてDOM operationsを計算すること</p>
</li>
<li><p>Fiberはunit of workであり，virtual stack frameのようなものだということ</p>
</li>
</ul>
<p>が説明されている.</p>
<p>これは非常に重要だ.
Reactにおいて主役なのは，signal dependency graphではなく<strong>in-memory tree / fiber graph</strong>の方だ.
state updateはそのtreeを再計算するための引き金であり，最終的なDOM updateの主体はreconciler側にある.</p>
<p>つまりReactでは，</p>
<pre><code class="language-text">state update -&gt; rerun components -&gt; new in-memory tree -&gt; reconcile -&gt; commit DOM
</code></pre>
<p>である.</p>
<p>ここでReactivityは存在しないわけではない.
しかしそれはVueやSolidのようにfirst-classなdependency graphとして前景化しているわけではない.
Reactの場合，reactivityは主に <strong>どのcomponent subtreeを再評価するか</strong> というinvalidationの問題として現れ，実際の更新はRetained UIが引き受ける.</p>
<h2>Vueはその中間にいる</h2>
<p>面白いのは，Vueが昔からこの二層を両方持っていることだ.</p>
<p><a href="https://vuejs.org/guide/extras/rendering-mechanism.html" target="_blank" rel="noopener noreferrer">VueのRendering Mechanism</a>は，</p>
<ol>
<li><p>templateをrender functionにcompileし</p>
</li>
<li><p>mountをreactive effectとして実行し</p>
</li>
<li><p>dependency change時にeffectをrerunして新しいVDOM treeを作り</p>
</li>
<li><p>patch / reconciliationでDOMを更新する</p>
</li>
</ol>
<p>と説明している.</p>
<p>さらにVueは，同じページでpatch flagsやtree flatteningを用いた<strong>compiler-informed virtual DOM</strong>を説明している.
つまりVueは昔から，</p>
<ul>
<li><p>下層には<code>ref</code> / <code>reactive</code> / effectというリアクティビティ</p>
</li>
<li><p>上層にはcompiler hints付きVDOM runtime</p>
</li>
</ul>
<p>を重ねていた.</p>
<p>これはReactとSolidの中間というより，<strong>SignalsとRetained UIを両方持つ構造</strong> と言った方がよい.</p>
<p>だからVueを見ているとわかる.
SignalsはVDOMの否定ではない.
SignalsはVDOMの下にも置けるし，上にも別のrendererを置ける.
実際<a href="https://github.com/tc39/proposal-signals" target="_blank" rel="noopener noreferrer">TC39 Signals proposalのFAQ</a>も，SignalsはVDOMにもnative DOMにもcombinationにも載ると言っている.</p>
<h1>Vue Vaporの未来</h1>
<p>この観点から<a href="https://github.com/vuejs/core/issues/13687" target="_blank" rel="noopener noreferrer">Vue Vapor</a>を見ると，話はかなり明確になる.</p>
<p>2025年12月23日の<a href="https://github.com/vuejs/core/releases/tag/v3.6.0-beta.1" target="_blank" rel="noopener noreferrer">Vue 3.6.0-beta.1 release note</a>は，Vapor Modeを</p>
<ul>
<li><p>Vue SFCのnew compilation mode</p>
</li>
<li><p>baseline bundle sizeとperformance改善を目指すもの</p>
</li>
<li><p>100% opt-in</p>
</li>
<li><p>feature-completeだがstill unstable</p>
</li>
</ul>
<p>と説明している.</p>
<p>しかも同じrelease noteには，かなり重要なことが書いてある.</p>
<ol>
<li><p>Vapor-only modeではSuspenseを直接は持たないが，<strong>VDOM Suspenseの中でVapor componentをrenderできる</strong></p>
</li>
<li><p><code>createApp</code>側に<code>vaporInteropPlugin</code>を入れると，<strong>VDOM appの中でVapor componentを使える</strong> 3.逆にVapor app側でもinterop pluginを入れれば<strong>VDOM componentを使える</strong> 4.ただしmixed nestingにはrough edgesがあり，<strong>distinct regions</strong>を推奨する</p>
</li>
</ol>
<p>これはかなり示唆的だ.
少なくとも近未来のVue Vaporは，「VDOMを完全に捨てて全面移行する新しいVue」ではない.
むしろ <strong>高性能subsetとしてのVapor</strong>を，既存Vue runtimeとinteropさせながら段階的に広げる構想に見える.</p>
<p><a href="https://github.com/vuejs/core/issues/13687" target="_blank" rel="noopener noreferrer">Vapor Roadmap</a>でも，</p>
<ul>
<li><p>SSR / Hydration</p>
</li>
<li><p>Template Ref Interop</p>
</li>
<li><p>Suspense support</p>
</li>
<li><p>Vue Router</p>
</li>
<li><p>Pinia</p>
</li>
<li><p>Nuxt.js</p>
</li>
<li><p>DevTools Integration</p>
</li>
<li><p>Vue Test Utils</p>
</li>
</ul>
<p>といった項目が並んでいる.</p>
<p>ここから読み取れるのは単純だ.
Vaporの本質的な課題は，単に「速いDOM更新ができるか」ではない.
<strong>Vue ecosystem全体とどう噛み合うか</strong> が課題なのだ.</p>
<p>つまりVue Vaporの未来は，私はこう見る.</p>
<ul>
<li><p>VDOMがlegacy residueとして残るのではない</p>
</li>
<li><p>Vaporがperf-sensitive pathを担う高性能subsetになる</p>
</li>
<li><p>VDOM runtimeはinterop / ecosystem / library compatibilityの母体として当面は重要なままである</p>
</li>
<li><p>そして両者の境界が，より明示的に設計されていく</p>
</li>
</ul>
<p>これはむしろVueらしい.
Vueは昔からprogressive / incrementally adoptableであることを重視してきた.
Vaporが将来成功するとしても，その成功の仕方は「全部置き換える」より，「適切な境界を持って共存する」に近いはずだ.</p>
<h1>なぜこれはReactとの比較で重要なのか</h1>
<p>ReactではReactivityよりもRetained UIの方がコアであり，Fiberはそのscheduling / reconciliation / prioritizationを担う.
一方でVapor / Svelte / Solid Compilerの方向は，依存追跡とDOM sink trackingを近づけることで，保持されたtreeへの依存を薄くしようとする.</p>
<p>ここで比較すべきなのは「どちらが上か」ではない.
比較すべきなのは，<strong>どのレイヤが更新責任を持つか</strong> だ.</p>
<ul>
<li><p>React / classic VDOM: in-memory node runtimeが更新責任を持つ</p>
</li>
<li><p>runtime signals: signal graphがinvalidationを持つが，rendererは別にいる</p>
</li>
<li><p>compiler + DOM tracker signals: signal graphがrendererの責務の一部まで持ち始める</p>
</li>
</ul>
<p>この三つは別物だ.</p>
<p>そしてVueは今，その全部を一つの体系の中に保持したまま前進しようとしている.
<code>ref</code>は残り，VDOM modeも残り，その上でVaporが増える.
私はこれをかなり健全だと思っている.</p>
<p>Signalsは魔法の銀の弾丸ではない.
またVDOMは単なる過去の遺物でもない.
Retained UIは，component composition，dynamic rendering，ecosystem interop，debuggability，toolingとの整合において依然として強い.
一方でcompiler-guidedなSignals.は，hot pathの細粒度更新とbaseline sizeの面で強い.</p>
<p>問題は常に「どちらを捨てるか」ではなく，「どこに境界を引くか」なのだ.</p>
<hr>
<p>これは私の個人的な考察であり，Vue Team / React Team / TC39の公式見解ではありません.</p>
]]></content:encoded>
    </item>
    <item>
      <title>ReactはUI = f(State)であるか？</title>
      <link>https://wtrclred.vercel.app/ja/posts/09</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/09</guid>
      <description>純粋と冪等とは何か — 圏論，Algebraic Effects，Applicativeな構造から見るReactのより精密な読み方</description>
      <pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-17T08:53:41.000Z</dc:date>
      <category>react</category>
      <category>javascript</category>
      <category>oss</category>
      <category>category-theory</category>
      <category>algebraic-effects</category>
      <category>applicative</category>
      <category>mathematics</category>
      <category>semantics</category>
      <content:encoded><![CDATA[<blockquote>
<p><strong>注意</strong>: 私は数学の専門家ではない. この文章は，圏論・Algebraic Effects・Applicativeな構造の語彙を使ってReactを理解するための試論であり，厳密な定理としてではなく，読み方の提案として読んでほしい. 数学的に誤っている点があればぜひ教えてほしい.</p>
</blockquote>
<h1>序</h1>
<p>「UI = f(State)」— Reactに関する言説の中で最も広く流通している等式の一つである.
この等式は一見シンプルで，Reactの核心を捉えているように見える. 状態が決まればUIが決まる，と.</p>
<p>しかし，この等式を数学的な言葉で丁寧に読むと，いくつか確認すべき点が出てくる.</p>
<ul>
<li><p>この <code>f</code> は何なのか？ 数学的な意味での「関数」なのか？</p>
</li>
<li><p>もし関数なら，それはどの圏の射なのか？</p>
</li>
<li><p>Reactが言う「純粋」「冪等」は，数学におけるそれと同じか？</p>
</li>
</ul>
<p>このエッセイでは，圏論・Algebraic Effects・Applicativeな構造の語彙を用いて，React Componentをより精密に読むことを試みる.</p>
<hr>
<h1>「純粋」の数学的意味</h1>
<p>Reactのドキュメントや議論でよく使われる「純粋(pure)」という言葉の数学的意味を明確にしよう.</p>
<p>数学における <strong>純粋関数</strong> (pure function)とは，参照透過性(referential transparency)を持つ写像のことだ. 圏論の言葉で言えば，集合の圏<strong>Set</strong>における射(morphism)に対応する:</p>
<p>$$f: A \to B$$</p>
<p>この射 <code>f</code> は以下を満たす:</p>
<ul>
<li><p>同じ入力に対して常に同じ出力を返す(deterministic)</p>
</li>
<li><p>外部の状態を読み書きしない(no side effects)</p>
</li>
<li><p>値の計算以外のことをしない(no observable effects)</p>
</li>
</ul>
<p>ここでReactのFunction Componentを考えてみよう:</p>
<pre><code class="language-jsx">function Greeting({ name }) {
  const [count, setCount] = useState(0);
  const theme = useContext(ThemeContext);

  useEffect(() =&gt; {
    document.title = `Hello, ${name}`;
  }, [name]);

  return (
    &lt;h1 style={{ color: theme.primary }}&gt;
      Hello, {name} ({count})
    &lt;/h1&gt;
  );
}
</code></pre>
<p>このComponentは:</p>
<ul>
<li><p><code>useState</code>を通じて内部状態を読み，更新手段を受け取っている</p>
</li>
<li><p><code>useContext</code>を通じてコンポーネントツリーの外部状態を読んでいる</p>
</li>
<li><p><code>useEffect</code>を通じて副作用をスケジュールしている</p>
</li>
</ul>
<p>これは<strong>Set</strong>の射ではない. 引数(props)以外のものを読んでいるし，純粋な値の計算以外のことをしている.</p>
<p><strong>React Componentは数学的な意味で「純粋関数」ではない.</strong></p>
<p>ではなぜReactはComponentを「純粋」と呼ぶのか？ それはReactが <strong>異なる意味で</strong>「純粋」という言葉を使っているからだ. Reactにおける「純粋」とは:</p>
<blockquote>
<p>レンダリング中に観測可能な副作用がないこと — より正確には，Reactのエフェクトシステムの中で正しく振る舞うこと</p>
</blockquote>
<p>これは数学的純粋性よりもはるかに弱い条件であり，用語の混同は本質的な誤解を招く.</p>
<hr>
<h1>「冪等」の本来の意味</h1>
<p>Reactのドキュメントには次のような記述がある（<a href="https://react.dev/reference/rules/components-and-hooks-must-be-pure" target="_blank" rel="noopener noreferrer">Components and Hooks must be pure</a>）:</p>
<blockquote>
<p>&quot;Components must be idempotent – React components are assumed to always return the same output with respect to their inputs – props, state, and context.&quot;</p>
</blockquote>
<p>ここで使われている「冪等(idempotent)」は，数学的に何を意味するのか.</p>
<h2>数学における冪等</h2>
<p>代数学における冪等元の定義:</p>
<blockquote>
<p><strong>定義 (冪等元).</strong> モノイド <code>(M, ·, e)</code> において，元 <code>a ∈ M</code> が<strong>冪等</strong> (idempotent)であるとは，<code>a · a = a</code> を満たすことをいう.</p>
</blockquote>
<p>これを関数に拡張すると:</p>
<blockquote>
<p><strong>定義 (冪等な自己準同型).</strong> 圏 <code>𝒞</code> において，射 <code>e: A → A</code> が<strong>冪等</strong>であるとは，<code>e ∘ e = e</code> を満たすことをいう.</p>
</blockquote>
<p>この2つの定義は，冪等元については <a href="https://mathworld.wolfram.com/Idempotent.html" target="_blank" rel="noopener noreferrer">Eric W. Weisstein, &quot;Idempotent,&quot; <em>MathWorld</em></a> を，冪等な自己準同型については <a href="https://emilyriehl.github.io/files/context.pdf" target="_blank" rel="noopener noreferrer">Emily Riehl, <em>Category Theory in Context</em>, Example 3.2.14</a> を参照している.</p>
<p>具体例:</p>
<ul>
<li><p>絶対値関数は冪等: <code>||x|| = |x|</code></p>
</li>
<li><p>射影行列 <code>P</code> は冪等: <code>P^2 = P</code></p>
</li>
<li><p><code>Math.floor</code>は冪等: <code>Math.floor(Math.floor(x)) === Math.floor(x)</code></p>
</li>
</ul>
<p>ここで重要なのは，<strong>冪等は自己準同型(endomorphism) <code>f: A -&gt; A</code> の性質</strong>であるということだ. <code>f</code> の出力にもう一度 <code>f</code> を適用しても結果が変わらない — これが冪等の本来の意味である.</p>
<h2>Reactの「冪等」は数学的には冪等ではない</h2>
<p>ReactがComponentについて「冪等」と言っているのは，「同じ入力に対して同じ出力を返す」という意味だ.
これは数学的には冪等ではなく，<strong>決定性(determinism)</strong>あるいは <strong>整合性(well-definedness)</strong>と呼ぶべきものだ.</p>
<p>なぜなら:</p>
<ol>
<li><p>Componentの型は <code>Props -&gt; VDOM</code> であり，<code>Props != VDOM</code>. つまりそもそも自己準同型ですらない.</p>
</li>
<li><p><code>f ∘ f = f</code> を検証するためには <code>f</code> の出力を <code>f</code> の入力に渡す必要がある. だがComponentの出力(VDOM)をComponentの入力(Props)に渡すことに意味はない.</p>
</li>
<li><p>Reactが言っているのは「<code>f(x) = f(x)</code>（同じ入力なら同じ出力）」であって，「<code>f(f(x)) = f(x)</code>」ではない.</p>
</li>
</ol>
<p><code>f(x) = f(x)</code> はあらゆる関数が定義上満たすべき性質であり，わざわざ「冪等」と呼ぶようなものではない. 正確に言えば，Reactが意図しているのは「Hookが内部状態を持っていても，同じ(props, state, context)の組に対して同じVDOMを返す」ということだろう. しかしこれは「決定的関数(deterministic function)」であり，「冪等(idempotent)」とは異なる概念だ.</p>
<h2>レンダリング操作は冪等として表現できる</h2>
<p>ただし，視点を変えるとReactには冪等として表現できる操作がある.</p>
<p>状態 <code>s</code> を固定し，ユーザーのEffectや外部I/Oをいったん捨象して，DOM状態だけを見る. その上でレンダリングとコミットの合成をひとつの操作として考えてみよう:</p>
<p>$$
\begin{array}{c}
u_{s}: \mathrm{DOM} \to \mathrm{DOM}
\\
u_{s} = \operatorname{commit}(\operatorname{reconcile}(\operatorname{render}(s)))
\end{array}
$$</p>
<p>このとき:</p>
<p>$$u_{s}(u_{s}(\operatorname{dom})) = u_{s}(\operatorname{dom})$$</p>
<p>同じ状態に対してレンダリング・コミットを2回適用しても，DOM状態だけを見れば1回適用した結果と同じになる. 2回目の適用は差分がなければno-opとして扱える. <strong>これはDOM状態変換として見たときの冪等</strong>である.</p>
<p>つまり，Reactにおいて冪等として扱うべき対象は<strong>Component関数そのもの</strong> ではなく，理想化された<strong>レンダリング・コミット操作</strong> <code>u_s</code> の方だ. この区別は決定的に重要である.</p>
<hr>
<h1>React FiberとAlgebraic Effects</h1>
<p>ここで，Reactの内部構造に目を向けよう.</p>
<p>React Fiber — Reactのコアランタイム — には，Algebraic Effectsを思わせる特徴がある. Dan Abramovも，Reactのいくつかの仕組みを考えるための見方としてAlgebraic Effectsを紹介している（<a href="https://overreacted.io/algebraic-effects-for-the-rest-of-us/" target="_blank" rel="noopener noreferrer">Dan Abramov, &quot;Algebraic Effects for the Rest of Us&quot;</a>, 2019）. ただし同記事は，Reactとの対応を「stretch」とし，SuspenseはAlgebraic Effectsそのものではなく，JavaScriptでは本当に継続をresumeしているわけではないとも明記している.</p>
<h2>Algebraic Effectsとは</h2>
<p>Algebraic Effectsは，計算とエフェクト（副作用）を分離するための数学的枠組みだ.
この節の一般的な説明は，<a href="https://doi.org/10.1023/A:1023064908962" target="_blank" rel="noopener noreferrer">Plotkin and Power, &quot;Algebraic Operations and Generic Effects&quot;</a> (2003) を参考にしている.</p>
<p>基本構造は3つの要素からなる:</p>
<ol>
<li><p><strong>エフェクトシグネチャ</strong>: 利用可能な操作の集合</p>
</li>
<li><p><strong>計算(computation)</strong>: エフェクト操作を呼び出しうるプログラム</p>
</li>
<li><p><strong>ハンドラ(handler)</strong>: エフェクト操作に意味を与える解釈器</p>
</li>
</ol>
<p>これはReactの実装そのものではなく，対応関係を説明するための擬似コードだ:</p>
<pre><code>// エフェクトシグネチャ
effect GetState  : Unit → State
effect SetState  : State → Unit
effect ReadProps : Unit → Props
effect ReadContext: Key → Value
effect Suspend   : Promise&lt;A&gt; → A
effect Throw     : Error → ⊥

// ハンドラ（= React Fiber ランタイム）
handler ReactFiber {
  return vdom → vdom
  ReadProps(_, resume)      → resume(currentProps)
  GetState(_, resume)       → resume(currentFiber.memoizedState)
  SetState(newState, resume) → enqueueUpdate(newState); resume(unit)
  Suspend(promise, retryRender) → showFallback(); promise.then(() → retryRender())
  Throw(error, _)           → propagateToErrorBoundary(error)
}
</code></pre>
<p>ここで <strong>ユーザーが書くReact Function Component</strong>と<strong>React Fiberランタイム</strong> の関係を次のように対応づけられる:</p>
<table>
<thead>
<tr>
<th></th>
<th>Algebraic Effectsにおける役割</th>
<th>Reactにおける対応</th>
</tr>
</thead>
<tbody>
<tr>
<td>エフェクトシグネチャ</td>
<td>利用可能な操作の宣言</td>
<td>Hooks API (<code>useState</code>, <code>useContext</code>, <code>use</code>, ...)</td>
</tr>
<tr>
<td>計算</td>
<td>エフェクト操作を呼び出すプログラム</td>
<td><strong>ユーザーのFunction Component</strong></td>
</tr>
<tr>
<td>ハンドラ</td>
<td>操作に意味を与える解釈器</td>
<td><strong>React Fiberランタイム</strong></td>
</tr>
</tbody>
</table>
<p>この対応では，ReactのHooksはエフェクト操作として読める:</p>
<table>
<thead>
<tr>
<th>Hook</th>
<th>エフェクト操作</th>
<th>ハンドラ</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>useProps</code></td>
<td><code>ReadProps</code></td>
<td>現在のprops</td>
</tr>
<tr>
<td><code>useState</code></td>
<td><code>GetState / SetState</code></td>
<td>Fiberのstate queue</td>
</tr>
<tr>
<td><code>useContext</code></td>
<td><code>ReadContext</code></td>
<td>Provider chainの探索</td>
</tr>
<tr>
<td><code>use(promise)</code></td>
<td><code>Suspend</code></td>
<td>Suspense boundary</td>
</tr>
<tr>
<td><code>throw error</code></td>
<td><code>Throw</code></td>
<td>Error boundary</td>
</tr>
<tr>
<td><code>useEffect</code></td>
<td><code>ScheduleEffect</code></td>
<td>commit phaseのeffect queue</td>
</tr>
</tbody>
</table>
<p>ここで <code>useProps</code> はReactの実在APIではない. 以降では，通常はComponentの関数引数として受け取るpropsも，ハンドラから現在のpropsを読む概念的なHook操作として一般化して扱う.</p>
<p><strong>ユーザーが書くComponentはエフェクトフルな計算(effectful computation)として，React Fiberはそのハンドラ（解釈器）として見なせる.</strong></p>
<p>ただし，これはReactのすべてをAlgebraic Effectsだけで説明できる，という主張ではない. 特にRules of Hooksが要求する「Hook呼び出し列の静的な形」は，通常のMonadやAlgebraic Effectsの語彙だけでは捉えにくい. ここでは，まずRules of Hooksという規約があり，それがComponent bodyの形に制約をかける. その結果として，Hook呼び出し列をApplicative / Free Applicative的な構造へ比較的忠実に埋め込める，という順序で考える.</p>
<hr>
<h1>Kleisli圏における関数合成</h1>
<p>「UI = f(State)」で暗黙に想定されている「関数合成」も，数学的に検証する必要がある.</p>
<h2>Kleisli圏</h2>
<p>Monad <code>T</code> が与えられた圏 <code>𝒞</code> に対して，<strong>Kleisli圏</strong> <code>𝒞_T</code> を構成できる.
ここでのMonadは圏論的Monad，すなわち自己関手 <code>T: 𝒞 → 𝒞</code> と自然変換 <code>η: Id ⇒ T</code>（unit），<code>μ: T² ⇒ T</code>（multiplication）の三つ組 <code>(T, η, μ)</code> でMonad則を満たすものとしている. MonadとKleisli圏の定義は <a href="https://emilyriehl.github.io/files/context.pdf" target="_blank" rel="noopener noreferrer">Emily Riehl, <em>Category Theory in Context</em>, Definition 5.1.1 と Definition 5.2.10</a> を参照している:</p>
<ul>
<li><p><strong>対象</strong>: <code>𝒞</code> と同じ</p>
</li>
<li><p><strong>射</strong>: <code>A -&gt; B</code> in <code>𝒞_T</code> は <code>A -&gt; T(B)</code> in <code>𝒞</code></p>
</li>
<li><p><strong>合成</strong>: <code>f: A -&gt; T(B)</code> と <code>g: B -&gt; T(C)</code> のKleisli合成は:</p>
</li>
</ul>
<p>$$g \circ_{T} f = \mu_{C} \circ T(g) \circ f$$</p>
<p>すなわち:</p>
<p>$$
A \xrightarrow{f} T(B) \xrightarrow{T(g)} T(T(C)) \xrightarrow{\mu_{C}} T(C)
$$</p>
<h2>React ComponentはKleisli射</h2>
<p>ここから先は，Reactの公式な形式意味論ではなく，Reactの各種エフェクトをひとつの抽象的なエフェクトMonadとして読むためのスケッチである. ReactのエフェクトMonadを便宜的に <code>R</code> と置く. JSの表面構文に合わせるなら，React Componentの型は次のように読める:</p>
<p>$$\mathrm{Component}: \mathrm{Props} \to R(\mathrm{VDOM})$$</p>
<p>これはKleisli圏 <code>Set_R</code> における射 <code>Props -&gt; VDOM</code> として読むための表現である. ただし，Hook操作のプログラム構造を揃えて扱うなら，propsも外側の引数ではなく，概念的なHook操作として内側に入れられる:</p>
<p>$$\operatorname{useProps}: R(\mathrm{Props})$$</p>
<p>この読み方では，<code>Props -&gt; R(VDOM)</code> は「propsを先に与える」表現であり，<code>useProps</code> を含む <code>R(VDOM)</code> は「propsをReact Fiberが解釈時に供給する」表現である.</p>
<p><strong>Setの射（純粋関数）ではなく，Kleisli射（エフェクトフルな計算）として読む方が，Reactの振る舞いに近い.</strong></p>
<h2>コンポーネントの合成</h2>
<p>JSXにおけるコンポーネントの合成を考えてみよう:</p>
<pre><code class="language-jsx">function Parent({ data }) {
  const processed = use(processData(data));
  return &lt;Child items={processed} /&gt;;
}
</code></pre>
<p>この依存関係を抽象化すれば，Kleisli合成として次のようにスケッチできる:</p>
<p>$$\mathrm{Parent} = \mathrm{Child} \circ_{R} \mathrm{process}$$</p>
<p>ここで <code>∘_R</code> は<strong>Set</strong>の通常の合成 <code>∘</code> ではなく，抽象化されたReactエフェクトMonadの<strong>Kleisli合成</strong> だ. <code>use</code>の呼び出し — つまりSuspendに相当するエフェクト — を経由しているため，純粋な関数合成では表現しにくい.</p>
<p>つまり，Reactにおける「関数合成」は，少なくともHooksやSuspenseまで含めて考えるなら，通常の意味での関数合成だけでは捉えきれない. これも「UI = f(State)」が素朴に正しくない理由のひとつである.</p>
<h2>MonadのbindとApplicativeなスケルトン — Component Bodyの正体</h2>
<p>Kleisli射の合成を理解したところで，<strong>Componentの関数bodyの中で何が起きているか</strong> をもう一段掘り下げよう.</p>
<p>Monad <code>T</code> に対して，<strong>bind演算</strong>（Haskellでは <code>&gt;&gt;=</code> と書かれる）は次の型を持つ:</p>
<p>$$\operatorname{bind}_{T}: T(A) \times (A \to T(B)) \to T(B)$$</p>
<p>これは「エフェクトフルな計算の結果を取り出して，次のエフェクトフルな計算に渡す」操作だ. Haskellの<strong>do記法</strong> はこのbindの連鎖を読みやすく書くための構文糖衣である:</p>
<pre><code class="language-haskell">-- do 記法
do
  state &lt;- getState
  ctx   &lt;- readContext themeCtx
  pure (view state ctx)

-- 脱糖後（bind の連鎖）
getState    &gt;&gt;= \state -&gt;
readContext themeCtx &gt;&gt;= \ctx -&gt;
pure (view state ctx)
</code></pre>
<p>この見方は，ReactのComponent bodyを「エフェクトを順に呼び出し，その結果を使ってVDOMを返す計算」として読む上では有用だ. <code>useProps</code> まで含めて抽象化すると，たとえば:</p>
<pre><code class="language-jsx">function Component() {
  const props = useProps(); // 概念的なHook操作: ReadProps &gt;&gt;= \props -&gt;
  const [state, setState] = useState(init); // ← bind: GetState &gt;&gt;= \state -&gt;
  const ctx = useContext(ThemeCtx); // ← bind: ReadContext &gt;&gt;= \ctx -&gt;
  return &lt;View name={props.name} state={state} ctx={ctx} /&gt;; // ← pure: return (View props state ctx)
}
</code></pre>
<p><strong>React Componentのbodyは，局所的にはReactエフェクトMonadにおけるdo記法に近いものとして読める.</strong></p>
<p>ただし，ここで注意が必要だ. <strong>通常のMonadはReactが要求する「Hook呼び出し列の静的性」を表すには強すぎる.</strong> bindは前段の計算結果に応じて，次に実行するエフェクトの形そのものを変えられるからだ:</p>
<pre><code class="language-haskell">do
  x &lt;- action1
  if condition x
    then do { y &lt;- action2; pure (f x y) }  -- action2 がある
    else pure (g x)                          -- action2 がない
</code></pre>
<p>これはMonad計算としては自然に書ける. しかしReactのHooksでは，このように値や分岐に応じてHook列の形を変えることが禁止されている.</p>
<p>この「エフェクトの形は先に決まっていて，値だけが後から流れ込む」という性質は，Monadよりも <strong>Applicative</strong> の方が近い. Applicative <code>F</code> は，おおよそ次の操作を持つ:</p>
<p>$$
\begin{array}{c}
\operatorname{pure}_{F}: A \to F(A)
\\
\operatorname{ap}_{F}: F(A \to B) \times F(A) \to F(B)
\end{array}
$$</p>
<p>ここで <code>ap_F</code> は，Haskellでいう <code>&lt;*&gt;</code> に対応する.</p>
<p>Applicativeにはbindのような <code>F(A) -&gt; (A -&gt; F(B)) -&gt; F(B)</code> はない. そのため，前のエフェクトの結果に応じて後続のエフェクト構造を変えることはできない. 代わりに，<strong>どのエフェクトをどの順序で使うかというスケルトンが静的に決まり，その結果を純粋な関数へ流し込む</strong> 形になる.</p>
<p>Rules of Hooksを満たすComponentに限ってかなり粗くスケッチすれば，Hook呼び出し列は次のようなApplicative的な形として読める:</p>
<pre><code class="language-haskell">pure view &lt;*&gt; useProps &lt;*&gt; useState init &lt;*&gt; useContext themeCtx
</code></pre>
<p>もちろんこれはReactの実装ではない. しかし，propsも <code>useProps</code> という概念的Hookとして含めると，「入力とHookの列は固定され，そこから得た値でJSXを構成する」という構造を表すには，bindの連鎖よりもApplicativeのスケルトンの方が近い.</p>
<h3>Rules of HooksがApplicativeなスケルトンへの埋め込みを可能にする</h3>
<p>ここで，Reactの<a href="https://react.dev/reference/rules/rules-of-hooks" target="_blank" rel="noopener noreferrer"><strong>Rules of Hooks</strong></a>を見直す:</p>
<blockquote>
<ul>
<li><p>Hookをループ，条件分岐，ネストされた関数の中で呼んではならない</p>
</li>
<li><p>Hookは常にFunction Componentのトップレベルで呼ばなければならない</p>
</li>
</ul>
</blockquote>
<p>順序としては，この規約が先にある. つまり「Applicativeな表現がRules of Hooksを説明する」というより，<strong>Rules of HooksがComponent bodyの形を制限することで，<code>useProps</code> を含むHook操作からなるApplicativeなスケルトンへ埋め込めるようにしている</strong> と考える方が自然だ.</p>
<p>この意味での「忠実な埋め込み」とは，Hook呼び出しの個数と順序を落とさずに，プログラム構造として取り出せるという意味である.</p>
<p>Reactではこれに相当するものが<strong>禁止</strong>されている:</p>
<pre><code class="language-jsx">// NG: 条件分岐の中の Hook
function Component({ condition }) {
  const [x] = useState(0);
  if (condition) {
    const y = useContext(Ctx); // ← Rules of Hooks 違反
    return &lt;A x={x} y={y} /&gt;;
  }
  return &lt;B x={x} /&gt;;
}
</code></pre>
<p>なぜか？ React FiberはHookの呼び出し順序をlinked listとして保持している. レンダリングごとにHook操作の個数や順序が変わると，Fiberは前回のレンダリング結果と今回のHook呼び出しを正しく対応させられない.</p>
<p>一方で，Hook列を先に固定し，その後で値に応じて返すUIを分岐させることはできる:</p>
<pre><code class="language-jsx">// OK: Hook列は固定されている
function Component({ condition }) {
  const [x] = useState(0);
  const y = useContext(Ctx);
  return condition ? &lt;A x={x} y={y} /&gt; : &lt;B x={x} /&gt;;
}
</code></pre>
<p>つまり，Reactが静的に保ちたいのは「値を使ったレンダリング結果」ではなく，<strong>Hook操作のプログラム構造</strong> である. Rules of Hooksによってこの形が固定されるからこそ，<code>FreeApplicative HookOp</code>，あるいはコンポーネントツリーの再帰性まで含めるなら <code>Fix (FreeApplicative HookOp)</code> のような構造へ写す余地が生まれる.</p>
<p>この方向の参考実装として，<a href="https://github.com/ubugeeei/mreact" target="_blank" rel="noopener noreferrer">ubugeeei/mreact</a> がある. React HooksをHaskellのindexed Monadとして表現し，Hookの呼び出し順序を型レベルのlistとして表現することで，Rules of Hooksを型検査で扱う実験である. ただし，本稿の整理では，この「型レベルのHook列」はReactに後から貼り付ける表現というより，Rules of Hooksが先に固定しているプログラム形を，Applicative / Free Applicative的な静的スケルトンとして写し取ったものだ，と位置づけ直す.</p>
<hr>
<h1><code>use</code>と<code>Suspense</code> — Algebraic Effectsを思わせる具象</h1>
<p><code>use</code> Hookと<code>Suspense</code>は，Reactの中でもAlgebraic Effectsを思わせる振る舞いが最も見えやすい部分だ. ただし，これはReactの実装が本物のAlgebraic Effectsを持つという意味ではない.</p>
<pre><code class="language-jsx">function UserProfile({ userId }) {
  const user = use(fetchUser(userId));
  return &lt;div&gt;{user.name}&lt;/div&gt;;
}

function App() {
  return (
    &lt;Suspense fallback={&lt;Loading /&gt;}&gt;
      &lt;UserProfile userId={1} /&gt;
    &lt;/Suspense&gt;
  );
}
</code></pre>
<p>このメカニズムを，Algebraic Effectsの語彙で記述してみる:</p>
<p><strong>1. <code>use(promise)</code>はエフェクト操作の実行(perform)に対応する:</strong></p>
<p>Promiseが未解決なら，Reactの公開APIとしてはそのComponentが<strong>suspend</strong>する（<a href="https://react.dev/reference/react/use" target="_blank" rel="noopener noreferrer">use</a>はSuspenseと統合されている）. これはAlgebraic Effectsにおける「操作の呼び出し」を思わせる非局所的制御フロー(non-local control flow)として読める.</p>
<p><strong>2. <code>Suspense</code>はエフェクトハンドラ(handler)に対応する:</strong></p>
<p><code>Suspense</code>はsuspendを受け止め，fallback UIを表示し，Promiseがresolveしたらレンダリングを再試行する.</p>
<p><strong>3.再レンダリング時に<code>use(promise)</code>は解決済みの値を返す:</strong></p>
<p>これはAlgebraic Effectsにおける「継続に値を渡して再開する」操作に似ている. ただし，ReactはJavaScriptの実行スタックを本当に保存してその地点から再開しているわけではなく，Promiseの解決後にComponent treeを再レンダリングする. この点は <a href="https://react.dev/reference/react/Suspense" target="_blank" rel="noopener noreferrer">React Suspense docs</a> と <a href="https://overreacted.io/algebraic-effects-for-the-rest-of-us/" target="_blank" rel="noopener noreferrer">Dan Abramov, &quot;Algebraic Effects for the Rest of Us&quot;</a> の注意に基づいている.</p>
<p>擬似的なハンドラ記法で表現すると:</p>
<pre><code>handle(UserProfile(userId)) with {
  return vdom → vdom
  Suspend(promise, retryRender) →
    display &lt;Loading /&gt;;
    await promise;
    retryRender()  // ← 本物の継続再開ではなく，再レンダリング
}
</code></pre>
<p>ここで特に重要なのは，<strong><code>use</code>がComponentのレンダリングを中断し，Reactが後で再レンダリングできる</strong> ということだ.</p>
<p>通常のJavaScript関数は，呼び出したら最後まで実行される. <code>async</code> / <code>await</code>なら <code>await</code> の地点から継続できるが，React Componentは<code>async function</code>として書かれていない. それでもReactはSuspense boundaryと再レンダリングによって，見かけ上は「そこで待って，値が来たら続きを評価した」ような体験を作る. この点が，Algebraic Effects（あるいは限定継続 / delimited continuation）を思わせる.</p>
<hr>
<h1>Client-Server「同型」の誤り</h1>
<p>「同型JavaScript (Isomorphic JavaScript)」あるいは「ユニバーサルJavaScript」という言葉がある. Server Componentsの文脈でもしばしば使われるが，この「同型」は数学的に正しいのか.</p>
<h2>圏論における同型</h2>
<blockquote>
<p><strong>定義 (同型射).</strong> 圏 <code>𝒞</code> において，射 <code>f: A → B</code> が<strong>同型射</strong> (isomorphism)であるとは，射 <code>g: B → A</code> が存在して <code>g ∘ f = id_A</code> かつ <code>f ∘ g = id_B</code> を満たすことをいう.</p>
</blockquote>
<p>この定義は，<a href="https://emilyriehl.github.io/files/context.pdf" target="_blank" rel="noopener noreferrer">Emily Riehl, <em>Category Theory in Context</em>, Definition 1.1.10</a> における同型射の定義に沿っている.</p>
<p>同型は「構造を保存する可逆な変換」を意味する. <code>A</code> と <code>B</code> が同型ならば，圏論的には区別できない.</p>
<h2>ServerレンダリングとClientレンダリングは同型ではない</h2>
<p>サーバーとクライアントのレンダリングを関手として考える:</p>
<p>$$
\begin{array}{c}
F_{S}: \mathrm{Component} \to \mathrm{HTML}
\\
F_{C}: \mathrm{Component} \to \mathrm{DOM}
\end{array}
$$</p>
<p>そもそも <code>HTML != DOM</code> である. 出力カテゴリが異なるので，<code>F_S</code> と <code>F_C</code> の間に同型を構成する余地はない.</p>
<p>HTML文字列からDOMへの変換（パース）は存在するが，その逆（DOM → HTML）はシリアライズであり，これらの合成が恒等射になるとは限らない（イベントハンドラ，内部状態，クロージャなどは失われる）.</p>
<h2>より正確な記述: 異なるエフェクトハンドラ</h2>
<p>Algebraic Effectsの枠組みでは，サーバーレンダリングとクライアントレンダリングは<strong>同じエフェクトフルな計算に対する異なるハンドラ</strong>として理解できる:</p>
<p>$$
\begin{array}{c}
\mathrm{Component}: \mathrm{Props} \to \mathrm{Eff}_{\mathrm{React}}(\mathrm{VDOM})
\\
\mathrm{handleServer}: \mathrm{Eff}_{\mathrm{React}}(\mathrm{VDOM}) \to \mathrm{HTMLString}
\\
\mathrm{handleClient}: \mathrm{Eff}_{\mathrm{React}}(\mathrm{VDOM}) \to \mathrm{DOMMutations}
\end{array}
$$</p>
<p>Server ComponentsとClient Componentsの違いは，<a href="https://react.dev/reference/rsc/use-client" target="_blank" rel="noopener noreferrer">React公式ドキュメント</a>が説明するように，実行環境と利用可能なAPIの違いとして現れる. この文章の読み方では，それを<strong>利用可能なエフェクトシグネチャの違い</strong> として捉える:</p>
<ul>
<li><p><strong>Server Component</strong>: DBクエリ，ファイルシステムアクセスなどのサーバー側の処理を直接使える. <code>useState</code>, <code>useEffect</code>など，多くのHooksは使えない.</p>
</li>
<li><p><strong>Client Component</strong>: <code>useState</code>, <code>useEffect</code>などのクライアント側のAPIを使える. サーバー専用コードをClient module subtreeへ直接持ち込むことはできない.</p>
</li>
</ul>
<p>これは同型ではなく，エフェクトシグネチャ間の<strong>包含関係</strong>あるいは<strong>サブタイピング</strong>として理解すべきものだ.</p>
<p>$$
\begin{array}{c}
\mathrm{ServerEffects} \neq \mathrm{ClientEffects}
\\
\mathrm{SharedEffects} = \mathrm{ServerEffects} \cap \mathrm{ClientEffects}
\end{array}
$$</p>
<p>共有される部分は，利用箇所によってServer ComponentにもClient ComponentにもなりうるComponentとして動作する. サーバー専用の操作はServer側でのみ，クライアント専用の操作はClient側でのみ利用できる. これは「同型」ではなく，エフェクトシグネチャの部分的な重なりに基づく<strong>互換性(compatibility)</strong>として見る方がよい.</p>
<hr>
<h1>結: UI = f(State)を書き直す</h1>
<p>ここまでの議論をまとめよう.</p>
<h2>素朴な等式の問題</h2>
<p>$$\mathrm{UI} = f(\mathrm{State})$$</p>
<p>この等式の問題点:</p>
<ol>
<li><p><code>f</code> は純粋関数ではない — Hooksを通じてエフェクトを実行する</p>
</li>
<li><p>「冪等」の誤用 — Component関数は数学的に冪等ではない（冪等として表現できるのはレンダリング操作）</p>
</li>
<li><p>合成が通常の関数合成だけでは捉えきれない — エフェクトの解釈はKleisli合成として表現できるが，Rules of Hooksで固定されたHook列はApplicativeな構造として見る方が自然</p>
</li>
<li><p>Client-Serverは同型ではない — 異なるエフェクトハンドラとして表現できる</p>
</li>
</ol>
<h2>より正確な記述</h2>
<p>React Componentは，この文章の読み方では，JSの表面構文上はエフェクトMonad <code>R</code> のKleisli射として表現できる:</p>
<p>$$\mathrm{Component}: \mathrm{Props} \to R(\mathrm{VDOM})$$</p>
<p>ただし，Hook操作のプログラム構造を揃えて扱うなら，propsも外側の引数としてではなく，概念的なHook操作として内側に入れる:</p>
<p>$$\operatorname{useProps}: R(\mathrm{Props})$$</p>
<p>この場合，この式が捉えるのは「ComponentがReactランタイムに解釈されるエフェクトフルな計算である」という側面であって，Rules of Hooksが要求する静的なHook列そのものではない. Hook操作のプログラム構造については，Rules of Hooksが先に形を固定している. その制約があるから，<code>ReadProps</code> も含めて次のようなApplicative / Free Applicative的なスケルトンへ写せる:</p>
<p>$$\mathrm{HookProgram}\ A \approx \mathrm{FreeApplicative}(\mathrm{HookOp})\ A$$</p>
<p>UIの生成は，エフェクトハンドラ <code>h</code> による解釈として表せる:</p>
<p>$$\mathrm{UI} = h_{\mathrm{props}}(\mathrm{ComponentProgram})$$</p>
<p>ここで:</p>
<ul>
<li><p><code>ComponentProgram: HookProgram(VDOM)</code> は，<code>useProps</code>, <code>useState</code>, <code>useContext</code> などを含む，ユーザーが書くエフェクトフルな計算の<strong>記述</strong></p>
</li>
<li><p><code>h_props: HookProgram(VDOM) -&gt; DOM</code> は，現在のpropsを供給しながらReact Fiberランタイムが計算を解釈することの表現</p>
</li>
</ul>
<p>そして，ユーザーのEffectや外部I/Oを捨象したDOM状態変換として見るなら，レンダリング操作 <code>u_s = h(render(s))</code> はDOM上の冪等な自己準同型として表現できる:</p>
<p>$$u_{s}(u_{s}(\operatorname{dom})) = u_{s}(\operatorname{dom})$$</p>
<hr>
<p>$$\mathrm{UI} = f(\mathrm{State})$$</p>
<p>は教育的な直感としては有用だが，Reactの振る舞いを数学的に細かく見るには，いくつかの補足が必要になる.</p>
<p>React Componentは数学的な意味での純粋関数そのものではない. React Fiberによる解釈という側面はAlgebraic Effectsシステムにおけるエフェクトフルな計算として表現できる. 一方で，静的なHook列については表現がRules of Hooksを説明するのではなく，Rules of Hooksが先にプログラム形を制約する. その結果として，propsも <code>useProps</code> として含めたApplicative / Free Applicative的な構造へ忠実に写せる，と読む方が自然だ.</p>
<p>$$\mathrm{UI} = \mathrm{ReactHandle}_{\mathrm{props}}(\mathrm{ComponentProgram})$$</p>
<p>この認識によって，Reactの「ルール」やHooksの振る舞い，<code>use</code>とSuspenseの仕組み，Server Componentsの設計原理を，散在する個別知識としてではなく，<strong>エフェクトの解釈</strong> と <strong>静的なHookスケルトン</strong> という二層に分けて見通しやすくなる.</p>
]]></content:encoded>
    </item>
    <item>
      <title>React is React, just. Part 2</title>
      <link>https://wtrclred.vercel.app/ja/posts/08</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/08</guid>
      <description>エッセイ: MetaとBabelとFlowとReact Compiler</description>
      <pubDate>Tue, 24 Feb 2026 00:00:00 GMT</pubDate>
      <dc:date>2026-04-17T09:22:06.000Z</dc:date>
      <category>react</category>
      <category>javascript</category>
      <category>oss</category>
      <category>meta</category>
      <category>babel</category>
      <category>flow</category>
      <category>compiler</category>
      <category>react-compiler</category>
      <category>language</category>
      <category>semantics</category>
      <content:encoded><![CDATA[<blockquote>
<p><strong>注意</strong>: 筆者はMetaの関係者でもReact Compilerの関係者でもありません．本記事はあくまで外部から観測できる情報に基づいた考察です．</p>
</blockquote>
<p><a href="https://wtrclred.vercel.app/posts/01">前回の記事</a>では，React Compilerの登場によってReact Componentが「Just JavaScript」ではなくなったのではないか，という議論をした．
今回はその続きとして，Metaのエコシステム全体を俯瞰し，Reactが「言語」として進化していく様子をより深く考察する．</p>
<h1>MetaとJavaScriptインフラ</h1>
<p>Meta (旧Facebook)はJavaScriptのエコシステムに対して長年にわたり大きな貢献をしてきた．</p>
<h2>Babel</h2>
<p><a href="https://babeljs.io/" target="_blank" rel="noopener noreferrer">Babel</a>は，もともと6to5という名前で始まったJavaScriptのトランスパイラだ．
ES6+やJSXなど，ブラウザがネイティブにサポートしていない構文を変換し，後方互換性を保つ役割を担っている．</p>
<blockquote>
<p>JSX was created as an extension to ECMAScript with the purpose of designing a more concise and easy-to-understand syntax for building DOM tree structures and attributes.
— <a href="https://medium.com/swlh/the-role-of-babel-in-react-dbcf78c69125" target="_blank" rel="noopener noreferrer">The Role of Babel in React</a></p>
</blockquote>
<p>Babelは独立したオープンソースプロジェクトだが，Reactのエコシステムにおいて不可欠な存在だ．
そして，<strong>React CompilerもBabel Pluginとして実装されている</strong>．</p>
<h2>Flow</h2>
<p><a href="https://flow.org/" target="_blank" rel="noopener noreferrer">Flow</a>はMetaが開発しているJavaScriptの静的型チェッカーだ．
<strong>FlowもBabel Plugin (<code>@babel/preset-flow</code>)として実装されており</strong>，型注釈や独自構文を実行時に削除する役割を担っている．</p>
<p>そして今日の主題の一つとして，<strong>Flowは型チェッカーを超えて「言語」としての拡張を行っている</strong>．</p>
<h1>FlowのComponent SyntaxとHook Syntax</h1>
<p>2024年4月，Flowチームは<a href="https://flow.org/blog/2024/04/03/New-Flow-Language-Features-for-React/" target="_blank" rel="noopener noreferrer">Component Syntax</a>を発表した．
これはReactのプリミティブであるコンポーネントとフックに対する <strong>一級言語サポート</strong> だ．</p>
<h2>なぜ追加されたのか</h2>
<blockquote>
<p>Component Syntax was designed in coordination with the React team, and is already being used across Meta&#39;s codebases.
— <a href="https://flow.org/blog/2024/04/03/New-Flow-Language-Features-for-React/" target="_blank" rel="noopener noreferrer">New Flow Language Features for React</a></p>
</blockquote>
<blockquote>
<p>These features bring improved ergonomics, expressiveness, and <strong>static enforcement for many of the Rules of React</strong>.
— <a href="https://flow.org/blog/2024/04/03/New-Flow-Language-Features-for-React/" target="_blank" rel="noopener noreferrer">Flow Blog</a></p>
</blockquote>
<p>つまり，FlowのComponent Syntaxは<strong>Reactチームと連携して設計</strong> され，<strong>Rules of Reactを静的に強制する</strong> ことを目的としている．</p>
<h2>Component Syntaxの構文</h2>
<p>通常の関数コンポーネントではなく，<code>component</code>キーワードを使って定義する:</p>
<pre><code class="language-js">// 従来の関数コンポーネント
function Introduction(props: { name: string, age: number }) {
  return (
    &lt;h1&gt;
      My name is {props.name} and I am {props.age} years old
    &lt;/h1&gt;
  );
}

// Flow の Component Syntax
component Introduction(name: string, age: number) {
  return (
    &lt;h1&gt;
      My name is {name} and I am {age} years old
    &lt;/h1&gt;
  );
}
</code></pre>
<p>参考: <a href="https://flow.org/en/docs/react/component-syntax/" target="_blank" rel="noopener noreferrer">Component Syntax | Flow</a></p>
<p>Component Syntaxでは:</p>
<ul>
<li><p><strong>propsが直接パラメータになる</strong>: ボイラープレートの削減</p>
</li>
<li><p><strong>refの自動処理</strong>: 内部で<code>React.forwardRef</code>でラップされる</p>
</li>
<li><p><strong>明示的なreturnが必須</strong>: すべてのブランチでreturnが必要</p>
</li>
<li><p><strong><code>this</code>の使用禁止</strong>: コンポーネント内で<code>this</code>は使えない</p>
</li>
</ul>
<h2>Hook Syntaxの構文</h2>
<p>同様に，<code>hook</code>キーワードを使ってフックを定義する:</p>
<pre><code class="language-js">// 従来のカスタムフック
function useOnlineStatus(initial: boolean): boolean {
  const [isOnline, setIsOnline] = useState(initial);
  // ...
  return isOnline;
}

// Flow の Hook Syntax
hook useOnlineStatus(initial: boolean): boolean {
  const [isOnline, setIsOnline] = useState(initial);
  // ...
  return isOnline;
}
</code></pre>
<p>参考: <a href="https://flow.org/en/docs/react/hook-syntax/" target="_blank" rel="noopener noreferrer">Hook Syntax | Flow</a></p>
<h2>Flowが検出するもの</h2>
<p>FlowのComponent/Hook Syntaxは，<a href="https://react.dev/reference/rules" target="_blank" rel="noopener noreferrer">Rules of React</a>に違反するコードを <strong>静的に</strong> 検出する:</p>
<ol>
<li><p><strong>Propsのmutation検出</strong>: コンポーネント内でpropsを変更しようとするコード</p>
</li>
<li><p><strong>条件付きフック呼び出しの禁止</strong>: if文内でのフック呼び出しをエラーとして報告</p>
</li>
<li><p><strong>レンダリング中のrefアクセス検出</strong>: レンダリング中にrefを読み書きするコード</p>
</li>
<li><p><strong>ネストしたcomponent/hookの検出</strong>: componentやhook内で別のcomponent/hookを定義するコード</p>
</li>
</ol>
<blockquote>
<p>Flow is able to detect mutations of props in a component or hook.
Another pattern enforced in component bodies is that refs are not read or written to during render.
— <a href="https://flow.org/en/docs/react/component-syntax/" target="_blank" rel="noopener noreferrer">Flow Documentation</a></p>
</blockquote>
<h2>これが意味すること</h2>
<p>FlowのComponent SyntaxとHook Syntaxは，<strong>Reactを「言語」として明示的に定義する試み</strong> だ．</p>
<p>従来，Reactコンポーネントは「JavaScriptの関数」として定義されていた．
しかし，Flowは新しいキーワード(<code>component</code>, <code>hook</code>)を導入することで，<strong>Reactのセマンティクスを言語レベルで表現</strong> している．</p>
<p>これは前回の記事で議論した「ReactはReactだ」という結論を，より明確な形で示している．</p>
<h1>React Compiler: 暗黙的なアプローチ</h1>
<p>Flowが「明示的」に新しい構文を導入しているのに対し，React Compilerは「暗黙的」なアプローチを取っている．</p>
<h2>React Compilerの歴史</h2>
<p>React Compilerは約10年 にわたる研究の成果だ:</p>
<ul>
<li><p><strong>2017年</strong>: FacebookがPrepackでコンパイラを初めて探索</p>
</li>
<li><p><strong>2021年</strong>: Xuan Huangが新しいアプローチの<a href="https://www.youtube.com/watch?v=lGEMwh32soc" target="_blank" rel="noopener noreferrer">最初のイテレーション</a>をデモ</p>
</li>
<li><p><strong>2024年</strong>: ベータ版リリース，React Conf 2024で発表</p>
</li>
<li><p><strong>2025年10月</strong>: <a href="https://react.dev/blog/2025/10/07/react-compiler-1" target="_blank" rel="noopener noreferrer">React Compiler v1.0</a>安定版リリース</p>
</li>
</ul>
<p>参考: <a href="https://react.dev/blog/2025/10/07/react-compiler-1" target="_blank" rel="noopener noreferrer">React Compiler v1.0 – React</a></p>
<h2>Hooks: Reactを「言語」として定義した瞬間</h2>
<p>ここで一つの重要な視点を提示したい．</p>
<p><strong>Reactを「言語」として定義したのはHooksの登場時点だった．</strong></p>
<p>Hooksには<strong>Rules of Hooks</strong>という，JavaScriptには存在しない制約がある:</p>
<ul>
<li><p>トップレベルでのみ呼び出す (ループ，条件分岐，ネストした関数内では呼ばない)</p>
</li>
<li><p>React関数からのみ呼び出す (通常のJavaScript関数からは呼ばない)</p>
</li>
</ul>
<p>これらはJavaScriptの関数には存在しないルールだ．
Hooksを使う関数は，もはや「JavaScriptの関数」ではなく <strong>「Reactの関数」</strong> になる．</p>
<p>つまり，<strong>Hooksの導入こそが，Reactを独自のセマンティクスを持つ「言語」として定義した瞬間だった</strong> と言えるかもしれない．</p>
<h2>Hooksの設計とコンパイラ</h2>
<p>そして，Hooksは最初からコンパイラによる最適化を前提として設計されていた．</p>
<p>React Compiler v1.0のブログには以下の記述がある:</p>
<blockquote>
<p>Hooksの設計自体がコンパイラの将来を見据えて設計されました．
— <a href="https://react.dev/blog/2025/10/07/react-compiler-1" target="_blank" rel="noopener noreferrer">React Compiler v1.0</a></p>
</blockquote>
<p>これは後付けの話ではない．<strong>2018年に公開されたHooksの公式ドキュメント</strong> には，既にこの意図が明記されていた:</p>
<blockquote>
<p>As Svelte, Angular, Glimmer, and others show, ahead-of-time compilation of components has a lot of future potential. Especially if it&#39;s not limited to templates.</p>
<p>— <a href="https://legacy.reactjs.org/docs/hooks-intro.html" target="_blank" rel="noopener noreferrer">Introducing Hooks – React (2018)</a></p>
</blockquote>
<blockquote>
<p>We wanted to present an API that makes it more likely for code to stay on the optimizable path. To solve these problems, Hooks let you use more of React&#39;s features without classes.</p>
<p>— <a href="https://legacy.reactjs.org/docs/hooks-intro.html" target="_blank" rel="noopener noreferrer">Introducing Hooks – React (2018)</a></p>
</blockquote>
<p>つまり，<strong>Hooksは最初からコンパイラによる最適化を前提として設計されていた</strong>．</p>
<h2>Dan Abramovの発言</h2>
<p>当時ReactチームにいたDan Abramovも，コンパイラによる自動メモ化について早い段階から言及していた．</p>
<p>2021年2月に公開された記事&quot;<a href="https://overreacted.io/before-you-memo/" target="_blank" rel="noopener noreferrer">Before You memo()</a>&quot;では，手動でのメモ化の手間について触れた上で，以下のように述べている:</p>
<blockquote>
<p>This last step is annoying, especially for components in between, and ideally a compiler would do it for you. In the future, it might.</p>
<p>— Dan Abramov, <a href="https://overreacted.io/before-you-memo/" target="_blank" rel="noopener noreferrer">Before You memo() (2021)</a></p>
</blockquote>
<p>「将来的には，コンパイラがこれを自動で行うかもしれない」——この発言はReact Compilerが一般公開される3年以上前 のものだ．</p>
<p>Reactチームは長年にわたり，<strong>コンパイラによる自動最適化</strong> という明確なビジョンを持っていた．
これは，Reactが「Just JavaScript」から「独自のセマンティクスを持つ言語」へと進化していく過程が，計画的なものであったことを示している．</p>
<h2>React Compilerが行うこと</h2>
<p>前回の記事でも触れたが，React Compilerは以下のような最適化を自動で行う:</p>
<pre><code class="language-tsx">// 入力
function VideoTag({ heading, videos, filter }) {
  const filteredVideos = [];
  for (const video of videos) {
    if (applyFilter(video, filter)) {
      filteredVideos.push(video);
    }
  }
  // ...
}
</code></pre>
<pre><code class="language-tsx">// 出力 (コンパイル後)
function VideoTab(t36) {
  const $ = useMemoCache(12);
  const { heading, videos, filter } = t36;
  let filteredVideos;

  if ($[0] !== videos || $[1] !== filter) {
    filteredVideos = [];
    for (const video of videos) {
      if (applyFilter(video, filter)) {
        filteredVideos.push(video);
      }
    }
    $[0] = videos;
    $[1] = filter;
    $[2] = filteredVideos;
  } else {
    filteredVideos = $[2];
  }
  // ...
}
</code></pre>
<p>参考: <a href="https://youtu.be/qOQClO3g8-Y?t=497" target="_blank" rel="noopener noreferrer">Understanding Idiomatic React – Joe Savona, Mofei Zhang, React Advanced 2023</a></p>
<h2>条件付きメモ化: useMemoでは不可能なこと</h2>
<p>React Compiler v1.0のブログでは，<strong>手動の<code>useMemo</code>/<code>useCallback</code>では実現できない最適化</strong> の例が示されている:</p>
<pre><code class="language-js">export default function ThemeProvider(props) {
  if (!props.children) {
    return null;
  }
  // コンパイラは条件付きリターン後のコードもメモ化できる
  // （手動の useMemo/useCallback では不可能）
  const theme = mergeTheme(props.theme, use(ThemeContext));
  return &lt;ThemeContext value={theme}&gt;{props.children}&lt;/ThemeContext&gt;;
}
</code></pre>
<blockquote>
<p>これは<code>useMemo</code>/<code>useCallback</code>では実装できない，コンパイラ固有の能力です．
— <a href="https://react.dev/blog/2025/10/07/react-compiler-1" target="_blank" rel="noopener noreferrer">React Compiler v1.0</a></p>
</blockquote>
<h2>Rules of Reactの静的強制</h2>
<p>React Compilerには<strong>Rules of Reactを検証するパス</strong> が含まれており，ESLintプラグインを通じて診断が提供される:</p>
<blockquote>
<p>React Compiler&#39;s ESLint plugin helps developers proactively identify and correct Rules of React violations. The team strongly recommends everyone use the linter today.
— <a href="https://react.dev/blog/2024/10/21/react-compiler-beta-release" target="_blank" rel="noopener noreferrer">React Compiler Beta Release</a></p>
</blockquote>
<h2>Metaでの成果</h2>
<p>React Compilerは既にMetaの本番環境で運用されている:</p>
<ul>
<li><p><strong>Instagram</strong>: 全ページで平均3%改善</p>
</li>
<li><p><strong>Quest Store</strong>: ロード時間が少なくとも4%改善，一部のインタラクションは2倍以上 高速化，初期ロードは最大12%改善</p>
</li>
</ul>
<p>参考: <a href="https://react.dev/blog/2025/10/07/react-compiler-1" target="_blank" rel="noopener noreferrer">React Compiler v1.0</a></p>
<h1>React CompilerとFlowの連携</h1>
<p>ここで重要なのは，<strong>React CompilerはFlowのComponent/Hook Syntaxを明示的にサポートしている</strong> ということだ．</p>
<h2>compilationModeオプション</h2>
<p>React Compilerには<a href="https://react.dev/reference/react-compiler/compilationMode" target="_blank" rel="noopener noreferrer"><code>compilationMode</code></a>というオプションがあり，どの関数をコンパイル対象とするかを制御できる:</p>
<ul>
<li><p><strong><code>&#39;infer&#39;</code></strong> (デフォルト): コンポーネント命名規則(PascalCase)とフック検出(<code>use</code>プレフィックス) を使用</p>
</li>
<li><p><strong><code>&#39;annotation&#39;</code></strong>: <code>&quot;use memo&quot;</code>ディレクティブでマークされた関数のみ</p>
</li>
<li><p><strong><code>&#39;syntax&#39;</code></strong>: <strong>Flowのcomponent/hook構文を使用した関数のみ</strong></p>
</li>
<li><p><strong><code>&#39;all&#39;</code></strong>: すべてのトップレベル関数 (非推奨)</p>
</li>
</ul>
<h2><code>&#39;syntax&#39;</code>モード</h2>
<p><code>&#39;syntax&#39;</code>モードを使用すると，Flowの専用構文で定義されたコンポーネントとフックのみがコンパイル対象となる:</p>
<pre><code class="language-js">// babel.config.js
{
  compilationMode: &quot;syntax&quot;;
}
</code></pre>
<pre><code class="language-js">// ✅ コンパイル対象：Flow component 構文
component Button(label: string) {
  return &lt;button&gt;{label}&lt;/button&gt;;
}

// ✅ コンパイル対象：Flow hook 構文
hook useCounter(initial: number) {
  const [count, setCount] = useState(initial);
  return [count, setCount];
}

// ❌ コンパイル対象外：通常の関数構文
function helper(data) {
  return process(data);
}
</code></pre>
<p>参考: <a href="https://react.dev/reference/react-compiler/compilationMode" target="_blank" rel="noopener noreferrer">compilationMode – React</a></p>
<h2>これが意味すること</h2>
<p>FlowとReact Compilerは <strong>別々のプロジェクトではなく，連携して設計されている</strong>．</p>
<ul>
<li><p>FlowのComponent SyntaxはReactチームと連携して設計された</p>
</li>
<li><p>React CompilerはFlowの構文を明示的にサポートしている</p>
</li>
<li><p>Metaの内部では，この二つが組み合わせて使用されている</p>
</li>
</ul>
<p>つまり，Metaは「Reactを言語として定義する」という一貫した戦略のもとで，FlowとReact Compilerの両方を開発している．</p>
<h1>Reactドキュメントとfunctionキーワード</h1>
<p>ここで一つの観察を共有したい．</p>
<p><a href="https://react.dev/" target="_blank" rel="noopener noreferrer">Reactの公式ドキュメント</a>では，コンポーネントの定義に <strong>一貫して<code>function</code>キーワード</strong> が使われている:</p>
<pre><code class="language-js">export default function Profile() {
  return &lt;img src=&quot;https://i.imgur.com/MK3eW3Am.jpg&quot; alt=&quot;Katherine Johnson&quot; /&gt;;
}
</code></pre>
<p>参考: <a href="https://react.dev/learn/your-first-component" target="_blank" rel="noopener noreferrer">Your First Component – React</a></p>
<p>アロー関数(<code>const Profile = () =&gt; {}</code>) についての言及は <strong>一切ない</strong>．</p>
<h2>なぜアロー関数ではないのか</h2>
<p>Dan Abramovは<a href="https://overreacted.io/how-are-function-components-different-from-classes/" target="_blank" rel="noopener noreferrer">overreacted.io</a>で以下のように述べている:</p>
<blockquote>
<p>it doesn&#39;t matter whether I use arrows or function declarations
— Dan Abramov</p>
</blockquote>
<p>技術的には，アロー関数でも関数宣言でもReactコンポーネントとして問題なく動作する．
しかし，Reactドキュメントが<code>function</code>キーワードを使い続けていることには，いくつかの考察ができる．</p>
<h3>1. FlowのComponent Syntaxとの親和性</h3>
<p>FlowのComponent Syntaxは<code>component</code>キーワードを使う:</p>
<pre><code class="language-js">component Introduction(name: string, age: number) {
  // ...
}
</code></pre>
<p>これは<code>function</code>キーワードを置き換える形で自然に導入できる．
もしReactドキュメントがアロー関数を推奨していたら，この移行は不自然になる．</p>
<h3>2.構文の一貫性</h3>
<p><code>function</code>キーワードによる宣言は，将来的に専用構文(FlowのComponent Syntaxのようなもの) へ移行する場合にも自然な形で置き換えられる．
アロー関数からの移行よりも，関数宣言からの移行の方が構文的に一貫性がある．</p>
<h1>二つのアプローチの対比</h1>
<p>FlowとReact Compilerは，同じ問題に対して異なるアプローチを取っている:</p>
<p><strong>Flow</strong>:</p>
<ul>
<li><p>方法: 新しいキーワード(<code>component</code>, <code>hook</code>)</p>
</li>
<li><p>明示性: 明示的 (新構文)</p>
</li>
<li><p>対象: Metaの内部コードベース</p>
</li>
<li><p>目的: Rules of Reactの静的強制</p>
</li>
</ul>
<p><strong>React Compiler</strong>:</p>
<ul>
<li><p>方法: 既存の構文を変換</p>
</li>
<li><p>明示性: 暗黙的 (変換)</p>
</li>
<li><p>対象: 全Reactユーザー</p>
</li>
<li><p>目的: 自動メモ化 + Rules of Reactの検証</p>
</li>
</ul>
<p>しかし，<strong>どちらも「Reactは独自のセマンティクスを持つ」という方向性では一致している</strong>．</p>
<h1>Rules of React</h1>
<p>ここで改めて<a href="https://react.dev/reference/rules" target="_blank" rel="noopener noreferrer">Rules of React</a>を確認しておこう:</p>
<blockquote>
<p>Rulesは，アイディオマティック（慣用的）で高品質なReactコードを書くためのルールです．単なるガイドラインではなく，これに従わないとバグが生じます．
— <a href="https://react.dev/reference/rules" target="_blank" rel="noopener noreferrer">Rules of React – React</a></p>
</blockquote>
<p>主要なルール:</p>
<ol>
<li><p><strong>ComponentsとHooksはPure（純粋）である必要がある</strong></p>
<ul>
<li><p>冪等性: 同じ入力に対して常に同じ出力</p>
</li>
<li><p>Side Effectsはレンダリングの外側でのみ実行</p>
</li>
<li><p>props, state, Hookの戻り値は不変</p>
</li>
</ul>
</li>
</ol>
<ol start="2">
<li><p><strong>ReactがComponentsとHooksを呼び出す</strong></p>
<ul>
<li><p>コンポーネント関数を直接呼ばない</p>
</li>
<li><p>Hookを通常の値として渡さない</p>
</li>
</ul>
</li>
</ol>
<ol start="3">
<li><p><strong>Rules of Hooks</strong></p>
<ul>
<li><p>トップレベルでのみ呼び出す</p>
</li>
<li><p>React関数からのみ呼び出す</p>
</li>
</ul>
</li>
</ol>
<p>これらのルールは「React的意味論」を形成している．
そして，FlowのComponent SyntaxもReact Compilerも，これらのルールを <strong>静的に強制する</strong> ことを目指している．</p>
<h1>結論: Reactは「言語」として進化している</h1>
<p>前回の記事で私は以下のように結論した:</p>
<blockquote>
<p>Reactは，Reactだ．
構文がJavaScriptと同じで，意味が異なる別の存在だ．</p>
</blockquote>
<p>今回の調査で，この結論はさらに強化された:</p>
<ol>
<li><p><strong>FlowのComponent Syntax</strong>は，Reactのセマンティクスを <strong>明示的に言語として定義</strong> している</p>
</li>
<li><p><strong>React Compiler</strong>は，Reactのセマンティクスを <strong>暗黙的にJavaScriptに変換</strong> している</p>
</li>
<li><p><strong>Hooksは最初からコンパイラを前提として設計されていた</strong></p>
</li>
<li><p><strong>Reactドキュメントは<code>function</code>キーワードを一貫して使用</strong> しており，将来の構文拡張に備えている可能性がある</p>
</li>
</ol>
<p>Reactが「Just JavaScript」であった時代は，徐々に終わりを迎えつつある．
<strong>ReactはReactだ</strong> ——この言葉の意味は，今後ますます重要になっていくだろう．</p>
<hr>
<p>これは私の単なるエッセイであり，React TeamやMetaの公式見解ではありません．</p>
]]></content:encoded>
    </item>
    <item>
      <title>Characterize Vue.js</title>
      <link>https://wtrclred.vercel.app/ja/posts/07</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/07</guid>
      <description>What makes Vue.js, Vue.js</description>
      <pubDate>Thu, 25 Dec 2025 00:00:00 GMT</pubDate>
      <dc:date>2026-04-24T16:40:55.000Z</dc:date>
      <category>vue</category>
      <category>javascript</category>
      <category>oss</category>
      <category>vite</category>
      <category>voidzero</category>
      <category>ecosystem</category>
      <category>community</category>
      <category>language</category>
      <content:encoded><![CDATA[<p>&lt;div style=&quot;text-align: center; margin: 3rem 0;&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 24 24&quot; style=&quot;max-width: 180px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;linearGradient id=&quot;vueGradient&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;100%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#42b883&quot;&gt;
        &lt;animate attributeName=&quot;stop-color&quot; values=&quot;#42b883;#35495e;#42b883&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
      &lt;/stop&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#35495e&quot;&gt;
        &lt;animate attributeName=&quot;stop-color&quot; values=&quot;#35495e;#42b883;#35495e&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
      &lt;/stop&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;path fill=&quot;url(#vueGradient)&quot; d=&quot;M2 3h3.5L12 15l6.5-12H22L12 21zm4.5 0h3L12 7.58L14.5 3h3L12 13.08z&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/path&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<h1>序文</h1>
<p>皆さんにとってVue.jsとはどういうものだろうか？</p>
<p>普段Vue.jsを仕事で書いている人，趣味で触れている人，SNSやメディアを通じてその名前だけを知っている人—— 色々な立場の方がいるだろう．</p>
<p>覚えることが多い？<br>
ルールがなくて分かりづらい？<br>
簡単？<br>
マイグレーションが大変？<br>
でもなんか好き？<br>
やっぱり嫌い？</p>
<p>いろんな感情やイメージを持った方がいると思う．</p>
<p>今日は，皆さんが何となく普段肌で感じているVue.jsの特徴やイメージについて改めて考え，体系的にまとめ直してみようと思う．そういう試みだ．</p>
<p>Vue.jsを<strong>4つのキャラクター</strong> で特徴づけてみる．</p>
<hr>
<p>&lt;div style=&quot;text-align: center; margin: 4rem 0 2rem;&quot;&gt;
&lt;svg viewBox=&quot;0 0 600 100&quot; style=&quot;max-width: 600px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;linearGradient id=&quot;titleGrad1&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;stop offset=&quot;50%&quot; style=&quot;stop-color:#35495e&quot;/&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;animate attributeName=&quot;x1&quot; values=&quot;0%;100%;0%&quot; dur=&quot;6s&quot; repeatCount=&quot;indefinite&quot;/&gt;
      &lt;animate attributeName=&quot;x2&quot; values=&quot;100%;200%;100%&quot; dur=&quot;6s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;text x=&quot;300&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; font-size=&quot;32&quot; font-weight=&quot;bold&quot; fill=&quot;url(#titleGrad1)&quot; font-family=&quot;system-ui, sans-serif&quot;&gt;
    1 / 4 — Approachable
  &lt;/text&gt;
  &lt;line x1=&quot;100&quot; y1=&quot;85&quot; x2=&quot;500&quot; y2=&quot;85&quot; stroke=&quot;url(#titleGrad1)&quot; stroke-width=&quot;2&quot; stroke-dasharray=&quot;5,5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dashoffset&quot; values=&quot;0;-20&quot; dur=&quot;1s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<h1>Vue.js is Easy?</h1>
<p>この言葉を聞いたことがあるだろうか？</p>
<p><strong>「Vue.js is Easy」</strong></p>
<p>巷ではEasy / Simpleという対比もよく目にする．</p>
<p>実は，私自身はこの言葉をあまり好んではいない．<br>
まず前提として，「何を簡単と感じるか」はその人のバックグラウンドに大きく左右されるものであり，かなり主観的だ．<br>
この時点で迂闘に「簡単」という言葉を使うのは避けたいが，加えてこの言葉には<strong>2つの誤解</strong> を生む可能性がある．</p>
<h2>誤解その1：「Vue.jsは難しくなった」</h2>
<p>Vue.jsは2020年にComposition APIとともにversion 3を発表し，大きな変化を得た．<br>
そしてそれから今にあたるまでもたくさんの進化を続けてきた．</p>
<p>そんな中，一定数「Vue.jsは難しくなった」とか，「昔のVue.jsはもっとシンプルでよかった」などといった意見を見ることがある．</p>
<p>確かに，一時期のVue.jsはシンプルで親しみやすく，&quot;簡単&quot; なものを目指していたかもしれない．Vue.jsが今よりも&quot;Easy&quot;だった時代は確かにあるだろう．</p>
<p>しかしここで考えたいのは，<strong>「難しくなったのは本当にVue.jsだろうか？」</strong> という話だ．</p>
<p>私はこれは違うと思う．</p>
<p><strong>難しくなったのはWebだ．</strong></p>
<p>フロントエンドが発足し出した2010年代から，徐々にUIの複雑さや，フロントエンドとしてのコードベースの規模，アプリ全体としてのパフォーマンスの課題が大きくなっていき，Webアプリケーション全体が難しくなった．<br>
Vue.jsはそれらのニーズに追従したのだ．</p>
<p>それを裏付けるものとして，<strong>「Vue.jsはまだ従来の形式をサポートしている」</strong> というのが挙げられる．<br>
具体的には，TypeScriptを利用しないケースや，CDNからVue.jsを利用するケース．Options API．などなど昔からある形式は今もサポートを続けている．</p>
<p>つまり，「難しくなかった頃のVue」を今も利用することは可能だ．</p>
<h2>誤解その2：「Vue.jsにはルールがないからダメだ」</h2>
<p>「Vue.jsは簡単だけどルールがない．それはバグを生むし，コードを汚くする」という誤解だ．</p>
<p>プログラムに対して設計や規約を設ける，というのは一般的には良いプラクティスとされている．私もそう思う．<br>
しかしこれらを設ける上で，注意するべきことがいくつかある．</p>
<p>まず1つ目は <strong>「どの課題を解決するためのルールなのか」</strong> ということだ．<br>
ルールには目的が必要だ．そのルールを設けることによってどういった恩恵が受けられるのかということは常に考えられるべきだ．</p>
<p>そして2つ目は，<strong>それによって発生するトレードオフの考慮</strong> だ．<br>
規約は0コストではない．その制約が強ければ強いほど，それに準拠することを強く求められ，小回りが効かなくなったり，学習コストが高くなったりする．</p>
<p>じゃあ結局あなたはルールはない方が良いと言っているのですか？と言いたくなるかもしれないがそうではない．<br>
<strong>「トレードオフに則ってあなたに合うものを選択しましょう．」</strong> ということだ．</p>
<p>Webアプリケーションというのはあまりに多種多様だ．それを開発しているメンバーや会社の状況なども含め多種多様だ．<br>
そして，それら全てに通じる銀の弾丸は存在しない．</p>
<p>加えて，設計や規約は0/1のものでもない．<br>
私は「ルールは必要ない」と言いたいわけではなく，<strong>「ルールは必要だが，色々なものがある．それを選択できる価値は大きい．」</strong> と言いたいわけだ．</p>
<p>そこをフレームワークが責任を負うか，実装者が責任を負うかはトレードオフであり，<br>
実装者が負う場合には多少の責任を請け負う代わりに規約の柔軟さを手に入れることができる．</p>
<p>「Vue.jsは簡単だけどルールがないからダメだ」という反応はこのトレードオフを十分に考慮できていない．</p>
<h2>Vue.js is Approachable</h2>
<p>&lt;div style=&quot;text-align: center; margin: 3rem 0;&quot;&gt;
&lt;svg viewBox=&quot;0 0 400 120&quot; style=&quot;max-width: 400px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;linearGradient id=&quot;approachGrad&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#35495e&quot;/&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;rect x=&quot;20&quot; y=&quot;20&quot; width=&quot;360&quot; height=&quot;80&quot; rx=&quot;10&quot; fill=&quot;none&quot; stroke=&quot;url(#approachGrad)&quot; stroke-width=&quot;2&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,1000;880,0;0,1000&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/rect&gt;
  &lt;text x=&quot;200&quot; y=&quot;70&quot; text-anchor=&quot;middle&quot; font-size=&quot;24&quot; font-weight=&quot;bold&quot; fill=&quot;url(#approachGrad)&quot; font-family=&quot;system-ui, sans-serif&quot;&gt;
    Approachable
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0;1;1;0&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<p>それでは，改めてVue.jsとは何なのだろうか？<br>
私は，<strong>Vue.js is Approachable</strong>だと思っている．</p>
<p>Approachableという言葉は実は公式ドキュメントのトップにも登場している．</p>
<p>https://vuejs.org/</p>
<p>さまざまなトレードオフがあるが，一貫して <strong>「いろんなユースケースにアプローチしやすい」</strong> というところがかなりポイントだと思っている．</p>
<p>また，Vue.jsは<strong>&quot;The Progressive Framework&quot;</strong>とも呼ばれている．<br>
これはApproachableと関連する概念で，「あなたのプロジェクトと共に成長し，あなたのニーズに適応できるフレームワーク」ということだ．<br>
ビルドステップなしの静的HTML拡張から始めて，必要に応じてSPAやSSR，SSGへとステップアップしていくこともできるし，最初からフルスタックな構成で始めることもできる．<br>
どんな規模でも，どんな段階からでもアプローチできる——これもまたVue.jsの柔軟さの表れだ．</p>
<p>https://ja.vuejs.org/guide/introduction#the-progressive-framework</p>
<p>Vue.jsはApproachableなUIフレームワークだ．</p>
<p>難しくなったわけでもなく，簡単さと引き換えにメチャクチャになるものでもない．<br>
<strong>柔軟に選択できる</strong>，ということだ．</p>
<blockquote>
<p>Vue.jsは難しくなったWebに追従した．従来の使い方も選択できる．<br>
実装者が規約や設計を柔軟に選択できる．</p>
</blockquote>
<p>これが，今日話したいVue.jsのキャラクターの<strong>1つ目</strong> だ．</p>
<blockquote>
<p>この話題についてさらに深掘りした記事も書いている．興味があればぜひ．</p>
<p><a href="https://wtrclred.vercel.app/posts/06">Vue.js is not Easy. It is Approachable.</a></p>
</blockquote>
<hr>
<p>&lt;div style=&quot;text-align: center; margin: 4rem 0 2rem;&quot;&gt;
&lt;svg viewBox=&quot;0 0 600 100&quot; style=&quot;max-width: 600px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;linearGradient id=&quot;titleGrad2&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#35495e&quot;/&gt;
      &lt;stop offset=&quot;50%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#35495e&quot;/&gt;
      &lt;animate attributeName=&quot;x1&quot; values=&quot;0%;100%;0%&quot; dur=&quot;6s&quot; repeatCount=&quot;indefinite&quot;/&gt;
      &lt;animate attributeName=&quot;x2&quot; values=&quot;100%;200%;100%&quot; dur=&quot;6s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;text x=&quot;300&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; font-size=&quot;32&quot; font-weight=&quot;bold&quot; fill=&quot;url(#titleGrad2)&quot; font-family=&quot;system-ui, sans-serif&quot;&gt;
    2 / 4 — Language
  &lt;/text&gt;
  &lt;line x1=&quot;100&quot; y1=&quot;85&quot; x2=&quot;500&quot; y2=&quot;85&quot; stroke=&quot;url(#titleGrad2)&quot; stroke-width=&quot;2&quot; stroke-dasharray=&quot;5,5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dashoffset&quot; values=&quot;0;-20&quot; dur=&quot;1s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<h1>Vue.jsは言語だ</h1>
<p>Vue.jsが言語であることを説明してみる前に，まずはプログラミング言語のおさらいをしよう．</p>
<h2>プログラミング言語とは</h2>
<p>プログラミング言語とは何だろうか？<br>
プログラミング言語はプログラムを書くための言語だ．名前の通りだ．</p>
<p>皆さんはこの世界にたくさんのプログラミング言語が存在していることを知っているはずだ．<br>
JavaScriptやTypeScript，Rust，C++，Haskell．<br>
それだけではない．HTMLやCSS，JSONもそのうちの1つだ．</p>
<p>これらは「<strong>汎用プログラミング言語</strong>」や，「<strong>ドメイン固有言語(DSL)</strong>」などのように分類される．どちらも言語だ．</p>
<p>どうしてこんなにもたくさんの言語が存在しているのだろうか？</p>
<p>ここで改めて考えて欲しいのだが，プログラムを記述しておくために，コンピューターにとって必要な形式というものはどういうものだろうか？</p>
<p>答えは <strong>バイナリ</strong> だ．これだけあれば全てのものは記述することができる．<br>
ほとんどの状況で，任意の言語は最終的にはバイナリとなって動作している．</p>
<p>じゃあプログラミング言語というのは誰のためのものなのだろうか？<br>
これは紛れもなく私たち，<strong>「人間」</strong> のためのものだ．</p>
<p>&lt;div style=&quot;text-align: center; margin: 3rem 0;&quot;&gt;
&lt;svg viewBox=&quot;0 0 620 300&quot; style=&quot;max-width: 620px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;marker id=&quot;languageArrow&quot; markerWidth=&quot;10&quot; markerHeight=&quot;7&quot; refX=&quot;9&quot; refY=&quot;3.5&quot; orient=&quot;auto&quot;&gt;
      &lt;polygon points=&quot;0 0, 10 3.5, 0 7&quot; fill=&quot;#35495e&quot;/&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;</p>
<p>&lt;text x=&quot;135&quot; y=&quot;32&quot; text-anchor=&quot;middle&quot; font-size=&quot;14&quot; fill=&quot;#35495e&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;人間が読める意味へ&lt;/text&gt;</p>
<p>&lt;rect x=&quot;40&quot; y=&quot;214&quot; width=&quot;190&quot; height=&quot;56&quot; rx=&quot;8&quot; fill=&quot;#35495e&quot; fill-opacity=&quot;0.06&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot;/&gt;
  &lt;text x=&quot;135&quot; y=&quot;237&quot; text-anchor=&quot;middle&quot; font-size=&quot;16&quot; fill=&quot;#35495e&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;バイナリ&lt;/text&gt;
  &lt;text x=&quot;135&quot; y=&quot;257&quot; text-anchor=&quot;middle&quot; font-size=&quot;12&quot; fill=&quot;#666&quot; font-family=&quot;monospace&quot;&gt;01010110...&lt;/text&gt;</p>
<p>&lt;rect x=&quot;40&quot; y=&quot;136&quot; width=&quot;190&quot; height=&quot;56&quot; rx=&quot;8&quot; fill=&quot;#35495e&quot; fill-opacity=&quot;0.06&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot;/&gt;
  &lt;text x=&quot;135&quot; y=&quot;159&quot; text-anchor=&quot;middle&quot; font-size=&quot;16&quot; fill=&quot;#35495e&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;アセンブリ&lt;/text&gt;
  &lt;text x=&quot;135&quot; y=&quot;179&quot; text-anchor=&quot;middle&quot; font-size=&quot;12&quot; fill=&quot;#666&quot; font-family=&quot;monospace&quot;&gt;mov, jmp...&lt;/text&gt;</p>
<p>&lt;path d=&quot;M135 210 L135 198 M128 205 L135 198 L142 205&quot; fill=&quot;none&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2.3&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;/&gt;
  &lt;path d=&quot;M135 132 L135 120 M128 127 L135 120 L142 127&quot; fill=&quot;none&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2.3&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;/&gt;</p>
<p>&lt;rect x=&quot;40&quot; y=&quot;58&quot; width=&quot;190&quot; height=&quot;56&quot; rx=&quot;8&quot; fill=&quot;#42b883&quot; fill-opacity=&quot;0.14&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;2.5&quot;/&gt;
  &lt;text x=&quot;135&quot; y=&quot;81&quot; text-anchor=&quot;middle&quot; font-size=&quot;16&quot; fill=&quot;#35495e&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;高水準言語&lt;/text&gt;
  &lt;text x=&quot;135&quot; y=&quot;101&quot; text-anchor=&quot;middle&quot; font-size=&quot;12&quot; fill=&quot;#35495e&quot; font-family=&quot;system-ui&quot;&gt;C, JS, Rust...&lt;/text&gt;</p>
<p>&lt;line x1=&quot;246&quot; y1=&quot;86&quot; x2=&quot;318&quot; y2=&quot;86&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;3&quot; marker-end=&quot;url(#languageArrow)&quot;/&gt;</p>
<p>&lt;text x=&quot;450&quot; y=&quot;82&quot; text-anchor=&quot;middle&quot; font-size=&quot;24&quot; fill=&quot;#35495e&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Vue.js — DSL for UI&lt;/text&gt;
&lt;line x1=&quot;320&quot; y1=&quot;102&quot; x2=&quot;580&quot; y2=&quot;102&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;2&quot;/&gt;</p>
<p>&lt;circle cx=&quot;337&quot; cy=&quot;138&quot; r=&quot;15&quot; fill=&quot;#42b883&quot;/&gt;
  &lt;text x=&quot;337&quot; y=&quot;143&quot; text-anchor=&quot;middle&quot; font-size=&quot;13&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;1&lt;/text&gt;
  &lt;text x=&quot;365&quot; y=&quot;143&quot; font-size=&quot;15&quot; fill=&quot;#35495e&quot; font-family=&quot;system-ui&quot;&gt;HTML + CSS + JS を拡張&lt;/text&gt;</p>
<p>&lt;circle cx=&quot;337&quot; cy=&quot;180&quot; r=&quot;15&quot; fill=&quot;#42b883&quot;/&gt;
  &lt;text x=&quot;337&quot; y=&quot;185&quot; text-anchor=&quot;middle&quot; font-size=&quot;13&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;2&lt;/text&gt;
  &lt;text x=&quot;365&quot; y=&quot;185&quot; font-size=&quot;15&quot; fill=&quot;#35495e&quot; font-family=&quot;system-ui&quot;&gt;宣言的 UI を記述&lt;/text&gt;</p>
<p>&lt;circle cx=&quot;337&quot; cy=&quot;222&quot; r=&quot;15&quot; fill=&quot;#42b883&quot;/&gt;
  &lt;text x=&quot;337&quot; y=&quot;227&quot; text-anchor=&quot;middle&quot; font-size=&quot;13&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;3&lt;/text&gt;
  &lt;text x=&quot;365&quot; y=&quot;227&quot; font-size=&quot;15&quot; fill=&quot;#35495e&quot; font-family=&quot;system-ui&quot;&gt;コンポーネントで整理&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<p>世の中には多様な記述対象がある．<br>
ハードウェアの制御や，OS，ブラウザ，Webアプリケーション，CLIツールまで．<br>
そしてこれらに必要な知識は異なる．</p>
<p>それぞれの領域で，いろんなコンセプトをもとに人間にとっての &quot;書きやすさ&quot; や &quot;正しさ&quot; を提供するためにたくさんの言語が存在しているわけだ．</p>
<p>非構造的なプログラミングから始まり，制御としての知識をプログラミング言語自体に与えるために構文を用意し，OOPをやる為にOOPの言語を作り，FPをやる為にFPの言語を作り，メモリ管理から解放されるためにまた新しい言語を作り，データを記述するためにデータ記述言語を作る．</p>
<h2>Vue.jsはUIを記述するための言語</h2>
<p>それでは，Vue.jsはどうだろうか．<br>
Vue.jsは<strong>UIを記述するための言語</strong> だと捉えることができる．</p>
<p>すでに，UIを記述するための言語として，HTML, CSS, そしてJavaScriptがある．<br>
Vue.jsはこれらをベースとして，足りない知識を拡張している．</p>
<h3>1.宣言的なテンプレート記述</h3>
<p>UIのtemplateを宣言的に，かつ動的に記述するということ．<br>
このプラクティスはHTML, CSS, JavaScriptが登場した時期にはあまりないものだった．</p>
<p>Webが発展していくとともに，クライアントで動作するコードの複雑さは大きくなり，さまざまなUIの記述方法が乱立し，安定性や可読性の観点からUIは宣言的に記述できる方が良いということがわかってきた．</p>
<p>Vue.jsはtemplateを記述するための言語をサポートし，この問題を解決した．</p>
<h3>2.コンポーネント中心の設計</h3>
<p>関連するHTML, CSS, JavaScriptを1つにまとめて記述し，UIに関連する記述の凝集度を高める．<br>
加えて，scoped cssやslotの記述もVue.jsが提供している拡張知識の1つだ．</p>
<pre><code class="language-vue">&lt;script setup lang=&quot;ts&quot;&gt;
import { ref } from &quot;vue&quot;;
const count = ref(0);
function increment() {
  count.value++;
}
&lt;/script&gt;

&lt;template&gt;
  &lt;button type=&quot;button&quot; @click=&quot;increment&quot;&gt;count is: {{ count }}&lt;/button&gt;
&lt;/template&gt;

&lt;style scoped&gt;
button {
  color: #42b883;
}
&lt;/style&gt;
</code></pre>
<p>ここで注目したいのは，<code>&lt;script&gt;</code> 部分では<strong>JavaScriptのメンタルモデルをほぼそのまま使える</strong> ということだ．<br>
ViewとViewModelの境界——つまりReactivityにさえ気をつけていれば，あとは普通のJavaScriptだ．<br>
特別なルールやフレームワーク固有の制約はほとんどない．これもVue.jsのApproachableさの一つだ．</p>
<h3>3. Optimizerとしてのコンパイラ</h3>
<p>コンパイラによって静的に解析できるパフォーマンスチューニングは人の手で行うべきではない．<br>
こういった最適化はコンパイラに任せて，私たちはUIを記述するために必要な本質的なことに集中しているべきだ．</p>
<p>Vue.jsのコンパイラはさまざまな最適化を行なってくれる．</p>
<ul>
<li><p><strong>Static Hoisting</strong>: 静的なノードを巻き上げ，再生成を回避</p>
</li>
<li><p><strong>Patch Flags</strong>: 動的な部分にフラグを付与し，差分検出を高速化</p>
</li>
<li><p><strong>Tree Flattening</strong>: ネストした静的ノードをフラット化</p>
</li>
<li><p><strong>Vapor Mode</strong>: 仮想DOMを介さない，より直接的なレンダリング</p>
</li>
</ul>
<h2>言語機能としての構文</h2>
<p>皆さんはif文やwhile, for, loopなどを知っているはずだ．<br>
しかし，コンピュータからするとこれらは本質的にはただの &quot;判定してジャンプ&quot; だ．<br>
なぜ &quot;判定してジャンプ&quot; という1つの命令に対して複数の構文があるのだろうか？</p>
<p>それぞれのユースケースで意味付けがされていた方が人間にとって分かりやすいからだ．<br>
単に分岐をしたい時には&quot;if&quot;，特定の条件下にある時まで繰り返したい時には&quot;while&quot;．<br>
コンピューターにとってはただのジャンプでも，人間にとってはこういった意味付がプログラムを書く時の補助になる．</p>
<p>Vue.jsについても同じだ．<br>
プログラム上，コンポーネント間のデータの受け渡しは，その名の通り &quot;受け渡し&quot; でしかない．<br>
しかし，UIを記述する際にはUIの知識を拡張し，意味付を行うことができる．</p>
<ul>
<li><p>コンポーネントにデータをバインドしたい時には<code>v-bind</code></p>
</li>
<li><p>イベントを購読/発行したい時には<code>v-on</code>, <code>emit</code></p>
</li>
<li><p>コンポーネントにテンプレートを差し込みたい時には<code>slot</code></p>
</li>
</ul>
<p>そう，<strong>Vue.jsはUIを記述する時に発生する知識を言語として整理した</strong> のだ．<br>
これはかなりプログラミング言語の価値観そのものだ．</p>
<h2>言語ツール</h2>
<p>そしてもう1つ，忘れてはいけないことは <strong>「Vue.jsには十分に発達した言語ツールがある」</strong> ということだ．</p>
<p>プログラミング言語を作る際にはさまざまな課題がある．<br>
仕様の決定，コンパイラの実装，そして言語ツールだ．<br>
たくさんの時間と労力がかかる．</p>
<p>確かに，一昔前のVue.jsは他のフレームワークに比べて少し遅れていた．<br>
だが時間は進み，多大な努力とともにかなり改善されている．</p>
<p>ここで改めて，<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener noreferrer">vuejs/language-tools</a>，そして<a href="https://github.com/johnsoncodehk" target="_blank" rel="noopener noreferrer">Johnson Chu</a>さん，コミュニティメンバーの皆さんに感謝したい．</p>
<blockquote>
<p>「Vue.jsは言語だ」という話についてさらに深掘りした記事も書いている．</p>
<p><a href="https://wtrclred.vercel.app/posts/05">What is Vue.js? It&#39;s just a language lol</a></p>
</blockquote>
<hr>
<p>&lt;div style=&quot;text-align: center; margin: 4rem 0 2rem;&quot;&gt;
&lt;svg viewBox=&quot;0 0 600 100&quot; style=&quot;max-width: 600px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;linearGradient id=&quot;titleGrad3&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;stop offset=&quot;50%&quot; style=&quot;stop-color:#35495e&quot;/&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;animate attributeName=&quot;x1&quot; values=&quot;0%;100%;0%&quot; dur=&quot;6s&quot; repeatCount=&quot;indefinite&quot;/&gt;
      &lt;animate attributeName=&quot;x2&quot; values=&quot;100%;200%;100%&quot; dur=&quot;6s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;text x=&quot;300&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; font-size=&quot;32&quot; font-weight=&quot;bold&quot; fill=&quot;url(#titleGrad3)&quot; font-family=&quot;system-ui, sans-serif&quot;&gt;
    3 / 4 — Ecosystem
  &lt;/text&gt;
  &lt;line x1=&quot;100&quot; y1=&quot;85&quot; x2=&quot;500&quot; y2=&quot;85&quot; stroke=&quot;url(#titleGrad3)&quot; stroke-width=&quot;2&quot; stroke-dasharray=&quot;5,5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dashoffset&quot; values=&quot;0;-20&quot; dur=&quot;1s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<h1>Vue.jsは拡大するエコシステム</h1>
<p>Vue.jsのもう一つの大きな特徴は，<strong>Vue.jsから始まり，Vue.jsの枠を超えて広がっていくエコシステム</strong> だ．</p>
<p>&lt;div style=&quot;text-align: center; margin: 3rem 0;&quot;&gt;
&lt;svg viewBox=&quot;-20 0 740 500&quot; style=&quot;max-width: 740px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;linearGradient id=&quot;evanGrad&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;100%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#646cff&quot;/&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;</p>
<p>&lt;!-- Central Evan You node --&gt;
  &lt;circle cx=&quot;360&quot; cy=&quot;70&quot; r=&quot;50&quot; fill=&quot;url(#evanGrad)&quot;&gt;
    &lt;animate attributeName=&quot;r&quot; values=&quot;50;53;50&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;360&quot; y=&quot;66&quot; text-anchor=&quot;middle&quot; font-size=&quot;13&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Evan You&lt;/text&gt;
  &lt;text x=&quot;360&quot; y=&quot;80&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;#ddd&quot; font-family=&quot;system-ui&quot;&gt;Creator&lt;/text&gt;</p>
<p>&lt;!-- Evan You to Vue.js connection --&gt;
  &lt;line x1=&quot;310&quot; y1=&quot;70&quot; x2=&quot;235&quot; y2=&quot;70&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;3&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,100;100,0;0,100&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;185&quot; cy=&quot;70&quot; r=&quot;45&quot; fill=&quot;#42b883&quot;&gt;
    &lt;animate attributeName=&quot;r&quot; values=&quot;45;48;45&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;185&quot; y=&quot;75&quot; text-anchor=&quot;middle&quot; font-size=&quot;15&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Vue.js&lt;/text&gt;</p>
<p>&lt;!-- Evan to Vite connection (Evan created Vite) --&gt;
  &lt;line x1=&quot;325&quot; y1=&quot;110&quot; x2=&quot;260&quot; y2=&quot;160&quot; stroke=&quot;#646cff&quot; stroke-width=&quot;2&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,100;100,0;0,100&quot; dur=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;</p>
<p>&lt;!-- Vue.js to Vite connection (Vue birthed Vite) --&gt;
  &lt;line x1=&quot;210&quot; y1=&quot;110&quot; x2=&quot;230&quot; y2=&quot;155&quot; stroke=&quot;#646cff&quot; stroke-width=&quot;3&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,60;60,0;0,60&quot; dur=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;250&quot; cy=&quot;195&quot; r=&quot;40&quot; fill=&quot;#646cff&quot;&gt;
    &lt;animate attributeName=&quot;r&quot; values=&quot;40;43;40&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;250&quot; y=&quot;200&quot; text-anchor=&quot;middle&quot; font-size=&quot;13&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Vite&lt;/text&gt;</p>
<p>&lt;!-- VoidZero --&gt;
  &lt;line x1=&quot;360&quot; y1=&quot;120&quot; x2=&quot;360&quot; y2=&quot;160&quot; stroke=&quot;#f59e0b&quot; stroke-width=&quot;3&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,50;50,0;0,50&quot; dur=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;360&quot; cy=&quot;200&quot; r=&quot;40&quot; fill=&quot;#f59e0b&quot;&gt;
    &lt;animate attributeName=&quot;r&quot; values=&quot;40;43;40&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;360&quot; y=&quot;205&quot; text-anchor=&quot;middle&quot; font-size=&quot;11&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;VoidZero&lt;/text&gt;</p>
<p>&lt;!-- VoidZero to Vite connection --&gt;
  &lt;line x1=&quot;320&quot; y1=&quot;200&quot; x2=&quot;290&quot; y2=&quot;195&quot; stroke=&quot;#f59e0b&quot; stroke-width=&quot;2&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,50;50,0;0,50&quot; dur=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;</p>
<p>&lt;!-- Volar.js from Vue --&gt;
  &lt;line x1=&quot;140&quot; y1=&quot;70&quot; x2=&quot;95&quot; y2=&quot;70&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;2&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,50;50,0;0,50&quot; dur=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;65&quot; cy=&quot;70&quot; r=&quot;25&quot; fill=&quot;#35495e&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;65&quot; y=&quot;75&quot; text-anchor=&quot;middle&quot; font-size=&quot;8&quot; fill=&quot;#42b883&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Volar.js&lt;/text&gt;</p>
<p>&lt;!-- Volar.js expansions --&gt;
  &lt;line x1=&quot;50&quot; y1=&quot;92&quot; x2=&quot;35&quot; y2=&quot;125&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.7&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,40;40,0;0,40&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;30&quot; cy=&quot;145&quot; r=&quot;16&quot; fill=&quot;#ff5d01&quot; opacity=&quot;0.9&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.7;0.9;0.7&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;30&quot; y=&quot;149&quot; text-anchor=&quot;middle&quot; font-size=&quot;6&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Astro&lt;/text&gt;</p>
<p>&lt;line x1=&quot;80&quot; y1=&quot;92&quot; x2=&quot;95&quot; y2=&quot;125&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.7&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,40;40,0;0,40&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;100&quot; cy=&quot;145&quot; r=&quot;16&quot; fill=&quot;#fcb32c&quot; opacity=&quot;0.9&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.7;0.9;0.7&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;100&quot; y=&quot;149&quot; text-anchor=&quot;middle&quot; font-size=&quot;6&quot; fill=&quot;#000&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;MDX&lt;/text&gt;</p>
<p>&lt;!-- Nuxt from Vue (curved path to avoid Vite) --&gt;
  &lt;line x1=&quot;150&quot; y1=&quot;100&quot; x2=&quot;80&quot; y2=&quot;270&quot; stroke=&quot;#00dc82&quot; stroke-width=&quot;2&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,200;200,0;0,200&quot; dur=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;65&quot; cy=&quot;300&quot; r=&quot;25&quot; fill=&quot;#00dc82&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;65&quot; y=&quot;305&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Nuxt&lt;/text&gt;</p>
<p>&lt;!-- UnJS from Nuxt --&gt;
  &lt;line x1=&quot;65&quot; y1=&quot;325&quot; x2=&quot;65&quot; y2=&quot;355&quot; stroke=&quot;#00dc82&quot; stroke-width=&quot;2&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,40;40,0;0,40&quot; dur=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;65&quot; cy=&quot;380&quot; r=&quot;22&quot; fill=&quot;#00dc82&quot; opacity=&quot;0.9&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.7;0.9;0.7&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;65&quot; y=&quot;380&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;8&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;UnJS&lt;/text&gt;</p>
<p>&lt;!-- Nitro from UnJS --&gt;
  &lt;line x1=&quot;87&quot; y1=&quot;375&quot; x2=&quot;115&quot; y2=&quot;355&quot; stroke=&quot;#00dc82&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,40;40,0;0,40&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;135&quot; cy=&quot;340&quot; r=&quot;16&quot; fill=&quot;#00dc82&quot; opacity=&quot;0.7&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.5;0.7;0.5&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;135&quot; y=&quot;340&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;6&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Nitro&lt;/text&gt;</p>
<p>&lt;!-- h3 from Nitro --&gt;
  &lt;line x1=&quot;151&quot; y1=&quot;340&quot; x2=&quot;180&quot; y2=&quot;340&quot; stroke=&quot;#00dc82&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,30;30,0;0,30&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;198&quot; cy=&quot;340&quot; r=&quot;14&quot; fill=&quot;#00dc82&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.4;0.6;0.4&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;198&quot; y=&quot;340&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;5&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;h3&lt;/text&gt;</p>
<p>&lt;!-- citty from UnJS --&gt;
  &lt;line x1=&quot;87&quot; y1=&quot;380&quot; x2=&quot;120&quot; y2=&quot;380&quot; stroke=&quot;#00dc82&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,35;35,0;0,35&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;138&quot; cy=&quot;380&quot; r=&quot;14&quot; fill=&quot;#00dc82&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.4;0.6;0.4&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;138&quot; y=&quot;380&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;5&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;citty&lt;/text&gt;</p>
<p>&lt;!-- ofetch from UnJS --&gt;
  &lt;line x1=&quot;87&quot; y1=&quot;388&quot; x2=&quot;115&quot; y2=&quot;410&quot; stroke=&quot;#00dc82&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,35;35,0;0,35&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;135&quot; cy=&quot;425&quot; r=&quot;14&quot; fill=&quot;#00dc82&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.4;0.6;0.4&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;135&quot; y=&quot;425&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;5&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;ofetch&lt;/text&gt;</p>
<p>&lt;!-- unplugin from UnJS --&gt;
  &lt;line x1=&quot;80&quot; y1=&quot;400&quot; x2=&quot;95&quot; y2=&quot;445&quot; stroke=&quot;#00dc82&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,50;50,0;0,50&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;105&quot; cy=&quot;465&quot; r=&quot;14&quot; fill=&quot;#00dc82&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.4;0.6;0.4&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;105&quot; y=&quot;463&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;4&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;un-&lt;/text&gt;
  &lt;text x=&quot;105&quot; y=&quot;470&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;4&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;plugin&lt;/text&gt;</p>
<p>&lt;!-- jiti from UnJS --&gt;
  &lt;line x1=&quot;50&quot; y1=&quot;400&quot; x2=&quot;35&quot; y2=&quot;435&quot; stroke=&quot;#00dc82&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,40;40,0;0,40&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;28&quot; cy=&quot;455&quot; r=&quot;14&quot; fill=&quot;#00dc82&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.4;0.6;0.4&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;28&quot; y=&quot;455&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;5&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;jiti&lt;/text&gt;</p>
<p>&lt;!-- mlly from UnJS --&gt;
  &lt;line x1=&quot;45&quot; y1=&quot;395&quot; x2=&quot;20&quot; y2=&quot;405&quot; stroke=&quot;#00dc82&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,30;30,0;0,30&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;8&quot; cy=&quot;415&quot; r=&quot;14&quot; fill=&quot;#00dc82&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.4;0.6;0.4&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;8&quot; y=&quot;415&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;5&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;mlly&lt;/text&gt;</p>
<p>&lt;!-- Analog, TanStack Start from Nitro --&gt;
  &lt;line x1=&quot;135&quot; y1=&quot;356&quot; x2=&quot;135&quot; y2=&quot;380&quot; stroke=&quot;#00dc82&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.4&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,25;25,0;0,25&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;text x=&quot;180&quot; y=&quot;370&quot; text-anchor=&quot;middle&quot; font-size=&quot;5&quot; fill=&quot;#8899aa&quot; font-family=&quot;system-ui&quot;&gt;Analog, TanStack Start...&lt;/text&gt;</p>
<p>&lt;!-- Vite expansions (other frameworks) --&gt;
  &lt;line x1=&quot;250&quot; y1=&quot;235&quot; x2=&quot;250&quot; y2=&quot;275&quot; stroke=&quot;#646cff&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,45;45,0;0,45&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;text x=&quot;250&quot; y=&quot;295&quot; text-anchor=&quot;middle&quot; font-size=&quot;6&quot; fill=&quot;#8899aa&quot; font-family=&quot;system-ui&quot;&gt;React, Svelte,&lt;/text&gt;
  &lt;text x=&quot;250&quot; y=&quot;308&quot; text-anchor=&quot;middle&quot; font-size=&quot;6&quot; fill=&quot;#8899aa&quot; font-family=&quot;system-ui&quot;&gt;Solid, Qwik...&lt;/text&gt;</p>
<p>&lt;!-- Vite to Nuxt connection --&gt;
  &lt;line x1=&quot;215&quot; y1=&quot;215&quot; x2=&quot;85&quot; y2=&quot;285&quot; stroke=&quot;#646cff&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,150;150,0;0,150&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;</p>
<p>&lt;!-- Vitest from VoidZero --&gt;
  &lt;line x1=&quot;400&quot; y1=&quot;190&quot; x2=&quot;465&quot; y2=&quot;160&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;2&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,80;80,0;0,80&quot; dur=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;490&quot; cy=&quot;145&quot; r=&quot;28&quot; fill=&quot;#42b883&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;490&quot; y=&quot;150&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Vitest&lt;/text&gt;</p>
<p>&lt;!-- Rolldown from VoidZero --&gt;
  &lt;line x1=&quot;360&quot; y1=&quot;240&quot; x2=&quot;360&quot; y2=&quot;290&quot; stroke=&quot;#ff6b6b&quot; stroke-width=&quot;2&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,55;55,0;0,55&quot; dur=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;360&quot; cy=&quot;320&quot; r=&quot;28&quot; fill=&quot;#ff6b6b&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;360&quot; y=&quot;325&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Rolldown&lt;/text&gt;</p>
<p>&lt;!-- Oxc from VoidZero --&gt;
  &lt;line x1=&quot;400&quot; y1=&quot;215&quot; x2=&quot;470&quot; y2=&quot;260&quot; stroke=&quot;#f97316&quot; stroke-width=&quot;2&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,90;90,0;0,90&quot; dur=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;500&quot; cy=&quot;285&quot; r=&quot;28&quot; fill=&quot;#f97316&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;500&quot; y=&quot;290&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Oxc&lt;/text&gt;</p>
<p>&lt;!-- Oxc to Rolldown connection --&gt;
  &lt;line x1=&quot;475&quot; y1=&quot;295&quot; x2=&quot;388&quot; y2=&quot;320&quot; stroke=&quot;#ff6b6b&quot; stroke-width=&quot;1.5&quot; stroke-dasharray=&quot;4,4&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.4;0.7;0.4&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;text x=&quot;432&quot; y=&quot;300&quot; text-anchor=&quot;middle&quot; font-size=&quot;5&quot; fill=&quot;#8899aa&quot; font-family=&quot;system-ui&quot;&gt;powers&lt;/text&gt;</p>
<p>&lt;!-- Rolldown to Vite connection --&gt;
  &lt;line x1=&quot;332&quot; y1=&quot;320&quot; x2=&quot;278&quot; y2=&quot;220&quot; stroke=&quot;#ff6b6b&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,120;120,0;0,120&quot; dur=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;</p>
<p>&lt;!-- Oxc expansions (6 tools) --&gt;
  &lt;!-- Parser --&gt;
  &lt;line x1=&quot;528&quot; y1=&quot;270&quot; x2=&quot;565&quot; y2=&quot;210&quot; stroke=&quot;#f97316&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,70;70,0;0,70&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;580&quot; cy=&quot;195&quot; r=&quot;14&quot; fill=&quot;#f97316&quot; opacity=&quot;0.7&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.5;0.7;0.5&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;580&quot; y=&quot;199&quot; text-anchor=&quot;middle&quot; font-size=&quot;5&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Parser&lt;/text&gt;</p>
<p>&lt;!-- Linter --&gt;
  &lt;line x1=&quot;528&quot; y1=&quot;280&quot; x2=&quot;575&quot; y2=&quot;250&quot; stroke=&quot;#f97316&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,55;55,0;0,55&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;590&quot; cy=&quot;235&quot; r=&quot;14&quot; fill=&quot;#f97316&quot; opacity=&quot;0.7&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.5;0.7;0.5&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;590&quot; y=&quot;239&quot; text-anchor=&quot;middle&quot; font-size=&quot;5&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Linter&lt;/text&gt;</p>
<p>&lt;!-- Formatter --&gt;
  &lt;line x1=&quot;528&quot; y1=&quot;285&quot; x2=&quot;575&quot; y2=&quot;285&quot; stroke=&quot;#f97316&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,50;50,0;0,50&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;595&quot; cy=&quot;285&quot; r=&quot;14&quot; fill=&quot;#f97316&quot; opacity=&quot;0.7&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.5;0.7;0.5&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;595&quot; y=&quot;283&quot; text-anchor=&quot;middle&quot; font-size=&quot;4&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Forma-&lt;/text&gt;
  &lt;text x=&quot;595&quot; y=&quot;290&quot; text-anchor=&quot;middle&quot; font-size=&quot;4&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;tter&lt;/text&gt;</p>
<p>&lt;!-- Transformer --&gt;
  &lt;line x1=&quot;528&quot; y1=&quot;295&quot; x2=&quot;575&quot; y2=&quot;325&quot; stroke=&quot;#f97316&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,55;55,0;0,55&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;590&quot; cy=&quot;340&quot; r=&quot;14&quot; fill=&quot;#f97316&quot; opacity=&quot;0.7&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.5;0.7;0.5&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;590&quot; y=&quot;338&quot; text-anchor=&quot;middle&quot; font-size=&quot;4&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Trans-&lt;/text&gt;
  &lt;text x=&quot;590&quot; y=&quot;345&quot; text-anchor=&quot;middle&quot; font-size=&quot;4&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;former&lt;/text&gt;</p>
<p>&lt;!-- Minifier --&gt;
  &lt;line x1=&quot;520&quot; y1=&quot;305&quot; x2=&quot;560&quot; y2=&quot;375&quot; stroke=&quot;#f97316&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,80;80,0;0,80&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;575&quot; cy=&quot;390&quot; r=&quot;14&quot; fill=&quot;#f97316&quot; opacity=&quot;0.7&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.5;0.7;0.5&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;575&quot; y=&quot;394&quot; text-anchor=&quot;middle&quot; font-size=&quot;5&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Minifier&lt;/text&gt;</p>
<p>&lt;!-- Resolver --&gt;
  &lt;line x1=&quot;515&quot; y1=&quot;310&quot; x2=&quot;545&quot; y2=&quot;420&quot; stroke=&quot;#f97316&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,120;120,0;0,120&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;circle cx=&quot;555&quot; cy=&quot;440&quot; r=&quot;14&quot; fill=&quot;#f97316&quot; opacity=&quot;0.7&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.5;0.7;0.5&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;text x=&quot;555&quot; y=&quot;444&quot; text-anchor=&quot;middle&quot; font-size=&quot;5&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Resolver&lt;/text&gt;</p>
<p>&lt;!-- Web Community label --&gt;
  &lt;text x=&quot;360&quot; y=&quot;470&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#42b883&quot; font-family=&quot;system-ui&quot; opacity=&quot;0.8&quot;&gt;
    → Web Community へ広がる
  &lt;/text&gt;</p>
<p>&lt;!-- Outer ring pulse --&gt;
  &lt;circle cx=&quot;360&quot; cy=&quot;260&quot; r=&quot;260&quot; fill=&quot;none&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1&quot; opacity=&quot;0.2&quot;&gt;
    &lt;animate attributeName=&quot;r&quot; values=&quot;260;265;260;275;260&quot; dur=&quot;5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.2;0.15;0.2;0.08;0.2&quot; dur=&quot;5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<h2>Vite — Vueから始まった革命</h2>
<p>皆さんご存知Viteは昨今のフロントエンドには欠かせない存在だ．</p>
<p>元々Viteは<strong><a href="https://github.com/vitejs/vite/commit/820c2cfbefd376b7be2d0ba5ad1fd39d3e45347e" target="_blank" rel="noopener noreferrer">vue-dev-server</a></strong>という名前で始まった．<br>
これは，Vue.jsの作者である<a href="https://github.com/yyx990803" target="_blank" rel="noopener noreferrer">Evan You</a>氏が，ブラウザのNative ESMを利用し，バンドラを使わない開発セットアップを行うというアイデアから始まったものだった．</p>
<p>それから，No-bundle Dev Server for Vue Single-File ComponentsとしてViteに改名され，v2でcoreがframework agnosticになり，「Next Generation Frontend Tooling」という位置付けになった．</p>
<p>そうだ．<strong>ViteはVue.jsから始まり進化を続け，昨今ではVue.jsにとどまらず，フロントエンドのツールとして広がった．</strong></p>
<h2>Volar.js — 言語ツールフレームワーク</h2>
<p>先ほどもVue.jsのキャラクターの一つとして言語の側面と，優れた言語ツールがあることについて触れた．</p>
<p>Vue.jsの言語ツールは過去にもいくつかの実装があったが，今は<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener noreferrer">vuejs/language-tools</a>が公式の言語ツールになっている．</p>
<p>元々これはVolarというVue.js用のVSCode extensionとして始まったもので，コミュニティメンバーの個人的なプロジェクトだった．<br>
Volarはv1.0に到達するまでに2年という長い年月と膨大な労力とともに進化し，Vue.js公式の推奨ツールになった．</p>
<p>そこから，Vue.jsに依存しないコアな機能が<strong><a href="https://volarjs.dev" target="_blank" rel="noopener noreferrer">Volar.js</a></strong>として切り離され，Vue.jsの言語ツールはvuejs/language-toolsという名前になった．</p>
<p>そしてこのVolar.jsは現在，<strong><a href="https://astro.build" target="_blank" rel="noopener noreferrer">Astro</a></strong>や<strong>MDX</strong>，Angularのメタフレームワークである<strong><a href="https://analogjs.org" target="_blank" rel="noopener noreferrer">Analog</a></strong>の言語ツールの基盤として広まっている．</p>
<h2>UnJS — Nuxtから広がるエコシステム</h2>
<p><a href="https://unjs.io" target="_blank" rel="noopener noreferrer">UnJS</a>はUnified JavaScriptまたはUnleash JavaScriptというJavaScriptのライブラリ群だ．</p>
<p>コンセプトとしては，1つの目的を持った1つのライブラリを高品質に，どんな環境でも動作するように，そしてコラボレーティブにといったことが挙げられる．</p>
<p>このUnJSはNuxt2でリードエンジニアをやっていた<a href="https://github.com/pi0" target="_blank" rel="noopener noreferrer">Pooya Parsa</a>氏がNuxtとは別のエコシステムとして開発を続けていたもので，Nuxt3に向けた開発をしている際に切り出されたサーバーエンジンやPooya氏が作っていたライブラリ群をUnJSというオーガナイゼーションにリブランディングされた．</p>
<p>UnJSとNuxtは綿密にコラボレートし，今ではNuxtの多くの機能がUnJSによって実装されている．<br>
HTTPフレームワークである<strong><a href="https://h3.unjs.io" target="_blank" rel="noopener noreferrer">h3</a></strong>や，それをベースにしたサーバーエンジンである<strong><a href="https://nitro.build" target="_blank" rel="noopener noreferrer">Nitro</a></strong>などがその代表だ．<br>
Nitroは<strong><a href="https://analogjs.org" target="_blank" rel="noopener noreferrer">Analog</a></strong>などでも使われている．</p>
<h2>ecosystem-ci — テストの仕組みも共有</h2>
<p>Vue.jsはたくさんのエコシステムを抱えている．<br>
状態管理のライブラリやUIフレームワーク，ルーター，テスティングライブラリなど本当にたくさんある．</p>
<p>Vue2からVue3へのアップグレードではこれらのエコシステムの追従が大きな課題となった．<br>
そこで，これらのダウンストリームのエコシステムのインテグレーションテストを行う仕組みが導入された．</p>
<p>これは元々ViteのSvelteとのインテグレーションテストで取り組まれていたもので，Vue.jsにもこの仕組みが入った．<br>
そして，これもまた，Vue.js / Viteの垣根を超えて，<strong><a href="https://svelte.dev" target="_blank" rel="noopener noreferrer">Svelte</a></strong>や<strong><a href="https://astro.build" target="_blank" rel="noopener noreferrer">Astro</a></strong>，<strong><a href="https://oxc.rs" target="_blank" rel="noopener noreferrer">Oxc</a></strong>などでも同様の仕組みが導入されている．</p>
<hr>
<p>&lt;div style=&quot;text-align: center; margin: 4rem 0 2rem;&quot;&gt;
&lt;svg viewBox=&quot;0 0 600 120&quot; style=&quot;max-width: 600px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;linearGradient id=&quot;voidzeroGrad&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#f59e0b&quot;/&gt;
      &lt;stop offset=&quot;50%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#f59e0b&quot;/&gt;
      &lt;animate attributeName=&quot;x1&quot; values=&quot;0%;100%;0%&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
      &lt;animate attributeName=&quot;x2&quot; values=&quot;100%;200%;100%&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;text x=&quot;300&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;28&quot; font-weight=&quot;bold&quot; fill=&quot;url(#voidzeroGrad)&quot; font-family=&quot;system-ui, sans-serif&quot;&gt;
    VoidZero — 次世代への布石
  &lt;/text&gt;
  &lt;text x=&quot;300&quot; y=&quot;90&quot; text-anchor=&quot;middle&quot; font-size=&quot;14&quot; fill=&quot;#8899aa&quot; font-family=&quot;system-ui, sans-serif&quot;&gt;
    Vite, Vitest, Rolldown, Oxc を統合する新たなビジョン
  &lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<h1>VoidZeroと統一ツールチェーンへの挑戦</h1>
<p>Vue.jsエコシステムの広がりは，さらに大きな挑戦へと繋がっている．</p>
<h2>断片化という課題</h2>
<p><a href="https://github.com/yyx990803" target="_blank" rel="noopener noreferrer">Evan You</a>氏はViteの開発を通じて，JavaScriptエコシステム全体が抱える課題に直面した．</p>
<blockquote>
<p>「あらゆるアプリケーションは無数のサードパーティ依存関係に頼っており，それらがうまく連携するよう設定することは最も困難なタスクの一つだ」</p>
<p>— https://voidzero.dev/posts/announcing-voidzero-inc</p>
</blockquote>
<p>Viteは開発体験を大きく改善したが，内部では依然として様々な依存関係があり，異なるツール間での不整合を埋めるための抽象化やワークアラウンドが必要だった．そして，異なるツール間での重複したパースやシリアライゼーションコストがボトルネックになっていた．</p>
<h2>VoidZeroの誕生</h2>
<p>2024年，<a href="https://github.com/yyx990803" target="_blank" rel="noopener noreferrer">Evan You</a>氏は<a href="https://voidzero.dev" target="_blank" rel="noopener noreferrer">VoidZero</a>を設立した．<br>
<strong>「次世代のJavaScript統一ツールチェーンを構築する」</strong> というビジョンを掲げて．</p>
<p>https://voidzero.dev/posts/announcing-voidzero-inc</p>
<p>VoidZeroのビジョンは4つの特徴を持つ統合開発スタックの構築だ：</p>
<p>&lt;div style=&quot;text-align: center; margin: 3rem 0;&quot;&gt;
&lt;svg viewBox=&quot;0 0 500 300&quot; style=&quot;max-width: 500px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;linearGradient id=&quot;boxGrad&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;100%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#35495e&quot;/&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;</p>
<p>&lt;!-- Unified --&gt;
  &lt;rect x=&quot;20&quot; y=&quot;20&quot; width=&quot;220&quot; height=&quot;60&quot; rx=&quot;10&quot; fill=&quot;url(#boxGrad)&quot; opacity=&quot;0.9&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.9;1;0.9&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/rect&gt;
  &lt;text x=&quot;130&quot; y=&quot;45&quot; text-anchor=&quot;middle&quot; font-size=&quot;14&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Unified&lt;/text&gt;
  &lt;text x=&quot;130&quot; y=&quot;65&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#ccc&quot; font-family=&quot;system-ui&quot;&gt;同じ AST, resolver, module interop&lt;/text&gt;</p>
<p>&lt;!-- High Performance --&gt;
  &lt;rect x=&quot;260&quot; y=&quot;20&quot; width=&quot;220&quot; height=&quot;60&quot; rx=&quot;10&quot; fill=&quot;url(#boxGrad)&quot; opacity=&quot;0.9&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.9;1;0.9&quot; dur=&quot;2s&quot; begin=&quot;0.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/rect&gt;
  &lt;text x=&quot;370&quot; y=&quot;45&quot; text-anchor=&quot;middle&quot; font-size=&quot;14&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;High Performance&lt;/text&gt;
  &lt;text x=&quot;370&quot; y=&quot;65&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#ccc&quot; font-family=&quot;system-ui&quot;&gt;Rust による最大並列化&lt;/text&gt;</p>
<p>&lt;!-- Composable --&gt;
  &lt;rect x=&quot;20&quot; y=&quot;100&quot; width=&quot;220&quot; height=&quot;60&quot; rx=&quot;10&quot; fill=&quot;url(#boxGrad)&quot; opacity=&quot;0.9&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.9;1;0.9&quot; dur=&quot;2s&quot; begin=&quot;1s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/rect&gt;
  &lt;text x=&quot;130&quot; y=&quot;125&quot; text-anchor=&quot;middle&quot; font-size=&quot;14&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Composable&lt;/text&gt;
  &lt;text x=&quot;130&quot; y=&quot;145&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#ccc&quot; font-family=&quot;system-ui&quot;&gt;独立したコンポーネント&lt;/text&gt;</p>
<p>&lt;!-- Runtime Agnostic --&gt;
  &lt;rect x=&quot;260&quot; y=&quot;100&quot; width=&quot;220&quot; height=&quot;60&quot; rx=&quot;10&quot; fill=&quot;url(#boxGrad)&quot; opacity=&quot;0.9&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.9;1;0.9&quot; dur=&quot;2s&quot; begin=&quot;1.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/rect&gt;
  &lt;text x=&quot;370&quot; y=&quot;125&quot; text-anchor=&quot;middle&quot; font-size=&quot;14&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Runtime Agnostic&lt;/text&gt;
  &lt;text x=&quot;370&quot; y=&quot;145&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#ccc&quot; font-family=&quot;system-ui&quot;&gt;あらゆる JS 環境で一貫した体験&lt;/text&gt;</p>
<p>&lt;!-- Arrow down to projects --&gt;
  &lt;line x1=&quot;250&quot; y1=&quot;170&quot; x2=&quot;250&quot; y2=&quot;200&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;2&quot; marker-end=&quot;url(#arrowhead)&quot;/&gt;</p>
<p>&lt;!-- Project boxes --&gt;
  &lt;rect x=&quot;40&quot; y=&quot;210&quot; width=&quot;100&quot; height=&quot;40&quot; rx=&quot;5&quot; fill=&quot;#646cff&quot;/&gt;
  &lt;text x=&quot;90&quot; y=&quot;235&quot; text-anchor=&quot;middle&quot; font-size=&quot;12&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Vite&lt;/text&gt;</p>
<p>&lt;rect x=&quot;160&quot; y=&quot;210&quot; width=&quot;100&quot; height=&quot;40&quot; rx=&quot;5&quot; fill=&quot;#42b883&quot;/&gt;
  &lt;text x=&quot;210&quot; y=&quot;235&quot; text-anchor=&quot;middle&quot; font-size=&quot;12&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Vitest&lt;/text&gt;</p>
<p>&lt;rect x=&quot;280&quot; y=&quot;210&quot; width=&quot;100&quot; height=&quot;40&quot; rx=&quot;5&quot; fill=&quot;#ff6b6b&quot;/&gt;
  &lt;text x=&quot;330&quot; y=&quot;235&quot; text-anchor=&quot;middle&quot; font-size=&quot;12&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Rolldown&lt;/text&gt;</p>
<p>&lt;rect x=&quot;400&quot; y=&quot;210&quot; width=&quot;60&quot; height=&quot;40&quot; rx=&quot;5&quot; fill=&quot;#f97316&quot;/&gt;
  &lt;text x=&quot;430&quot; y=&quot;235&quot; text-anchor=&quot;middle&quot; font-size=&quot;12&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Oxc&lt;/text&gt;</p>
<p>&lt;!-- Open Source badge --&gt;</p>
<p>&lt;text x=&quot;250&quot; y=&quot;280&quot; text-anchor=&quot;middle&quot; font-size=&quot;11&quot; fill=&quot;#42b883&quot; font-family=&quot;system-ui&quot;&gt;Open Source&lt;/text&gt;
&lt;/svg&gt;</p>
<p>&lt;/div&gt;</p>
<h2>Oxc — Rustで書かれた高速コンパイラ</h2>
<p><a href="https://oxc.rs/" target="_blank" rel="noopener noreferrer">Oxc (JavaScript Oxidation Compiler)</a>は，<a href="https://github.com/boshen" target="_blank" rel="noopener noreferrer">Boshen</a>氏が率いるRustで書かれたJavaScript/TypeScriptツールチェーンだ．</p>
<p>パーサー，リンター(Oxlint)，フォーマッター(Oxfmt)，トランスフォーマー，ミニファイアー，リゾルバーを含む包括的なツールセットで，<strong>圧倒的なパフォーマンス</strong> を実現している：</p>
<ul>
<li><p><strong>Oxlint</strong>: ESLintの50-100倍高速</p>
</li>
<li><p><strong>Oxfmt</strong>: Prettierの35倍高速</p>
</li>
<li><p><strong>oxc-resolver</strong>: webpackのenhanced-resolveの30倍高速</p>
</li>
<li><p><strong>transformer</strong>: Babelの40倍高速</p>
</li>
</ul>
<p>https://oxc.rs/</p>
<p>2025年6月にはOxlint 1.0が安定版としてリリースされ，ShopifyやAirbnbなど5,200以上の早期採用者がいる．</p>
<h2>Rolldown — Rustベースのバンドラー</h2>
<p><a href="https://rolldown.rs/" target="_blank" rel="noopener noreferrer">Rolldown</a>はVoidZeroが開発するRustベースのバンドラーで，<strong>Rollup互換のAPIを持ちながら10-30倍高速</strong> なパフォーマンスを実現する．</p>
<p>https://ja.vite.dev/blog/announcing-vite8</p>
<p>2026年3月12日には<strong>Vite 8.0</strong>が安定版としてリリースされ，Rolldownが単一のRustベースバンドラーとして統合された．<br>
これにより，従来のesbuild + Rollupという二重構成は，Vite 8ではRolldownを中心にした一本化されたアーキテクチャへ移行した．</p>
<p>preview / beta期間の実プロジェクトでは，印象的なパフォーマンス改善が報告されている：</p>
<ul>
<li><p>Linear: 本番ビルド時間46秒 → 6秒</p>
</li>
<li><p>Ramp: ビルド時間57%削減</p>
</li>
<li><p>Mercedes-Benz.io: 最大38%削減</p>
</li>
<li><p>Beehiiv: 64%削減</p>
</li>
</ul>
<p>この取り組みにより，Viteは <strong>ビルドツール(Vite)，バンドラー(Rolldown)，コンパイラ(Oxc)</strong>という近いチームが連携してメンテナンスするエンドツーエンドのツールチェーンのエントリーポイントになっていく．</p>
<h2>Vite+ — 統合されたツールチェイン</h2>
<p>さらにVoidZeroは2026年3月13日に<strong>Vite+ Alpha</strong>をリリースした．<br>
Viteのスーパーセットとして，グローバルCLIの<code>vp</code>とプロジェクトローカルの<code>vite-plus</code>パッケージを中心に，開発者に必要なツール群を単一の統合されたツールチェインとして提供する：</p>
<ul>
<li><p><code>vp create</code> — プロジェクトやモノレポのスキャフォールディング</p>
</li>
<li><p><code>vp env</code> — Node.jsランタイムのグローバル / プロジェクト単位の管理</p>
</li>
<li><p><code>vp install</code> — 選択したパッケージマネージャー経由の依存インストール</p>
</li>
<li><p><code>vp dev</code> — Vite開発サーバー</p>
</li>
<li><p><code>vp check</code> — 型チェック，リンティング，フォーマットの一括実行</p>
</li>
<li><p><code>vp test</code> — Vitestによるテスト</p>
</li>
<li><p><code>vp build</code> — 本番ビルド</p>
</li>
<li><p><code>vp run</code> — Vite Taskによるスクリプト / モノレポタスク実行</p>
</li>
<li><p><code>vp pack</code> — tsdownによるライブラリ / CLIパッケージング</p>
</li>
</ul>
<p>https://voidzero.dev/posts/announcing-vite-plus-alpha</p>
<p>https://viteplus.dev/</p>
<h2>信頼とオープンソースへのコミットメント</h2>
<p>ここで重要なのは，VoidZeroの <strong>オープンソースへの姿勢</strong> だ．</p>
<blockquote>
<p>「コミュニティがViteに置いてくれた信頼」が，この方向性を深く考えるきっかけになった</p>
<p>— https://voidzero.dev/posts/announcing-voidzero-inc</p>
</blockquote>
<p>VoidZeroは明確に宣言している：</p>
<blockquote>
<p>「オープンソース化したものは全てオープンソースのまま．Vite, Vitest, Rolldown, Oxcはオープンソース．」</p>
<p>— https://voidzero.dev/posts/announcing-voidzero-inc</p>
</blockquote>
<p>そのVite+ Alphaでは，Vite+自体もMITライセンスで完全にオープンソース化された．</p>
<p>そして重要なのは，VoidZeroは<strong>Vue.jsをファーストクラスでサポートする</strong> ということだ．Vue.jsから始まったこれらのツールは，Vue.jsエコシステムに対して最高の体験を提供し続ける．</p>
<p>これらのプロジェクトは既にOpenAI，Google，Apple，Microsoft，Shopifyなど主要な組織で採用されている．その信頼性は実証済みだ．</p>
<p>https://github.com/vitejs/companies-using-vite</p>
<hr>
<p>&lt;div style=&quot;text-align: center; margin: 4rem 0 2rem;&quot;&gt;
&lt;svg viewBox=&quot;0 0 600 100&quot; style=&quot;max-width: 600px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;linearGradient id=&quot;titleGrad4&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#35495e&quot;/&gt;
      &lt;stop offset=&quot;50%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#35495e&quot;/&gt;
      &lt;animate attributeName=&quot;x1&quot; values=&quot;0%;100%;0%&quot; dur=&quot;6s&quot; repeatCount=&quot;indefinite&quot;/&gt;
      &lt;animate attributeName=&quot;x2&quot; values=&quot;100%;200%;100%&quot; dur=&quot;6s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;text x=&quot;300&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; font-size=&quot;32&quot; font-weight=&quot;bold&quot; fill=&quot;url(#titleGrad4)&quot; font-family=&quot;system-ui, sans-serif&quot;&gt;
    4 / 4 — Community
  &lt;/text&gt;
  &lt;line x1=&quot;100&quot; y1=&quot;85&quot; x2=&quot;500&quot; y2=&quot;85&quot; stroke=&quot;url(#titleGrad4)&quot; stroke-width=&quot;2&quot; stroke-dasharray=&quot;5,5&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dashoffset&quot; values=&quot;0;-20&quot; dur=&quot;1s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<h1>Vue.jsはコミュニティ</h1>
<p>最後のキャラクター．これはVue.jsとは切っても切れない大事な要素だ．</p>
<p>&lt;div style=&quot;text-align: center; margin: 3rem 0;&quot;&gt;
&lt;svg viewBox=&quot;0 0 500 320&quot; style=&quot;max-width: 500px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;radialGradient id=&quot;coreGlow&quot; cx=&quot;50%&quot; cy=&quot;50%&quot; r=&quot;50%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#42b883;stop-opacity:0.4&quot;/&gt;
      &lt;stop offset=&quot;70%&quot; style=&quot;stop-color:#42b883;stop-opacity:0.1&quot;/&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#42b883;stop-opacity:0&quot;/&gt;
    &lt;/radialGradient&gt;
    &lt;linearGradient id=&quot;nodeGrad&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;100%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#64d4a3&quot;/&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#35495e&quot;/&gt;
    &lt;/linearGradient&gt;
    &lt;filter id=&quot;glow&quot;&gt;
      &lt;feGaussianBlur stdDeviation=&quot;2&quot; result=&quot;coloredBlur&quot;/&gt;
      &lt;feMerge&gt;
        &lt;feMergeNode in=&quot;coloredBlur&quot;/&gt;
        &lt;feMergeNode in=&quot;SourceGraphic&quot;/&gt;
      &lt;/feMerge&gt;
    &lt;/filter&gt;
  &lt;/defs&gt;</p>
<p>&lt;!-- Background pulse --&gt;
  &lt;circle cx=&quot;250&quot; cy=&quot;160&quot; r=&quot;80&quot; fill=&quot;url(#coreGlow)&quot;&gt;
    &lt;animate attributeName=&quot;r&quot; values=&quot;80;100;80&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;</p>
<p>&lt;!-- Network connections between nodes --&gt;
  &lt;!-- Inner ring connections --&gt;
  &lt;line x1=&quot;175&quot; y1=&quot;90&quot; x2=&quot;325&quot; y2=&quot;90&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1&quot; opacity=&quot;0.3&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.2;0.5;0.2&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;line x1=&quot;175&quot; y1=&quot;90&quot; x2=&quot;130&quot; y2=&quot;180&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1&quot; opacity=&quot;0.3&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.2;0.5;0.2&quot; dur=&quot;3s&quot; begin=&quot;0.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;line x1=&quot;325&quot; y1=&quot;90&quot; x2=&quot;370&quot; y2=&quot;180&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1&quot; opacity=&quot;0.3&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.2;0.5;0.2&quot; dur=&quot;3s&quot; begin=&quot;1s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;line x1=&quot;130&quot; y1=&quot;180&quot; x2=&quot;190&quot; y2=&quot;260&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1&quot; opacity=&quot;0.3&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.2;0.5;0.2&quot; dur=&quot;3s&quot; begin=&quot;1.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;line x1=&quot;370&quot; y1=&quot;180&quot; x2=&quot;310&quot; y2=&quot;260&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1&quot; opacity=&quot;0.3&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.2;0.5;0.2&quot; dur=&quot;3s&quot; begin=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;line x1=&quot;190&quot; y1=&quot;260&quot; x2=&quot;310&quot; y2=&quot;260&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1&quot; opacity=&quot;0.3&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.2;0.5;0.2&quot; dur=&quot;3s&quot; begin=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;</p>
<p>&lt;!-- Connections to center --&gt;
  &lt;line x1=&quot;175&quot; y1=&quot;90&quot; x2=&quot;250&quot; y2=&quot;160&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,100;100,0&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;line x1=&quot;325&quot; y1=&quot;90&quot; x2=&quot;250&quot; y2=&quot;160&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,100;100,0&quot; dur=&quot;2s&quot; begin=&quot;0.33s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;line x1=&quot;130&quot; y1=&quot;180&quot; x2=&quot;250&quot; y2=&quot;160&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,100;100,0&quot; dur=&quot;2s&quot; begin=&quot;0.66s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;line x1=&quot;370&quot; y1=&quot;180&quot; x2=&quot;250&quot; y2=&quot;160&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,100;100,0&quot; dur=&quot;2s&quot; begin=&quot;1s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;line x1=&quot;190&quot; y1=&quot;260&quot; x2=&quot;250&quot; y2=&quot;160&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,100;100,0&quot; dur=&quot;2s&quot; begin=&quot;1.33s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
  &lt;line x1=&quot;310&quot; y1=&quot;260&quot; x2=&quot;250&quot; y2=&quot;160&quot; stroke=&quot;#42b883&quot; stroke-width=&quot;1.5&quot; opacity=&quot;0.6&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,100;100,0&quot; dur=&quot;2s&quot; begin=&quot;1.66s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;</p>
<p>&lt;!-- Outer nodes for depth --&gt;
  &lt;circle cx=&quot;100&quot; cy=&quot;60&quot; r=&quot;6&quot; fill=&quot;#35495e&quot; opacity=&quot;0.4&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.3;0.6;0.3&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;circle cx=&quot;400&quot; cy=&quot;60&quot; r=&quot;5&quot; fill=&quot;#35495e&quot; opacity=&quot;0.3&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.2;0.5;0.2&quot; dur=&quot;4s&quot; begin=&quot;1s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;circle cx=&quot;60&quot; cy=&quot;240&quot; r=&quot;7&quot; fill=&quot;#35495e&quot; opacity=&quot;0.35&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.25;0.55;0.25&quot; dur=&quot;4s&quot; begin=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;circle cx=&quot;440&quot; cy=&quot;240&quot; r=&quot;5&quot; fill=&quot;#35495e&quot; opacity=&quot;0.3&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.2;0.5;0.2&quot; dur=&quot;4s&quot; begin=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;circle cx=&quot;250&quot; cy=&quot;30&quot; r=&quot;6&quot; fill=&quot;#35495e&quot; opacity=&quot;0.4&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.3;0.6;0.3&quot; dur=&quot;4s&quot; begin=&quot;0.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;
  &lt;circle cx=&quot;250&quot; cy=&quot;300&quot; r=&quot;5&quot; fill=&quot;#35495e&quot; opacity=&quot;0.35&quot;&gt;
    &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.25;0.55;0.25&quot; dur=&quot;4s&quot; begin=&quot;1.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/circle&gt;</p>
<p>&lt;!-- Faint outer connections --&gt;
  &lt;line x1=&quot;100&quot; y1=&quot;60&quot; x2=&quot;175&quot; y2=&quot;90&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;0.5&quot; opacity=&quot;0.2&quot;/&gt;
  &lt;line x1=&quot;400&quot; y1=&quot;60&quot; x2=&quot;325&quot; y2=&quot;90&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;0.5&quot; opacity=&quot;0.2&quot;/&gt;
  &lt;line x1=&quot;60&quot; y1=&quot;240&quot; x2=&quot;130&quot; y2=&quot;180&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;0.5&quot; opacity=&quot;0.2&quot;/&gt;
  &lt;line x1=&quot;440&quot; y1=&quot;240&quot; x2=&quot;370&quot; y2=&quot;180&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;0.5&quot; opacity=&quot;0.2&quot;/&gt;
  &lt;line x1=&quot;250&quot; y1=&quot;30&quot; x2=&quot;250&quot; y2=&quot;160&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;0.5&quot; opacity=&quot;0.15&quot;/&gt;
  &lt;line x1=&quot;250&quot; y1=&quot;300&quot; x2=&quot;250&quot; y2=&quot;160&quot; stroke=&quot;#35495e&quot; stroke-width=&quot;0.5&quot; opacity=&quot;0.15&quot;/&gt;</p>
<p>&lt;!-- Central Vue logo simplified --&gt;
  &lt;g filter=&quot;url(#glow)&quot;&gt;
    &lt;polygon points=&quot;250,120 270,155 280,140 250,95 220,140 230,155&quot; fill=&quot;#42b883&quot; opacity=&quot;0.9&quot;&gt;
      &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/polygon&gt;
    &lt;polygon points=&quot;250,120 265,145 250,175 235,145&quot; fill=&quot;#35495e&quot; opacity=&quot;0.9&quot;&gt;
      &lt;animate attributeName=&quot;opacity&quot; values=&quot;0.8;1;0.8&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/polygon&gt;
  &lt;/g&gt;</p>
<p>&lt;!-- Main community nodes --&gt;
  &lt;g filter=&quot;url(#glow)&quot;&gt;
    &lt;circle cx=&quot;175&quot; cy=&quot;90&quot; r=&quot;14&quot; fill=&quot;#42b883&quot; opacity=&quot;0.85&quot;&gt;
      &lt;animate attributeName=&quot;r&quot; values=&quot;14;16;14&quot; dur=&quot;3s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/circle&gt;
    &lt;circle cx=&quot;325&quot; cy=&quot;90&quot; r=&quot;14&quot; fill=&quot;#42b883&quot; opacity=&quot;0.85&quot;&gt;
      &lt;animate attributeName=&quot;r&quot; values=&quot;14;16;14&quot; dur=&quot;3s&quot; begin=&quot;0.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/circle&gt;
    &lt;circle cx=&quot;130&quot; cy=&quot;180&quot; r=&quot;14&quot; fill=&quot;#42b883&quot; opacity=&quot;0.85&quot;&gt;
      &lt;animate attributeName=&quot;r&quot; values=&quot;14;16;14&quot; dur=&quot;3s&quot; begin=&quot;1s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/circle&gt;
    &lt;circle cx=&quot;370&quot; cy=&quot;180&quot; r=&quot;14&quot; fill=&quot;#42b883&quot; opacity=&quot;0.85&quot;&gt;
      &lt;animate attributeName=&quot;r&quot; values=&quot;14;16;14&quot; dur=&quot;3s&quot; begin=&quot;1.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/circle&gt;
    &lt;circle cx=&quot;190&quot; cy=&quot;260&quot; r=&quot;14&quot; fill=&quot;#42b883&quot; opacity=&quot;0.85&quot;&gt;
      &lt;animate attributeName=&quot;r&quot; values=&quot;14;16;14&quot; dur=&quot;3s&quot; begin=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/circle&gt;
    &lt;circle cx=&quot;310&quot; cy=&quot;260&quot; r=&quot;14&quot; fill=&quot;#42b883&quot; opacity=&quot;0.85&quot;&gt;
      &lt;animate attributeName=&quot;r&quot; values=&quot;14;16;14&quot; dur=&quot;3s&quot; begin=&quot;2.5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/circle&gt;
  &lt;/g&gt;</p>
<p>&lt;!-- Node labels --&gt;</p>
<p>&lt;text x=&quot;175&quot; y=&quot;90&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;8&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Dev&lt;/text&gt;
&lt;text x=&quot;325&quot; y=&quot;90&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;8&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Dev&lt;/text&gt;
&lt;text x=&quot;130&quot; y=&quot;180&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;8&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Dev&lt;/text&gt;
&lt;text x=&quot;370&quot; y=&quot;180&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;8&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Dev&lt;/text&gt;
&lt;text x=&quot;190&quot; y=&quot;260&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;8&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Dev&lt;/text&gt;
&lt;text x=&quot;310&quot; y=&quot;260&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;8&quot; fill=&quot;#fff&quot; font-weight=&quot;bold&quot; font-family=&quot;system-ui&quot;&gt;Dev&lt;/text&gt;
&lt;/svg&gt;</p>
<p>&lt;/div&gt;</p>
<h2>コミュニティから生まれるもの</h2>
<p>ここまで紹介してきたプロジェクトの多くは，コミュニティメンバーの個人的なプロジェクトとして始まったものだ．</p>
<ul>
<li><p><strong>Volar</strong> — <a href="https://github.com/johnsoncodehk" target="_blank" rel="noopener noreferrer">Johnson Chu</a>氏の個人プロジェクトからVue.js公式言語ツールへ</p>
</li>
<li><p><strong>Oxc</strong> — <a href="https://github.com/boshen" target="_blank" rel="noopener noreferrer">Boshen</a>氏が始めたRustパーサーがVoidZeroの中核技術へ</p>
</li>
<li><p><strong>UnJS</strong> — <a href="https://github.com/pi0" target="_blank" rel="noopener noreferrer">Pooya Parsa</a>氏のNuxt外での取り組みが独立エコシステムへ</p>
</li>
</ul>
<p>これらは偶然ではない．<br>
Vue.jsコミュニティには，<strong>個人の情熱がエコシステム全体を動かす力</strong> がある．</p>
<h2>信頼の連鎖</h2>
<p>Vue.jsのエコシステムが他のフレームワークやツールにも影響を与えている現象は，単なる技術的な優位性だけでは説明できない．</p>
<p>それは <strong>信頼</strong> だ．</p>
<p><a href="https://github.com/yyx990803" target="_blank" rel="noopener noreferrer">Evan You</a>氏がVoidZeroを設立する際に述べたように：</p>
<blockquote>
<p>「コミュニティがViteに置いてくれた信頼」</p>
<p>— https://voidzero.dev/posts/announcing-voidzero-inc</p>
</blockquote>
<p>この信頼は一朝一夕に築かれたものではない．<br>
10年以上にわたる一貫したオープンソースへのコミットメント，後方互換性への配慮，そしてコミュニティの声に耳を傾ける姿勢．</p>
<p>その結果として，Vue.jsから始まったツールがReactエコシステムでも使われ，Svelteでも使われ，Solidでも使われている．</p>
<h2>Webコミュニティへの貢献</h2>
<p>これはVue.jsコミュニティだけの話ではない．<br>
<strong>Web開発者コミュニティ全体</strong> への貢献だ．</p>
<ul>
<li><p><a href="https://vite.dev" target="_blank" rel="noopener noreferrer">Vite</a>はVue.js以外のあらゆるフレームワークの開発体験を変えた</p>
</li>
<li><p><a href="https://volarjs.dev" target="_blank" rel="noopener noreferrer">Volar.js</a>はAstroやMDXの言語サポートを可能にした</p>
</li>
<li><p><a href="https://nitro.build" target="_blank" rel="noopener noreferrer">Nitro</a>はNuxt以外のメタフレームワークにも採用された</p>
</li>
<li><p><a href="https://oxc.rs" target="_blank" rel="noopener noreferrer">Oxc</a>と<a href="https://rolldown.rs" target="_blank" rel="noopener noreferrer">Rolldown</a>はJavaScriptツールチェーン全体の高速化に貢献している</p>
</li>
</ul>
<p><strong>一つのコミュニティで生まれたイノベーションが，境界を越えてWeb全体に広がっていく．</strong></p>
<p>これこそが，オープンソースの真の力であり，Vue.jsコミュニティが体現している価値だと私は思う．</p>
<hr>
<p>&lt;div style=&quot;text-align: center; margin: 4rem 0 2rem;&quot;&gt;
&lt;svg viewBox=&quot;0 0 600 150&quot; style=&quot;max-width: 600px; width: 100%;&quot;&gt;
  &lt;defs&gt;
    &lt;linearGradient id=&quot;finGrad&quot; x1=&quot;0%&quot; y1=&quot;0%&quot; x2=&quot;100%&quot; y2=&quot;0%&quot;&gt;
      &lt;stop offset=&quot;0%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;stop offset=&quot;25%&quot; style=&quot;stop-color:#35495e&quot;/&gt;
      &lt;stop offset=&quot;50%&quot; style=&quot;stop-color:#646cff&quot;/&gt;
      &lt;stop offset=&quot;75%&quot; style=&quot;stop-color:#f59e0b&quot;/&gt;
      &lt;stop offset=&quot;100%&quot; style=&quot;stop-color:#42b883&quot;/&gt;
      &lt;animate attributeName=&quot;x1&quot; values=&quot;-100%;100%&quot; dur=&quot;5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
      &lt;animate attributeName=&quot;x2&quot; values=&quot;0%;200%&quot; dur=&quot;5s&quot; repeatCount=&quot;indefinite&quot;/&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;
  &lt;text x=&quot;300&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; font-size=&quot;36&quot; font-weight=&quot;bold&quot; fill=&quot;url(#finGrad)&quot; font-family=&quot;system-ui, sans-serif&quot;&gt;
    Characterize Vue.js
  &lt;/text&gt;
  &lt;text x=&quot;300&quot; y=&quot;100&quot; text-anchor=&quot;middle&quot; font-size=&quot;16&quot; fill=&quot;#666&quot; font-family=&quot;system-ui, sans-serif&quot;&gt;
    Approachable × Language × Ecosystem × Community
  &lt;/text&gt;
  &lt;line x1=&quot;100&quot; y1=&quot;125&quot; x2=&quot;500&quot; y2=&quot;125&quot; stroke=&quot;url(#finGrad)&quot; stroke-width=&quot;3&quot;&gt;
    &lt;animate attributeName=&quot;stroke-dasharray&quot; values=&quot;0,400;400,0;0,400&quot; dur=&quot;4s&quot; repeatCount=&quot;indefinite&quot;/&gt;
  &lt;/line&gt;
&lt;/svg&gt;
&lt;/div&gt;</p>
<h1>結び</h1>
<p>Vue.jsを4つのキャラクターで特徴づけてみた．</p>
<ol>
<li><p><strong>Approachable</strong> — 柔軟に選択できる，いろんなユースケースにアプローチしやすい</p>
</li>
<li><p><strong>Language</strong> — UIを記述するためのドメイン固有言語</p>
</li>
<li><p><strong>Ecosystem</strong> — Vue.jsから始まり，枠を超えて広がるエコシステム</p>
</li>
<li><p><strong>Community</strong> — 個人の情熱がエコシステムを動かし，信頼を築く</p>
</li>
</ol>
<p>これらは独立した特徴ではない．<br>
相互に影響し合い，強化し合っている．</p>
<p>Approachableだからこそ多くの人が参入し，コミュニティが育つ．<br>
コミュニティが育つからこそ，言語ツールやエコシステムが発展する．<br>
エコシステムが発展するからこそ，より多くの選択肢が生まれ，Approachableさが増す．</p>
<p>この循環がVue.jsの価値の本質だと私は思う．</p>
<hr>
<p>Vue.jsはただのUIフレームワークではない．<br>
アプローチしやすく，<br>
人間のために設計された言語であり，<br>
境界を越えて広がるエコシステムであり，<br>
信頼と情熱で繋がるコミュニティだ．</p>
<hr>
<blockquote>
<p>これは私の単なるエッセイであり，Vue.js Teamの公式見解ではありません．</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Vue.js is not Easy. It is Approachable.</title>
      <link>https://wtrclred.vercel.app/ja/posts/06</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/06</guid>
      <description>言語と誤謬による抽象化</description>
      <pubDate>Wed, 24 Dec 2025 00:00:00 GMT</pubDate>
      <dc:date>2026-04-11T17:05:07.000Z</dc:date>
      <category>vue</category>
      <category>javascript</category>
      <category>language</category>
      <category>easy</category>
      <category>simple</category>
      <category>architecture</category>
      <category>tradeoff</category>
      <content:encoded><![CDATA[<h2>耳馴染み</h2>
<p>昨今, Simple/Easyというマーケティング用語がよく使われる.<br>
今回は他の具体的な言語やフレームワークについて言及するつもりはない.<br>
あくまでVue.jsとの向き合い方についての物語だ.</p>
<p>「<strong>Vue.js is Easy</strong>」</p>
<p>このフレーズを聞いたことがあるだろうか？<br>
私は何度も聞いた.<br>
そして, それと同じくらい「<strong>Vue.jsにはルールがないからダメだ</strong>」という言葉もたくさん聞いてきた.</p>
<h2>誤謬</h2>
<p>私がVue.js / Webを触り始めたのはおおよそ2020のQ4頃で, それ以前の世界がどうであったかは知らない.<br>
そして, どのタイミングからこのフレーズが流行り始めたのかも知らない.<br>
しかし, 「今」の自分の感覚として, この言葉を使うのが適切なのかどうかについてはいつも疑問を感じている.</p>
<p>「<strong>Vue.js is Easy</strong>」は私が思うに, 今も限られたコンテキストであれば正しいだろう.<br>
しかしそのコンテキストを理解している人たちはいったいどれくらいいるだろうか.</p>
<p>昨今は「ルールを持つ存在(Webフレームワークに限らず)」の流行もあり, 彼らは彼らで「〇〇 は △△ である(Easyではないけれど)」というマーケティングをよくやる.</p>
<p>そしてそれに飛来して,</p>
<p>「<strong>Vue.jsにはルールがないからダメだ</strong>」<br>
「<strong>目先の簡単さを求めて堅さを捨てている</strong>」<br>
「<strong>ルールがないことは, コードをメチャクチャにする</strong>」</p>
<p>などの言説が生まれる.</p>
<p><strong>抽象的なマーケティングは抽象的なマーケティングで打ちのめされるのだ</strong>.</p>
<h2>トレードオフ</h2>
<p>先ほども言ったように, 確かに「<strong>Vue.js is Easy</strong>」というのは一部のコンテキストで正しい.<br>
Vue.jsには明確なルールがないので, 何かルールを学んだりしなくても <strong>フレームワークの立場としては間違いではない</strong> という意味では正しい.</p>
<p>しかし実際にプロダクトを作るには, いろんなことを考える必要がある.<br>
そしてここには <strong>トレードオフ</strong> がある.</p>
<p>おそらく多くの人たちはこのトレードオフを考えずに, 「ルールはあった方が良い」という意志を先行させ, プログラミングと向き合っている.<br>
もちろん, 「ルールがあった方がいい」ということにすらトレードオフは存在するが, 今日話したいのはここではない.<br>
ある状況下で, 「ルールがあった方が良い」という結論になったことを想像しよう.</p>
<p>ここで考えるべきなのは,</p>
<ul>
<li><p>なんのためのルールなのか (そのルールがあると何が嬉しいのか)</p>
</li>
<li><p>今(未来)のチーム(自分)にとって適切なものか (括弧書きにしているのは, コンテキストによって変わるからだ.)</p>
</li>
<li><p>そのルールがどのようなトレードオフを生むのか</p>
</li>
</ul>
<p>と言ったことだ.</p>
<p>Webアプリケーションはあまりにも多種多様だ.<br>
バックエンドから得たデータにUIを実装し, 単純に表示するだけのものもあれば, <br>
複雑なDOMオペレーションと組み合わせてリッチなUIを提供するものもある.</p>
<p>そしてこれらに必要なルールは全く異なっている.<br>
<strong>設計とはかなりアドホックなもので, 銀の弾丸はない</strong> のだ.</p>
<h2>責任者</h2>
<p>ここで重要なのは「<strong>誰がそのルールの責任を持つのか</strong>」ということだ.<br>
私はWebアプリケーションの場合, 必ずしもフレームワークがルールを持つことが良いとは思わない.<br>
もちろん, 場合によってはフレームワークがルールを持つことが良いユースケースもあるだろう.</p>
<p>しかし「フレームワーク」という我々の基礎となる部分がこの責任を負うというのはやはりトレードオフを含むものだ.<br>
フレームワークがここの責任を持った場合, ほとんどの場合ではそれに準拠することが望まれる.<br>
例えそれが我々のプロダクトにとって重要じゃないことであってもだ.</p>
<p>そして同時に, それによって失われた他のトレードオフについても考えてはおくべきだ (学習コストなど).<br>
加えて「<strong>誰がそのルールの責任を持つのか</strong>」というのと, 「ルールが必要であるかどうか」というのは全く別の話であることに注意したい.</p>
<h2>言葉による抽象化</h2>
<p>ここまでの話で私が話したい本質的なことは済んだ.<br>
しかし, どうしても言い切り方の言葉遊びがしたくなるのが私達だ.</p>
<p>「<strong>Vue.js is Easy</strong>」に対してネガティブな誤謬が含まれやすく, 適切でないならばなんと言えばいいだろうか.<br>
私はVue.jsの公式ドキュメントに登場する「<strong>Approachable</strong>」という言葉が大好きだ.</p>
<p>https://vuejs.org/</p>
<p>先程も言った通り, 「Vue.js is Easy」は「フレームワークとしての責任」の範囲であれば正しいが,「Webアプリケーションの実装 (・保守運用)」の話に拡大されると間違った判断をする可能性を含んでいる.</p>
<p>何かしらの課題 (需要) があるなら規約や設計を考えるというのは当たり前のことだ.<br>
<strong>Easy</strong>という言葉は，ここをあたかも考えなくても良いものが作れるものだ，という誤謬を与えかねない.</p>
<p>Vue.jsは<strong>Approachable</strong>なのだ.<br>
何かを考える時，邪魔にならない.</p>
<p>しかし，それと引き換えに実装者は少しの責任を負う.<br>
これはトレードオフなのだ.</p>
<hr>
<p>これは私の単なるエッセイであり，Vue.js Teamの公式見解ではありません.</p>
]]></content:encoded>
    </item>
    <item>
      <title>What is Vue.js? It&apos;s just a language lol</title>
      <link>https://wtrclred.vercel.app/ja/posts/05</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/05</guid>
      <description>DSLという視点からVue.jsとプログラミング言語について考える</description>
      <pubDate>Tue, 23 Dec 2025 00:00:00 GMT</pubDate>
      <dc:date>2026-04-17T09:22:06.000Z</dc:date>
      <category>vue</category>
      <category>javascript</category>
      <category>language</category>
      <category>compiler</category>
      <category>dsl</category>
      <content:encoded><![CDATA[<h1>前提</h1>
<ul>
<li><p>ここで話すことは公式の見解ではない.</p>
</li>
<li><p>&quot;言語&quot; についての話をするが，かなり話したいことに寄せている. 現実はそんなに単純ではない.</p>
</li>
<li><p>あくまで，「こういう見方もできる」という一つの側面を示すものである.</p>
</li>
</ul>
<p>関連: <a href="https://wtrclred.vercel.app/posts/07">Characterize Vue.js</a>では，この言語としての見方を含めて，Vue.jsの性格をもう少し広く整理している．</p>
<h1>コンピュータへの命令</h1>
<p>Webアプリケーションの実装も，ネイティブアプリケーションの実装も，ブラウザの実装も，全てはコンピュータへの命令である.
そして，それらは全てバイナリで表現できる.</p>
<p>しかし，人間がバイナリを直接書くことは現実的ではない.
これは，日本語話者に対してモールス信号で会話を強いるようなものである.
意味を伝達する手段としては成立するが，実用的ではない.</p>
<h1>プログラミング言語という抽象</h1>
<p>プログラミング言語は大きく2つに分類される.</p>
<p><strong>汎用プログラミング言語</strong> (general-purpose programming language)は，特定の領域に限定されない汎用的な計算を記述するための言語である.
Rust, TypeScript, JavaScript, Python, C, Haskellなどがこれに該当する.</p>
<p>一方，<strong>ドメイン固有言語</strong> (DSL: domain-specific language)は，特定の問題領域に特化した言語である.
HTML, CSS, SQL, JSONなどがこれに当たる. そして，Vueもここに位置づけることができる.</p>
<h1>抽象の階層</h1>
<h2>バイナリとISA</h2>
<p>ISA (Instruction Set Architecture)はCPUの命令セットを定義するものである.
x86, ARM, RISC-Vなどがこれに該当する.</p>
<pre><code class="language-asm">ff 43 00 d1
ff 0f 00 b9
48 05 80 52
...
</code></pre>
<p>レジスタへの書き込み，算術演算，条件分岐，ジャンプ. これらの原始的な命令の組み合わせで，あらゆる計算が表現される.
if文もwhileループも関数呼び出しも，最終的にはこれらに帰着する.</p>
<h2>アセンブリ</h2>
<p>バイナリを人間が読める形式に変換したものがアセンブリである.</p>
<pre><code class="language-asm">my_program:
  sub   sp, sp, #16
  str   wzr, [sp, #12]
  mov   w8, #42
  str   w8, [sp, #8]
  ldr   w8, [sp, #8]
  mov   w10, #2
...
</code></pre>
<p><code>sub</code>は減算，<code>mov</code>は値の転送，<code>jmp</code>はジャンプ.
ニーモニックによって，バイナリ列に意味が与えられた.</p>
<p>しかし，これで十分だろうか.</p>
<p>変数に意味のある名前をつけたい. 条件分岐を構造化したい. 繰り返しを簡潔に記述したい. 関数という単位で処理をまとめたい.
アセンブリは計算機にとっては十分でも，人間の認知にとっては依然として負荷が高い.</p>
<h2>高級言語への進化</h2>
<pre><code class="language-c">int main() {
  int a = 42;
  if (a % 2 == 0) {
    a = 5;
  }
  a = a + 999;
  return a;
}
</code></pre>
<p>C言語(Fortran)の登場により，人間の思考に近い形式でプログラムを記述できるようになった.
変数，条件分岐，ループ，関数といった抽象が導入され，開発者の認知負荷は大幅に軽減された.</p>
<p>しかし，これで終わりではなかった.</p>
<p>メモリ管理の自動化，型システムの強化，オブジェクト指向，関数型プログラミング，並行処理の抽象化...
新たな要求が生まれるたびに，新たな言語が生まれてきた.</p>
<p>言語とは，ある種の「翻訳機」である. 人間の意図をコンピュータが理解できる形式に変換する.
そして，その翻訳の過程で，何を簡潔に書けるようにし，何を隠蔽するかが，言語の設計思想を決定づける.</p>
<h1>言語設計のモチベーション</h1>
<p>言語が生まれる背景には，常に開発者体験(DX)の向上がある.</p>
<ul>
<li><p><strong>構文の意味づけ</strong>: if, while, モジュール, 名前空間など，思考の単位に対応する構文を提供する</p>
</li>
<li><p><strong>対象領域の意味づけ</strong>: OOPならJavaやC++，システムプログラミングならRustやGo，UI記述ならHTML/CSS</p>
</li>
<li><p><strong>コンパイラによる最適化</strong>: 開発者は意図を記述し，最適化はコンパイラに委ねる</p>
</li>
</ul>
<p>言語とは，特定の問題領域において「何を書きやすくするか」という設計判断の集積である.</p>
<h1>Vue.jsは言語である</h1>
<p>ここで本題に入る.</p>
<p>Vue.jsとは何か. フレームワーク，ライブラリ，様々な呼び方があるが，ここでは一つの見方を提示したい.</p>
<p><strong>Vue.jsは，Web UIを記述するためのドメイン固有言語である.</strong></p>
<p>UIを宣言的に記述するために必要なものを考えてみる.</p>
<ul>
<li><p>Web標準であるHTML, CSS, JavaScriptとの親和性</p>
</li>
<li><p>データバインディングによる動的なViewの記述</p>
</li>
<li><p>コンポーネントを単位とした設計</p>
</li>
<li><p>スコープ付きスタイル</p>
</li>
<li><p>スロットによるコンテンツの受け渡し</p>
</li>
</ul>
<pre><code class="language-vue">&lt;script setup lang=&quot;ts&quot;&gt;
import { ref } from &quot;vue&quot;;
const count = ref(0);
function increment() {
  count.value++;
}
&lt;/script&gt;

&lt;template&gt;
  &lt;button type=&quot;button&quot; @click=&quot;increment&quot;&gt;count is: {{ count }}&lt;/button&gt;
&lt;/template&gt;

&lt;style scoped&gt;
button {
  color: red;
}
&lt;/style&gt;
</code></pre>
<p>このSFC (Single File Component)という形式は，HTML/CSS/JavaScriptの構文を借りながら，UI記述に特化した意味論を与えている.</p>
<h1>実用性と言語設計</h1>
<p>完全に新しい言語を設計することも可能である. しかし，それは現実的だろうか.</p>
<p>新しい言語を作るということは，エコシステムをゼロから構築することを意味する.
エディタサポート，型チェッカー，リンター，フォーマッター，ビルドツール... これらを全て新規に開発する必要がある.</p>
<p>Vue.jsはインクリメンタルなアプローチを取った.</p>
<p><strong>既存の資産を活用する.</strong> JavaScript/TypeScriptのエコシステム， による恩恵をある程度そのまま享受する.</p>
<p><strong>メンタルモデルを維持する.</strong> script部分はJavaScriptのセマンティクスをそのまま使う. HTMLとCSSの書き心地を損なわない.</p>
<p>これは，新しい言語を「方言」として設計するようなものである.
母語の文法を大きく逸脱せず，しかし特定の表現については独自の語彙と構文を導入する.</p>
<h1>コンパイラによる最適化</h1>
<p>言語がコンパイラを持つことで，最適化の恩恵を受けることができる.</p>
<p>開発者は意図を記述し，コンパイラが効率的なコードを生成する.
この分業により，開発者は本質的な問題に集中でき，低レベルの最適化から解放される.</p>
<p>Vue.jsのコンパイラには以下の最適化が実装されている.</p>
<ul>
<li><p><strong>Static Hoisting</strong>: 静的なノードをレンダリング関数の外に巻き上げ，再生成を回避する</p>
</li>
<li><p><strong>Patch Flags</strong>: 動的な部分にフラグを付与し，差分検出を高速化する</p>
</li>
<li><p><strong>Tree Flattening</strong>: ネストした静的ノードをフラット化し，走査コストを削減する</p>
</li>
<li><p><strong>Vapor Mode</strong>: 仮想DOMを介さない，より直接的なレンダリングモード</p>
</li>
</ul>
<p>これらの詳細は <a href="https://ja.vuejs.org/guide/extras/rendering-mechanism#compiler-informed-virtual-dom" target="_blank" rel="noopener noreferrer">公式ドキュメント</a>に記載されている.</p>
<p>SFC Playgroundで出力コードを眺めてみると，コンパイラがどのような変換を行っているかを観察できる.</p>
<h1>課題</h1>
<p>言語を設計し維持することには，相応のコストが伴う.</p>
<ul>
<li><p>言語仕様の設計: 一貫性と表現力のバランス</p>
</li>
<li><p>コンパイラの実装: 正確性と性能の両立</p>
</li>
<li><p>周辺ツールの整備: 言語サーバー，他のエコシステムとの統合</p>
</li>
</ul>
<p>Vue.jsはすでに多くの部分が成熟しているが，探せばまだやるべきことは見つかる.
貢献の機会は常に存在する.</p>
<h1>結び</h1>
<p>バイナリからアセンブリへ，アセンブリから高級言語へ.
プログラミングの歴史は，抽象化の積み重ねの歴史である.</p>
<p>Vue.jsもその系譜に位置づけることができる.
Web UIという問題領域に対して，HTML/CSS/JavaScriptを基盤としながら，宣言的UI記述のための意味論を与えた言語.</p>
<p>Vue.jsとは何か.</p>
<p>言語である.</p>
]]></content:encoded>
    </item>
    <item>
      <title>なぜ私がスポンサーを 募り始めたか</title>
      <link>https://wtrclred.vercel.app/ja/posts/04</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/04</guid>
      <description>しばらく公にスポンサーを募らなかった理由と， その考え方の変化について．</description>
      <pubDate>Sat, 14 Sep 2024 00:00:00 GMT</pubDate>
      <dc:date>2026-04-17T08:53:41.000Z</dc:date>
      <category>oss</category>
      <content:encoded><![CDATA[<h1>なぜ私がスポンサーを募り始めたか</h1>
<h2>稚拙さ</h2>
<p>まずはこちらの発言を見てほしい．</p>
<p>https://x.com/ubugeeei/status/1745815915353325898</p>
<p>実際のところ，長い間本当にこのように考えていました．<br>
なぜなら自分に対する寄付をするよりも，コミュニティに対する行動をして欲しかったからです．<br>
そのために行動を起こそうとしていた人を全力でサポートし，提案し，どうにか協力者を増やそうとしていました．</p>
<p>しかしこれにはいくつかの稚拙な深層心理と，コミュニティへの悪影響があることに気づきました．</p>
<h2>責任逃れ</h2>
<p>お金を受け取ってしまうと「やらなくてはならない」という責任感が多かれ少なかれ生まれます．<br>
私はこれに限らず余計な圧力というのは徹底的に排除したいタイプでしたが，時にはその強制力を原動力に変換する力も身につけるべきです．<br>
責任から逃げることもそうですが，変化に対する挑戦も怠っていました．<br>
これは稚拙な態度だったと反省しています．</p>
<h2>安売り</h2>
<p>私は私自身に対していつも悲観的です．<br>
これは良い側面もありますが，自分以外への影響を考えると後述の文化への悪影響にもつながることに気付かされました．<br>
「自分なんかが活動に対する対価を受け取るべきではない」「そんなことよりももっと活動しないと」「とにかく，下手 (したて) に，下手に」「傲慢であってはならない」<br>
そんなふうに考えていました．</p>
<h2>文化への悪影響</h2>
<p>ここからが本題です．<br>
問題が自分自身に対して閉じているならわざわざブログにするほどでもないのですが，この気づきはコミュニティや文化を発展させる上で関係者が全員把握するべき重要なものだと思ったので書きました．<br>
(ぜひ多くの人に届いてほしい)</p>
<p>簡潔に言うと，上記のような態度は文化を停滞させる方向に加担します．<br>
私の理想の世界では，活動をした者は全員それに見合った対価を受け取るべきですし，またそれを原動力に変換して次の活動につなげるべきです．</p>
<p>昨今，OSSやメンテナの努力に対する報酬の希薄さについてしばしば話題にあがります．<br>
OSSコミュニティはもっと経済が循環するべきだと強く思っています．</p>
<p>それなのに，自分が受け取るのをわざわざ断ったり，スポンサーを開けずに受け口を持たないことはこれに矛盾しています．<br>
自分が活動すればするほど，経済循環のハードルを上げてしまい，文化としては良い状態にならないと思いました．</p>
<p>実際のところ，世界で活躍するコントリビューターの方々に比べると自分の活動はまだまだですし，私1人がスポンサーを開けたり開けなったりしてもそれほどの影響力はありませんが，それであってもまだまだなりに少々の対価を受け取ったり，積極的に募ったりして文化貢献をしていくべきだと思いました．<br>
(小さく，自分の周りからでもそういう文化を育てていきたい)</p>
<p>額の大小はあるにしろ，活動に対する対価をみんなで支払っていくという文化はもっと前進するべきです．<br>
受け取りを拒否すればするほど，受け取りや支払いのハードルは上がりますし，文化としても「まぁ，そういうもんか」という認知が広まってしまいますし，何より「オープンソースは無料で使えるものである」というよくある誤解を助長してしまいます．<br>
これはあってはならないことだと思いました．</p>
<h2>おまけ話</h2>
<p>私がこのことについて考え直すきっかけを与えてくれたのは <a href="https://github.com/ota-meshi" target="_blank" rel="noopener noreferrer">@ota-meshi</a>さん(Vue.js Core Team Member)でした．<br>
Vue.jsの海外メンバーが日本に来るというのでオフ会をした時にotaさんと同席し，帰りの電車でこの話をしました．<br>
自分の従来の考えを伝えると，「それは間違っているよ」と言われてしまいました．<br>
考え直すと本当にその通りだと思いました．</p>
<p>また，「それはただの責任逃れなんじゃないか？」「あなたは自分を安売りしすぎじゃないか？」と言ってくれたのも別の知人でした．<br>
それもその通りだと思いました．</p>
<p>私は自分1人で考えているとどうしても自分の事ばかり，しかも悲観的になりすぎでしまう癖が強くあるのですが，自分よりも先をいく先輩の話や，客観的に自分を評価をできる人，自分とは違う行動を取っている人と話すことの重要性を再認識しました．</p>
<p>きっかけをくれた全ての方々に感謝します．</p>
<hr>
<p>GitHubスポンサーの募集を始めました．ぜひご支援よろしくお願いします！</p>
<p>https://github.com/sponsors/ubugeeei</p>
]]></content:encoded>
    </item>
    <item>
      <title>Instability / Propulsion</title>
      <link>https://wtrclred.vercel.app/ja/posts/03</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/03</guid>
      <description>Embrace your own instability.</description>
      <pubDate>Sat, 13 Jul 2024 00:00:00 GMT</pubDate>
      <dc:date>2026-04-11T12:02:04.000Z</dc:date>
      <category>philosophy</category>
      <content:encoded><![CDATA[<h1>不安定性 / 推進力</h1>
<p>私を含め，人々はしばしば持続的な安定を求める．<br>
仕事で良いパフォーマンスを維持し，個人的な趣味や関心事で一貫した進歩を遂げたいと思う．</p>
<p>しかし，最近，それが本当に目指すべき理想の状態なのかどうか，よく考えている．<br>
不安定さは常に悪いことなのだろうか？</p>
<p>私自身，調子の良し悪しがあり，良い状態でないときはいつもフラストレーションを感じる．<br>
振り返ってみると，「ああ，あの時もっと良い状態だったら，もっとうまくいっていたのに」と思うことがある．</p>
<p>しかし，それは間違いだ．</p>
<p>悪い状態にあることは，原動力になりうる．<br>
常にフラットな状態でいるよりも，高い速度の瞬間がある方が良い．</p>
<p>不安定性と推進力は表裏一体だ．</p>
<p>私はジャズが好きだ．ジャズには，ドミナントやスウィングといった概念がある．<br>
端的に言えば，ドミナントは不安定な音程を作り出し，スウィングは不安定なリズムを作り出す．</p>
<p>この単調でない不安定さが推進力を生み出し，強いグルーヴを作り出す．<br>
そしてそれがジャズの魅力だ．</p>
<p>その魅力を理解し，受け入れよう．</p>
]]></content:encoded>
    </item>
    <item>
      <title>Activist as an Information Compiler</title>
      <link>https://wtrclred.vercel.app/ja/posts/02</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/02</guid>
      <description>より一般化されたアクセシビリティの形</description>
      <pubDate>Sun, 16 Jun 2024 00:00:00 GMT</pubDate>
      <dc:date>2026-04-11T15:47:20.000Z</dc:date>
      <category>thinking</category>
      <category>concept</category>
      <category>accessibility</category>
      <category>compiler</category>
      <category>philosophy</category>
      <content:encoded><![CDATA[<h1>情報コンパイラとしてのアクティビスト</h1>
<p>より一般化されたアクセシビリティの形</p>
<h2>定義</h2>
<p>アクティビストとは？</p>
<p>ここで定義をまとめておこう．<br>
ここでいう「アクティビスト」とは，活動に積極的に関わっているすべての人を指す．（まあ...これでは何も説明していないが...）</p>
<p>会社で働いている人，何かのコミュニティで活動している人，独立したOSS開発者など...<br>
活動の形は様々だが，すべてに共通していることがある．</p>
<h2>情報の形態</h2>
<p>情報には様々な形態がある．テキストデータ，音声データ，そしてもちろん，あなたが考えていること．<br>
場合によっては，「あなたが考えていないこと」も情報になりうる．</p>
<p>これらには優先度の違いがあり，状況によって扱い方が異なる．</p>
<h2>動的情報と静的情報</h2>
<p>情報は動的と静的に分類できる．</p>
<p>動的:</p>
<ul>
<li><p>会話</p>
</li>
<li><p>考えていること</p>
</li>
<li><p>など...</p>
</li>
</ul>
<p>静的:</p>
<ul>
<li><p>テキストデータ</p>
</li>
<li><p>音声データ</p>
</li>
<li><p>など...</p>
</li>
</ul>
<p>この分類方法はかなり主観的だが，大まかに言えば「改ざんへの備え」と「改ざんの追跡可能性」の違いである．</p>
<p>そして私は，アクティビストの役割の一つは「活動に関連する動的情報を静的情報にコンパイルすること」だと強く信じている．<br>
なぜなら，静的情報には多くの価値があるからだ．</p>
<h2>静的情報の価値</h2>
<h3>アクセシビリティ</h3>
<p>まず，アクセシビリティについて考えてみよう．<br>
ここでいうアクセシビリティは，ウェブアクセシビリティの文脈とは異なる．<br>
ウェブコンテンツの範囲を超えた，より一般化された概念である．</p>
<p>アクティビストとして，情報へのアクセスにはいくつかの制約がある:</p>
<ul>
<li><p>時間的制約</p>
</li>
<li><p>処理能力の制約</p>
</li>
<li><p>記憶の制約</p>
</li>
</ul>
<p>まず，時間について．動的情報は時間を超えて他者と共有することができない．<br>
誰もが同時に存在し，話しているわけではない．<br>
ミクロな文脈では「会議」が良い例だ．もちろん，スケジュールが合わないために参加できない人もいる．<br>
マクロな文脈では，「世代交代（引き継ぎ）」や「新規参加者のキャッチアップ」が良い例だ．過去の出来事や歴史を知るために，その時の考え方や行われたことが静的にまとめられていることは価値がある．</p>
<p>次に処理能力について．人間の処理能力は人それぞれ異なる．<br>
物事を素早く理解する人もいれば，そうでない人もいる．<br>
同じ場所に同時にいて考えを共有していても，その情報を処理する能力は様々だ．<br>
これを補完する静的情報は非常に価値がある．</p>
<p>最後に記憶について．人間の記憶には限界がある．<br>
忘れることは普通のことであり，どこかで情報が変質することも普通のことだ．<br>
さらに，永続的で改ざんに強い静的情報は，記憶の補完として非常に価値がある．</p>
<h3>機械可読性</h3>
<p>この言葉もウェブコンテンツの文脈で登場するが，この話にも類似点がある．<br>
動的情報は機械が読み取るのが難しい．</p>
<p>一方，静的情報は機械が読み取りやすい．<br>
あえて最近のホットな話題と混ぜるなら，静的情報はLLMに直接入力できる．</p>
<h2>課題</h2>
<p>静的情報には様々な価値があり，アクティビストにはその価値を生み出す役割があることを理解した．</p>
<p>しかし，問題は「動的情報を静的情報に変換する能力は人それぞれ異なり，向上させるには訓練が必要である」ということだ．
この能力は，いわゆる「要約力」や「他者と効果的にコミュニケーションする能力」とは異なる．</p>
<p>「コンパイラ」は入力と出力の間で意味を変えてはならない．
つまり，できるだけ情報を失わずに動的情報を静的情報に変換する必要がある．<br>
その時の思考，感情，雰囲気，流れなど．</p>
<p>これらを過不足なく静的情報に変換するのは難しい．<br>
しかし，この能力は私たちアクティビストにとって必要なものだ．訓練しよう．</p>
]]></content:encoded>
    </item>
    <item>
      <title>React is React, just.</title>
      <link>https://wtrclred.vercel.app/ja/posts/01</link>
      <guid isPermaLink="true">https://wtrclred.vercel.app/ja/posts/01</guid>
      <description>エッセイ: React with React Compilerは&quot;Just JavaScript&quot;であるか&quot;</description>
      <pubDate>Sat, 17 Feb 2024 00:00:00 GMT</pubDate>
      <dc:date>2026-04-17T09:22:06.000Z</dc:date>
      <category>react</category>
      <category>javascript</category>
      <category>oss</category>
      <category>semantics</category>
      <category>compiler</category>
      <category>react-compiler</category>
      <category>language</category>
      <content:encoded><![CDATA[<p>Twitterで散らかしてしまったので軽くまとめておく.</p>
<p>続き: <a href="https://wtrclred.vercel.app/posts/08">React is React, just. Part 2</a>では，同じ流れでMeta，Babel，Flow，React Compilerについて考えている．</p>
<h1>序・前提</h1>
<p>まず前提として，コンパイラを使った最適化を行うという方針については私はとても<strong>賛成</strong>である.
インターフェースを崩さずにDXを改善するにあたってこのアプローチはしばしば有効的であるし，私自身もそれを全面に押し出すフレームワークを使っている.
ただし，コンパイラが介入するにあたってのメンタルモデルの変更(または統一 [^1])についていくつかの疑問がある.
私は普段からReactを書いているわけでもなく専門家でもないので，これから話すことはReactに対する意見というより，「Reactを使っている人は，この点どう感じているのだろうか？」という好奇心からくるもので，その是非に対するものではない.
何度も言うが私は <strong>賛成</strong> している.</p>
<p>また，これらはhttps://react.dev/blogでReact Compilerについての話が出た時に感じたことで，これまでReact Compilerについて行われてきた議論などをしっかり調べたりして根拠を作ったものではない. ただのエッセイだ.</p>
<p>私が持っている疑問に対するアンサーは歓迎しているが，このエッセイを根拠にしたReact CompilerやReact自体の是非については歓迎しない.
技術に対する是非はこの一部を切り取って評価できるものではない.
(と思っているので，もしこのエッセイにそう捉えられかねない発言があれば，ぜひ指摘してほしい．(是非について何か広めたいわけではない))</p>
<p>私の立場: 感想・疑問をまとめておく
読み手の立場: 感想・疑問について議論・返答する
やらないこと: 技術に対する是非</p>
<p>なぜわざわざここまで保険をかけたかというと，自分が叩かれたくなかった，とかそういう意味ではなく，
パラダイムに関する是非は視点によって大きく変わるものであり，それについて話すには議論が足りてなさすぎるからだ.
技術の是非に関して，誤謬が広まってしまうことは本当に良くないことだと思っているので，それは避けられたい.</p>
<h1>トピック</h1>
<p>まずここからすでに主観の話になるが，Reactは「純粋なJavaScript」であるという主張が多かれ少なかれあるはずだ.
ここで，「純粋」とは何かについては多義であると感じているので，注意が必要だと思っている．</p>
<p>純粋とは:
(ⅰ) React Componentは冪等なJavaScriptである
(ⅱ) React ComponentはJust JavaScriptである</p>
<p>今回，特に私が気になっていたのが，「React Compilerの登場で (ⅱ) を満たせなくなってしまったみたいだが，どう感じているのか？」という点だ.
前提として，React Compiler登場以前はどちらも満たせていたという主張のもとである.</p>
<p>(ⅰ) に関しては，React Compilerはむしろ「コンポーネントは冪等であるべき」というルールの元の最適化実装なのでReact Compiler登場後も当然そう言える．この，(ⅰ) で考えている意味論のことを「React的意味論」と呼ぶようにする.</p>
<p>問題は (ⅱ) だ．React CompilerはReact的意味論は変化させないが，「JS的意味論は変化させている」という主張だ.
ここについて，Reactを使っているの人たちがどう感じているのかについて興味がある．というのが今回のトピックだ.</p>
<hr>
<h1>「JS的意味論は変化させている」というのがどういう意味なのかについて.</h1>
<p>以下のようなコンポーネントを考えてみよう．</p>
<p>これは実際<a href="https://youtu.be/qOQClO3g8-Y?t=419" target="_blank" rel="noopener noreferrer">React Advanced 2023のReact Fogetについての登壇</a>で登場したコードだ．(一部省略)</p>
<pre><code class="language-tsx">function VideoTag({ heading, video, filter }) {
  const filteredVideos = [];
  for (const video of videos) {
    if (applyFilter(video, filter)) {
      filteredVideos.push(video);
    }
  }

  if (filteredVideos.length === 0) {
    return &lt;NoVideos /&gt;;
  }

  return (
    &lt;&gt;
      &lt;Heading
        heading={heading}
        count={filteredVideos.length}
      /&gt;
      &lt;VideoList videos={filterdVideos}&gt;
    &lt;/&gt;
  )
}
</code></pre>
<p>このようなコードを見た時，Reactユーザーならば不要な計算に気づき，最適化を行うことだろう.</p>
<pre><code class="language-tsx">function VideoTag({ heading, video, filter }) {
  const filteredVideos = useMemo(() =&gt; {
    const filteredVideos = [];
    for (const video of videos) {
      if (applyFilter(video, filter)) {
        filteredVideos.push(video);
      }
    }
    return filteredVideos;
  }, [videos, filters])


  if (filteredVideos.length === 0) {
    return &lt;NoVideos /&gt;;
  }

  return ...;
}
</code></pre>
<p>参考: <a href="https://youtu.be/qOQClO3g8-Y?t=196" target="_blank" rel="noopener noreferrer">Understanding Idiomatic React – Joe Savona, Mofei Zhang, React Advanced 2023 t=196</a></p>
<p>しかし，React Compilerの登場によってこれが大きく変わりそうだ.
React Compilerは，このコード(useMemoを使っていない方)をコンパイラが解析し，メモ化を行う<strong>JavaScriptコード</strong> に変換する.</p>
<pre><code class="language-tsx">function VideoTab(t36) {
  const $ = useMemoCache(12);
  const { heading, videos, filter } = t36;
  let filteredVideos;

  if ($[0] !== videos || $[1] !== filter) {
    filteredVideos = [];

    for (const video of videos) {
      if (applyFilter(video, filter)) {
        filteredVideos.push(video);
      }
    }
    $[0] = videos;
    $[1] = filter;
    $[2] = filteredVideos;

  } else {
    filteredVideos = $[2];
  }

  if (filteredVideos.length === 0) {
    return &lt;NoVideos /&gt;;
  }

  return ...;
}
</code></pre>
<p>参考: <a href="https://youtu.be/qOQClO3g8-Y?t=497" target="_blank" rel="noopener noreferrer">Understanding Idiomatic React – Joe Savona, Mofei Zhang, React Advanced 2023 (t=497)</a></p>
<p>これはつまり，「React的意味論におけるJavaScriptコード(出力コード)の最適化」であり，「JavaScriptの意味論」自体は変化している．</p>
<p>JavaScriptの関数というものは，</p>
<pre><code class="language-js">function Func(n) {
  let list = [];
  for (let i = 0; i &lt; n; i++) {
    list.push(i);
  }
  return list;
}
</code></pre>
<p>とかくと，その結果に関わらず必ず実行されるものだ.</p>
<p>だが，React Compiler登場後のReact Componentはそうではない.
<strong>見かけ上はJS関数だが，Just JS関数ではなくなってしまった</strong> と言えそうだ.</p>
<pre><code class="language-tsx">function VideoTag({ heading, video, filter }) {
  const filteredVideos = [];
  for (const video of videos) {
    if (applyFilter(video, filter)) {
      filteredVideos.push(video);
    }
  }

 ...
}
</code></pre>
<p>と記述したはずなのに，関数の呼び出し回数と実際に記述した処理の回数は一致しない.
このような評価規則はJavaScriptにはないもので，React CompilerがReact的意味論を元に最適化した結果だ.</p>
<p>これで，「React Compilerの登場によって，React ComponentがJust JavaScriptではなくなってしまう」というところが伝わっただろうか.</p>
<hr>
<h1>意味論は2つに増えたが，構文論は1つである.</h1>
<p>実際に，ReactでComponentを実装する際に「React的意味論」と「JS的意味論」の２つを併用するべきかという議論はさておき，</p>
<pre><code class="language-tsx">function VideoTag({ heading, video, filter }) {
  const filteredVideos = [];
  for (const video of videos) {
    if (applyFilter(video, filter)) {
      filteredVideos.push(video);
    }
  }

 ...
}
</code></pre>
<p>この関数を見たときに， 2つの意味論が存在してしまうことは事実だ. (React的意味論と，JS的意味論)
あるときは，関数はの呼び出し回数とbodyの実行回数は一致するかもしれないし，あるときはしないかもしれない.
これが開発者や学習者にとって負担になるかどうかは分からないが，そういった可能性はゼロではないはずだ.</p>
<h3>構文論に関する利点について，注意したいこと</h3>
<p>今までは主に，</p>
<pre><code class="language-tsx">function VideoTag({ heading, video, filter }) {
  const filteredVideos = [];
  for (const video of videos) {
    if (applyFilter(video, filter)) {
      filteredVideos.push(video);
    }
  }

 ...
}
</code></pre>
<p>という関数に対する意味論について取り上げてきたが，構文論についてだ.
構文論については，React Compilerの登場以前/後に限らず紛れもなく「Just JavaScript」だ.
(JSXはそもそもJSではないだろという話もあるが，今回の話の本筋ではないので割愛する)</p>
<p>ReactがJust JavaScriptであるという利点の一つとして，静的解析の容易さやツールチェーンとのインテグレーションのしやすがしばしば挙げられる.
これらに関しては引き続きそう語って概ね良い範囲だと個人的には思われる. (だからと言って，他のものがダメであるかどうかはまた別の議論)
厳密にはJS的意味論が重要なツールなどでは部分的にそうではなくなる可能性もあるが，感覚的にはそこが重要になってしまうようなものはほとんどない気がしている (完全に主観).</p>
<p>しかし，注意したいのはその切り分けである．
React Compiler登場以前，「ReactはJust JavaScriptだから ${任意の評価}」は意味論と構文論のどちらにも適用されることがあった．しかしReact Compilerの登場後はそれは構文論の話になったのではないかと思う.</p>
<p>「ReactはJust JavaScriptだから ${任意の評価}」と言われた時，この区別がないと間違った評価をしてしまうことがありそうだ，という考えが私にはある．これがどの程度問題になるかは定かではいが，新たに生まれた問題提起なのではないかな，と思った．</p>
<p>[^1]: もちろん，中にはもともとReact的意味論のみを考えていた方もいると思う．その方にとっては特に変わりがないが，JS的意味論も視野に入れていた人はReact的意味論のみを考えるように統一されていくものなのかな，と思った (実際どうなのかはわからない)</p>
<h1>つまりReact Componentは何であると言えそうか</h1>
<p>JavaScriptの意味論を変えてしまっては，「Just JavaScript」とは言えなさそうだ．
ではなんなのだろうか？
・
・
・
・
Reactは，Reactだ.
構文 がJavaScriptと同じで，意味が異なる別の存在だ.</p>
<p>と思ったが皆さんはどうだろうか... 🤔</p>
<blockquote>
<p>エッセイ: ReactはJavaScript + Ruleなのか，それとも言語なのか</p>
</blockquote>
<p>https://x.com/ubugeeei/status/1758298301609492584?s=20</p>
<hr>
<p>&lt;details&gt;
&lt;summary&gt;ref:&lt;/summary&gt;</p>
<p>https://x.com/ubugeeei/status/1758296154406821942?s=20</p>
<p>https://x.com/ubugeeei/status/1758308229602529304?s=20</p>
<p>https://x.com/ubugeeei/status/1758309937300808122?s=20</p>
<p>https://x.com/ubugeeei/status/1758458714749911445?s=20</p>
<p>https://x.com/ubugeeei/status/1758467330278035779?s=20</p>
<p>https://x.com/ubugeeei/status/1758508131385266543?s=20</p>
<p>https://x.com/ubugeeei/status/1758521001091191294?s=20</p>
<p>&lt;/details&gt;</p>
<hr>
]]></content:encoded>
    </item>
  </channel>
</rss>