🧊

Svelte 3の仕組みとその手触り

実は最近まとまった時間ができたので、フロントエンド勘を取り戻すためにも、ずっと気になってたSvelteを触ってみてる。

一通りDocsやTutorialに目を通しきったところで、備忘録も兼ねて記事を書いておこうかと思った次第。

Svelte 3: Rethinking reactivity

Svelteは`3`が最新のメジャーバージョンで、これがリリースされたのは2019年の4月のこと。
なんと1年も前の話なのでぜんぜん今さらではあるけど、まだ1年しか経ってないとも取れる・・はず。

モチベーション

冒頭の記事の中にある、Rethinking reactivityっていう公演の動画を見てハッとしたから。

動画の説明文を引用するとこんな具合。

Modern JavaScript frameworks are all about reactivity. Change your application's state, and the view updates automatically. But there's a catch — tracking state changes at runtime adds overhead that eats into your bundle size and performance budgets. In this talk, we'll discover an alternative approach: moving reactivity into the language itself. Your apps have never been smaller or faster than they're about to become.

これは・・気になる!

思えばまだAndroid 2.3とかカクカクの端末と戦ってた頃、ある日突然「これからはReactだ!VirtualDOMだ!」とか言われて、全然納得できてなかったのを思い出した。

あとはMobX信者としても、単純にReactivityの実装も気になるし、ひとつ試してみるか!ということで。

Svelteの概要

  • ReactなどのUIライブラリとは違い、コンパイラ(とそのランタイム)
    • もちろんVirtualDOMでもない
  • いわばSvelte言語を書くと、いい感じにコンパイルされる
    • とはいってもHTML/CSS/JSがベース
    • なのでランタイムが最低限に抑えられて、効率的であり軽くもなる
  • Svelte言語は基本的に直感的でわかりやすい
    • `markup`部のディレクティブまわりとかは、Vue|Angularのそれに似ているといえば似ている
    • むしろ既成概念がない方が扱いやすいかもしれない
    • むしろ何も知らなくても扱えるというのはメリットかもしれない

とりあえず書けば動く!Write less code!を地で行く感じがあって、とっつきやすさもよいなと思った。

フロントエンド界隈にありがちな、また雨後の竹の子が・・っていうパターンではないってのがミソ。

コンパイラがやってること

コントリビューターの人が書いてる記事があって、仕組みを知りたい人は必読。

The Svelte Compiler Handbook | Tan Li Hau

`*.svelte`なファイルは、コンパイル時にまずASTにされて、そこからあれこれ処理されていくらしい。

そしてその詳細が、また別のシリーズになってる。

Compile Svelte in your head (Part 1) | Tan Li Hau

Reactivityを実現してるところだけをすごい雑に説明すると、"コンパイル時に"こんな感じの変換が行われる。

// これが
const update = () => count++;

// こうなる
const update = () => $$invalidate(0, count++, count);

こんな感じの変換が、値の再代入やアップデート、テンプレでの参照が行われてる箇所ぜんぶに対して行われる!
コンパイル後のコードを見るとわかるけど、もし値が変更されていたら(dirtyだったら)みたいな`if`分が大量に生成される。

で、この`$$invalidate()`が、再描画が必要かどうかチェックしたりしてて、必要なら再描画がスケジュールされる。

というように、ここがMobXとかのライブラリとの違いで、"Observableな値の更新があったら、それに関連付けられたこの関数を呼ぶみたいな機構そのもの"が、ランタイムに落ちてこないのがSvelteの特徴的なところ。

↑の記事にもあるけど、dirtyかどうかのチェックにはビット演算が使われてて(`v3.16.0`から)、ランタイムの最適化の気概が感じられてよかった。
あと描画の処理も`Promise.then()`で投げられるので、MicrotaskになってUIをブロックしないとか。

このコンパイラは他にもなかなかに賢くて、Reactだと`onClick`にインライン関数書くと毎回無駄になるけど、それも最適化してくれたりする。

(「最小のコード片をコンパイルしたコード全部読む」記事、今度書こうかな・・・。)

ある程度以上の規模になってくると、ファイルサイズが実装よりも膨れてくるのでは?と思ったりもしたけど、そもそもそんな巨大コードつくるなCode Splittingしろって話か・・・!

使い勝手

本家サイトにある、Tutorialがよくできてて、順にやっていくとすごいわかった気になれる。

Introduction / Basics • Svelte Tutorial

そんな中からいくつか「これは!」ってなったやつをご紹介。

Reactive declarations / statements

MobXでいう`computed`で、自動的に導出される値ってやつ。

let count = 0;
// コレ
$: doubled = count * 2;

