Svelteを深く理解する(1)— Svelteのコンパイルプロセス
Svelteを深く理解する(1)— Svelteのコンパイルプロセス
本記事を読む前に、読者がSvelteやすでに他のフロントエンドフレームワークの使用経験があり、その実装の仕組みに興味を持っていることを前提としている。
まだ「Svelteはどうコンパイルされるのか(0)— 抽象構文木とは何か?」を読んでいない場合は、先にそちらを読んでから本記事を読むことをおすすめする。
今日の記事では、いくつかの疑問に答えたい:
- なぜSvelteはコードをJavaScriptへとコンパイルできるのか
- なぜSvelteの中ではテンプレートエンジンのような構文(
{#if}、{#await}など)が使えるのか、一般的なテンプレートエンジンの構文と何が違うのか
はじめに
最終的なコードを生成するために、Svelteはコンポーネントを一度コンパイルして必要な情報を取得する必要がある。Svelteのコンパイルからコード生成までは、主に以下のフェーズを経る:
- JavaScript、HTML + Svelteテンプレート構文、CSS構文をASTにパースする
new Component(ast)を呼び出してSvelteコンポーネントを生成する。コンポーネント内には主にinstance、fragment、varsなどの情報が含まれる(src/compiler/compile/Component.ts)renderer.renderを呼び出してjsとcssを生成する
実際のフローや処理は上記よりもはるかに複雑だ(intro、outroの制御、イベントリスナー、変数の追跡など)。フロー全体はこの図を参考にするといい:

1. ソースコードをASTにパースする
Svelteはまずコンポーネントを3つの主要部分に分解する:HTML(およびSvelte構文)、CSS、そしてJavaScriptであり、それぞれ異なるパーサーでパースされる。

Svelteコンポーネントがパースされた後にどのようになるかを知りたい場合は、AST Explorerで確認できる:
<script>
let count = 0;
count++;
</script>
<style>
p {
font-size: 14px;
}
</style>
<p>count is {count}</p>
生成された構文木(右半分):

パース後はhtml、css、instanceという3つのASTが生成されることがわかる。ここでinstanceは<script>の中に含まれるJavaScriptコードを指している。
2. Svelteコンポーネント(Component)の生成

このフェーズで、SvelteはASTの中から必要な情報をComponentというクラスの中に保持する。これにはコンポーネントのHTML(Svelte内ではfragmentと名付けられている)、宣言された変数、instanceのASTなどが含まれる。
その後、instance(つまり上図の<script>で囲まれた部分)を走査して、すべての変数の使用状況を把握する。この時点で、変数が宣言されたものの未使用であるかどうかや、$プレフィックスを持つ処理すべき変数があるかどうかをすでに検出できる。
続いてHTML部分の走査を開始し、fragmentを構築する。この部分は、Svelteのコンパイルにおいて最もコアなロジックの一つと言える。Fragmentは多くの種類に分類でき、通常のHTMLタグや、ifやawaitといったSvelte構文などが含まれる。
// https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/nodes/shared/map_children.ts
function get_constructor(type) {
switch (type) {
case 'AwaitBlock': return AwaitBlock;
case 'Body': return Body;
case 'Comment': return Comment;
case 'EachBlock': return EachBlock;
case 'Element': return Element;
case 'Head': return Head;
case 'IfBlock': return IfBlock;
case 'InlineComponent': return InlineComponent;
case 'KeyBlock': return KeyBlock;
case 'MustacheTag': return MustacheTag;
case 'Options': return Options;
case 'RawMustacheTag': return RawMustacheTag;
case 'DebugTag': return DebugTag;
case 'Slot': return Slot;
case 'Text': return Text;
case 'Title': return Title;
case 'Window': return Window;
default: throw new Error(`Not implemented: ${type}`);
}
}
扱いやすくするために、Svelteは異なるタイプのfragmentごとにクラスを別途定義している。それぞれのクラスの実装についてはここでは詳しく展開しないが、いくつか例を挙げる:
Elementは通常のHTMLタグに対応し、イベントハンドラーや属性チェック、a11yチェックなどを処理する。- 例えばtagNameが
aなのにhrefが付いていない場合に警告を出す(ソースコード)
- 例えばtagNameが
IfBlockは{#if}や{:else}の構文の処理を担当するEachBlockは{#each}の構文の処理を担当する
その後、Svelteは対応するCSSにハッシュを付与して命名の衝突を防ぎ、cssスタイルを生成する。
3. fragmentとblockの構築
いよいよコード生成のフェーズに到達した。コード生成のロジック全体はsrc/compiler/compile/render_dom/Renderer.tsにある(Svelteは現在がSSRかDOMかに応じてレンダラーを選択するが、ここではDOMを例とする)。
まずfragmentを構築し、Wrapper内のrender関数を使って生成するコードの内容を定義する。例えばText.tsはテキスト生成の部分を担当している。fragmentは子ノードを走査し続け、render関数を呼び出して対応するコードを生成し、コードスニペットをblockの中に入れていく。
次にblockを宣言する。blockの中には非常に多くのコードフラグメント(マウント時やアンマウント時に生成されるべきコードなど)があり、最終的にcreate_fragment関数を構築するために使用される。この部分はSvelteの中で最もコアであり、最も複雑な部分と言える。実装全体はsrc/compiler/render_dom/index.tsで確認できる。
コード生成の部分には、作者のRich Harrisが作成したcode-redが使われており、コード生成を容易にしている。このライブラリのユニークな点は、var a = 1のような記述で直接対応するASTノードを生成できるところだ。例えば例の中のvariableAは、実際にはVariableDeclarationのノードになる。さらにテンプレートリテラル構文を組み合わせることで、手軽にコードを生成できる。
コード生成の部分については、IT鐵人賽の動画「生成元件程式碼」も参考になる:
例えば、僕が動的にadd関数を生成したい場合はこのように書ける:
最後にprintというAPIを通して構文木をコードへと変換する。次にSvelte内での実装を見てみよう(EachBlock.tsを例にする。他の生成ロジックは比較的複雑だ)。コードを生成するソースコードがどのように書かれているかを確認してみる:
// 在元件 create 時呼叫 each_block_else.c()
block.chunks.create.push(b`
if (${each_block_else}) {
${each_block_else}.c();
}
`);
if (this.renderer.options.hydratable) {
block.chunks.claim.push(b`
if (${each_block_else}) {
${each_block_else}.l(${parent_nodes});
}
`);
}
// 在元件 mount 時呼叫 each_block_else.m()
block.chunks.mount.push(b`
if (${each_block_else}) {
${each_block_else}.m(${initial_mount_node}, ${initial_anchor_node});
}
`);
中で呼び出されているbこそがcode-redのAPIの一つであり、これらが最終的に生成されるコードとなる。
なぜSvelteはコードをJavaScriptにコンパイルできるのか?
本記事の冒頭の疑問に戻ると、Svelteはコードを事前にコンパイル・解析するため、.svelteをJavaScriptにコンパイルできるのだ。
なぜSvelteの中ではテンプレートエンジンのような構文が使え、一般的なテンプレートエンジンと何が違うのか?
Svelteはカスタムパーサーを実装しており、通常のHTMLをパースするだけでなく{}の中の構文もパースでき、上述したコンパイルプロセスを経て対応するJavaScriptコードを生成するからだ。一般的なテンプレートエンジンの構文との違いは、**Svelteの構文はリアクティブ(reactive)であるのに対し、一般的なテンプレートエンジンの構文は静的なHTMLである点だ。**erbを例に挙げると:
<% unless content.empty? %>
<div>
<%= content.text %>
</div>
<% end %>
このような構文は通常、バックエンドでHTMLがレンダリングされた後に返される。しかしSvelteにおける構文:
{#if content}
<div>
{content.text}
</div>
{/if}
これはcontentが空でなくなった際に、画面をリアクティブに更新する。
まとめ
本記事では、コードをASTにパースし、fragmentと対応するノードを構築し、最終的にレンダラーによってコードを生成するまでという、Svelteのコンパイルからコード生成までの大まかなフローを説明してきた。細かいディテールや実装にはあまり深く踏み込まなかったが、今後の記事でより深く掘り下げていく予定だ。本記事を通して、Svelteがコードを生成するプロセスについて読者のみんなが一定の理解を得られたなら幸いだ。
参考リソース
- Li Hau Tan — The Svelte Compiler Handbook:Svelteのコンパイルプロセスが非常に詳細に解説されており、おすすめだ!
関連記事
- Three.js で僕の部屋を表現する 僕が React Three Fiber を使って実際の自分の部屋をブラウザ上に再現し、実世界のオブジェクトを目録に見立て、空間の記憶を通してここ数年の生活と仕事について語った話。
- フロントエンドで画像を扱う際に注意すべきこと Jake Archibaldの記事を起点に、現代のレスポンシブ画像の書き方を整理する。なぜwidth/heightを付ける必要があるのか、CSSのaspect-ratioはいつ使うべきか、AVIFとWebPの選び方、そしてpicture/source/srcsetを使ったモバイル向け画像の切り替えについて。
- CSS field-sizing — たった1行のCSSでフォーム要素を自動リサイズする かつてtextareaの自動高さ調整は、JavaScriptでscrollHeightを監視するしかなかった。しかしCSSのfield-sizing: contentなら、わずか1行で代替でき、textarea、input、selectに対応している。本記事では従来のやり方のペインポイントと、field-sizingの使い方をまとめる。
- リンクの下線をもっと見栄え良くする:text-underline-offset デフォルトでは下線と文字が近すぎて、このスタイルを好まないデザイナーもいるし、僕自身もあまり綺麗ではないと感じていた。