ついに、frontend の 新な大本命、Svelte 5 が2024年10月22日にリリースされました!
2024年4月30日にSvelte 5 RC(Release Candidate)が登場してから、この日を心待ちにしていました。これまでSvelteは、小規模なアプリケーションや趣味の開発には使いやすいものの、大規模なアプリケーションでは採用しづらいという印象がありました。しかし、個人的に、Reactベースのフレームワークに比べて、仮想DOMを使わないコンパイラとしてのアプローチや、シンプルで理解しやすいソースコード、そして書き心地の良さから、Svelteには大きな期待を寄せていました。そんな中、Svelte 5ではついに大規模アプリケーションでも活用できると思える素晴らしい改善が施されています。
それでは、Svelte 5で最も注目すべき「Runes」についてご紹介します。
Svelteが大規模アプリケーションでは採用しづらい理由
Svelte 5 のリリースブログでもこれまでのSvelte 4までの大きな課題として下記が挙げられています。
For example, in Svelte 4, reactivity is driven entirely by the compiler. If you change a single property of a reactive object in Svelte 4, the entire object is invalidated, because that’s all the compiler can realistically do. Meanwhile, other frameworks have adopted fine-grained reactivity based on signals, leapfrogging Svelte’s performance.
Svelte 4までは、リアクティブな変数の値が変更されると、その変数全体が再評価されるようにコンパイルされていました。 コメントで指摘もらい、間違った情報を書いていました。
例えば、次のようなSvelteのコードがあったとします。
<script>
let user = { name: 'Alice', age: 25 };
function increaseAge() {
user = { ...user, age: user.age + 1 };
}
</script>
<div>
<p>{user.name}</p>
<p>{user.age}</p>
<button on:click={increaseAge}>Increase Age</button>
</div>
この場合、age
だけを変更したくても user
変数全体が再評価されてしまいます。結果的に、<p>{user.name}</p>
と<p>{user.age}</p>
の両方が再描画されてしまいます。本来は<p>{user.age}</p>
だけを更新すれば済むはずなのに、です。
この例では問題は小さいのですが、変数がより複雑なオブジェクト(例えば一覧を表示するためのリストなど)の場合、無駄な再描画が増えて、Svelteの「コンパイラだから速い」という利点が薄れてしまいます。
対策として、変数を細かく分割することもできますが、それによって本来一つにまとまっているべきオブジェクトが分割され、ソースコードの可読性が低下するリスクがあります。Svelteの強みである「可読性の良さ」がなくなってしまうのです!
というわけで、この問題は大きなボトルネックでした。
ReactとSolid.jsのアプローチ
一方で、Reactの場合は次のようになります。
function UserComponent() {
const [user, setUser] = useState({ name: 'Alice', age: 25 });
return (
<div>
<p>{user.name}</p>
<p>{user.age}</p>
<button onClick={() => setUser(prevUser => ({ ...prevUser, age: prevUser.age + 1 }))}>
Increase Age
</button>
</div>
);
}
Reactでは、setUser
によって新しいuser
オブジェクトが作成され、コンポーネント全体の再レンダリングが発生しますが、その後の仮想DOMの比較によって変更箇所のみが最小限に更新されます。これにより、無駄な再描画を避け、パフォーマンスを保つことができます。
なんと、仮想DOMはクソだと言っていた Svelte ですが、Reactの仮想DOMの方が速いじゃないですか!(そんなこと言ってませんね。私がそう言ってるように聞こえているだけです。)
ちなみに、Svelte 5 のリリースブログで触れられている「Signal」は、Solid.jsの状態管理手法です。
import { createSignal } from "solid-js";
function UserComponent() {
const [user, setUser] = createSignal({ name: 'Alice', age: 25 });
const increaseAge = () => {
setUser(prevUser => ({ ...prevUser, age: prevUser.age + 1 }));
};
return (
<div>
<p>{user().name}</p>
<p>{user().age}</p>
<button onClick={increaseAge}>Increase Age</button>
</div>
);
}
export default UserComponent;
Solid.jsでは、user
は関数であり、値の取得も関数呼び出しで行われるため、より効率的に更新が行われます。
そのうち、React にもシグナルの考え方が導入されるかと思います。
Svelte 5の解決
さて、ここからが本題です。Svelte 5ではこの問題が綺麗に解決されるのです。ぱちぱち。下記が Svelte 5でのコードです。
<script>
let user = $state({ name: 'Alice', age: 25 });
function increaseAge() {
user.age += 1;
}
</script>
<div>
<p>{user.name}</p>
<p>{user.age}</p>
<button on:click={increaseAge}>Increase Age</button>
</div>
$state()
は Svelte 5で導入されたRunesが提供する関数です。Runesという名前が付けられていますが、 Svelte言語の中に組み込まれているので利用するのにimportする必要はありません。この $state()
関数を使うとリアクティブな変数の宣言が行えます。さらに、オブジェクト全体の変更をしなくてもプロパティの値を直接変更することが可能です!
ちなみに、Svelte 5でも let
や$:
は動作しますがレガシーモードとして扱われています。Svelteは破壊的変更を極力避けるようにしているので、旧来の書き方でも書けますが、いずれ廃止されることになるので、これからはRunesを使っていきましょう。
Runesは、他に多くの関数を提供しており、全て $
から始まります。コンパイラの処理がシンプルになりますし、特殊な変数がソースコード上も一目でわかります。これは、アプリケーションを複数人で開発する上で重要な観点です。React が独自ルール多くて複雑であっても支援されている理由も、この点にあるのかもしれません。Svelteもその点で一歩近づいたと感じます。しかもソースコードはわかりやすいまま!
Runes自体の紹介は、2023年9月にブログで書かれています。
公式のドキュメントは下記です。
これからは Svelte 5の時代です。Svelte 5に続いて、SvelteKitの新バージョンも期待しています!
(2024/10/25追記)
コメントの内容を受け、Svelte 5で何の問題が解決されるかですが、オブジェクトの値全体を再代入する必要がなくなるという点ですね。変更あるプロパティのみ変更すれば再評価してくれるというのがSvelte 5の進化ポイントでした。