なんじゃこりゃ!って最初は思ったけど、label文っていうちゃんとしたJavaScriptの言語機能らしい。

Svelteでは`$:`で宣言すると、その中で依存している値の更新にあわせてReactiveに機能するようになる。

そこにはフックされる関数も何もなく、正にReactivityを宣言的に記述できる。

// ブロックもおけるので関数も書ける
$: {
  console.log(count);
}
// まさかのロジックまで
$: if (count >= 10) {
  console.log(`count: ${count} is dangerously high!`);
  count = 9;
}

コンパイラだからこそ為せるワザ・・・。

Await blocks

`markup`部で、`Promise`の解決を待てちゃう。

{#await promise}
  <p>...waiting</p>
{:then number}
  <p>The number is {number}</p>
{:catch error}
  <p>{error.message}</p>
{/await}

お手軽さがすごい。

Binding this

いわゆるReactでいう`ref`のこと。

<script>
  import { onMount } from "svelte";

  let canvas;
  onMount(() => {
    const ctx = canvas.getContext("2d");
  });
</script>

<canvas bind:this={canvas}></canvas>

`onMount()`を待たないと、`undefined`になってしまうので注意。

Builtin store module

`script`部で宣言した変数は、自動的に`markup`部で参照してバインディングされる。

けど、それ以外にコンポーネントのスコープをまたいだ状態を管理するための変数が欲しくなるはず・・。

で、なんとそれ用の仕組みもSvelteには用意されてて、それらは`svelte/store`で`import`できる。

import { writable } from "svelte/store";

const count = writable(0);

const unsubscribe = count.subscribe(value => {
	console.log(value);
}); // logs '0'

count.set(1); // logs '1'
count.update(n => n + 1); // logs '2'

手動で`subscribe()`する代わりに、`$count`という感じで自動で参照することもできて、`markup`部に埋めるときはこっちが便利。

あとは、

  • `writable()`な値以外に、`readable()`な値も定義できる
  • storeの値からさらに導出する`derived()`というヘルパーもある
  • `subscribe()`と`set()`メソッドさえ実装すれば、それを独自のstoreとして扱える

なかなかかゆいところに手が届く・・・。

Builtin motion/transition/animation

storeだけでなく、こんなものまである。

`writable()`の代わりに、`svelte/motion`から`tweened()`を使うだけで、それだけTweeningして値を設定できるようになる。

import { tweened } from 'svelte/motion';
const progress = tweened(0);

モーションの微調整もできるし、イージング関数も`svelte/easing`にあるものが標準で使える。

また、`markup`にちょろっと書くだけでフェードインやブラーのエフェクトがつけられる・・・。

楽ちん。

Module context

普通のJavaScriptは`script`要素の中に書ける + 通常は`*.svelte`ファイル内に1つしか置けない。

ただし、`context="module"`を指定したものは、複数のコンポーネントインスタンスからまたいで参照できる。

Module context / Exports • Svelte Tutorial

そのほか

  • Context API
  • `createEventDispatcher()`
    • その名の通り、独自のイベントを`dispatch()`できる
    • 中身はただのDOMの`CustomEvent`
  • `svelte`要素
    • `:window bind:scrollY={y}`とかできる
      • ほかには`innerWidth`とかある
      • 雑な高頻度イベントハンドラが書かれることを事前に阻止してる・・!

開発体験

まだちゃんとしたアプリは書いてないけど、Hello worldレベルでわかったこと。

パッと思い浮かんだ手の届かないところは、コミュニティとしても認識してるっぽく、だいたいFAQに書いてあった。

Frequently Asked Questions • Svelte

まとめ

パラダイムがそもそも違うので、よしあしの判断は前提条件に依りそうかなーと思った。
(なので「XXXより良い!」とか簡単に言えない)

https://gist.github.com/Rich-Harris/0f910048478c2a6505d1c32185b61934

Svelte is a language.

ほんとその一言に尽きるというところで、人は選びそう。
個人的にはバランスがとてもいいなと思ったし、便利ながらも遊びが残されたAPIたちにはとても好感が持てた。(使い手の練度を要求するあたり)

あとはなにより"早くて軽い"は正義。

Svelte on Twitter: "Svelte gets to those hard-to-reach places other frameworks can't.… "

非力な端末でいい感じに動くというだけでも採用する理由になるなとも。

まあまだまだこれから感は否めず、バグもそれなりにありそうなので、地雷を踏み抜きながら見守っていこうかと。
とりあえず手元にあるちょうどいいプロジェクトを書き直してみようと思ってます。

そういえばReactでいうNext.jsの立ち位置で、SvelteにもSapperというのがあるらしいけど、それはまた追々。