10月6日、Dave Rupertが「Recursion is DEAD and HTML killed it」と題した記事を公開した。
Redditのようなネスト構造スレッドをサーバーサイドでレンダリングするとき、エンジニアは長年「全データを取得してツリーを組み立て、そこからHTMLを生成する」という直列処理から逃れられなかった。Chromiumで試験実装が進む Declarative Partial Updates(DPU) の <?marker> という新構文は、そのボトルネックを根本から取り除く可能性がある。Dave Rupertが突きつける問いは単純だ——「HTMLが自分でコンテンツを正しい位置に差し込めるなら、サーバーはツリーを組み立てなくていいのではないか?」
なぜ今この仕様が注目されているか
Declarative Partial Updates(DPU) は、Chromiumで現在 Origin Trial(本番前の限定的な機能試験フェーズ)が行われているWeb Platform仕様だ。LLMチャットボットの普及によって「複数のウィジェットや本文を非同期・逐次的にページへ流し込む」という要件が急増したことが、この仕様への関心を高めている背景にある。生成速度の異なる複数のコンポーネントを、Reactのような重いフレームワークを使わずサーバーサイドだけでストリーミングできる——そのための仕組みとしてDPUが設計されている。
<?marker>とは何か
DPUが導入する <?marker> を使うと、最初に送ったHTMLの中にプレースホルダーを置いておき、後から届く <template for=""> の内容がそこに自動的に差し込まれる。
<main id="main">
<?marker name="post-abc123" ?>
</main>
<!-- ...後続のHTMLがここに続く... -->
<template for="post-abc123">
<div class="post">
<h2>Hello world</h2>
<div>First post</div>
</div>
</template>
<slot> 要素に似ているが、**<?marker> は一度使ったら使い捨て**だ。ただし挿入するコンテンツの末尾に同名の <?marker> を再度書くことで、チェーン状に続けることができる。
また、DPUは appendHTML() という新しいAPIも導入する。従来の innerHTML は文字列をそのままDOMに展開するためXSSリスクがあったが、appendHTML() はより安全なHTML挿入手段として設計されている点も注目に値する。
この機能の本質は「サーバーとの接続を閉じずに、HTMLを流し込み続けられること」にある。
「再帰はもう要らない」— ネストされたスレッドで何が変わるか
記事の最も面白いポイントはここだ。Redditのようなネスト構造スレッドをサーバーサイドでレンダリングする場合、従来のアプローチには構造的なボトルネックがある。
まず発想の素直な実装——階層ごとにループをべた書きするアプローチ:
// 3階層分のネストをべた書きするアプローチ
postsTree.map(post =>
html`<div class="post">
${post.replies?.map(reply =>
html`<div class="post">
${reply.replies?.map(nestedReply =>
html`<div class="post">...</div>`
).join('')}
</div>`
).join('')}
</div>`
)
スマートな解として再帰関数を使う実装もある:
const renderPost = (post) => html`
<div id="post-${post.id}" class="post">
<div>${post.username}</div>
<div>${post.body}</div>
${post.replies?.map(reply => renderPost(reply)).join('')}
</div>
`;
postsTree.map(post => renderPost(post)).join('')
コードはすっきりするが、どちらも同じ問題を抱えている。HTMLを生成するには、先にデータのツリーを全部組み立てなければならない。「全データ取得 → ツリー構築 → HTML生成」という直列処理が完了するまで、ユーザーは待たされ続ける。
<?marker>によるツリー構築ゼロのレンダリング
<?marker> を使うと、処理フローが根本から変わる。
従来: 全投稿取得 → ループ → ツリー構築 → ツリー走査 → HTML送信(一括)
<?marker>: 全投稿取得 → ループ → HTML送信(各投稿を取得した瞬間に逐次送信)
両者の本質的な違いは「送信タイミング」だ。従来はツリーが完成するまで一切送れなかったが、<?marker> を使えば各投稿のHTMLを生成した瞬間にクライアントへ流し出せる。ページ側のHTMLが自分でコンテンツをスナップフィットさせていくため、サーバー側でのツリー構築が一切不要になる。
コードはこうなる:
const posts = postsDb.find(p => p.thread_id === 'abc123');
const renderPost = (post) => html`
<template for="post-${post.parent_id ?? post.thread_id}">
<div id="post-${post.id}" class="post">
<div>${post.username}</div>
<div>${post.body}</div>
<?marker name="post-${post.id}">
</div>
<?marker name="post-${post.parent_id ?? post.thread_id}">
</template>
`;
posts.forEach(post => main.appendHTML(renderPost(post)));
再帰も、事前の組み立ても不要だ。さらに「もっと読む」ボタンで追加コメントを読み込む場合も、loadMoreComments() 関数が「どの位置にコメントを追加するか」を知る必要がなくなる。ページの末尾に追記するだけで、HTMLが自動的に正しい位置へ差し込む。
ただし、1万件の投稿を一気に流すとブラウザへの負荷が大きいため、分割読み込みそのものは依然として必要だ。
現時点での注意点
Dave Rupert自身も認めているように、<?marker> を使うと FOUC(Flash of Unstyled Content) やLCP(Largest Contentful Paint)の劣化といった問題が生じる可能性がある。この点の対策については「別の記事で」と言及するにとどめている。
現時点ではChromiumのOrigin Trial段階であり、本番環境での使用を前提とした機能ではない。SafariやFirefoxを含むクロスブラウザ対応には今後の標準化プロセスを待つ必要がある。とはいえ、「サーバーサイドのデータ構造とHTMLの構造を切り離す」というこのアイデアが実現すれば、ストリーミングレンダリングの設計は大きく変わりうる。
詳細はRecursion is DEAD and HTML killed itを参照していただきたい。