Preact Signals と lit
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ Preact/React で再レンダリングせず必要な箇所だけを更新できる
◆ lit3.0 の labs パッケージで Preact のシグナルをサポート
◆ lit でも再レンダリングを防げるけどパフォーマンス的にはあまり改善しないみたい
◆ lit3.0 の labs パッケージで Preact のシグナルをサポート
◆ lit でも再レンダリングを防げるけどパフォーマンス的にはあまり改善しないみたい
Preact にシグナルが追加されたというのは前に見かけて知ってたのですが あまり気にしてなかったです
つい最近 lit3.0 のプレリリースが出て そこで Preact のシグナルが使えるようになると発表されていました
lit に導入されるようなものならどんなものか気になるのでちゃんと見てみることにしました
https://preactjs.com/guide/v10/signals/
こんな感じで使えるようです
これだとシグナルがコンポーネントの外にあって全部の Counter で共有されます
コンポーネントのローカルに値を保持するなら useSignal を使います
useSignal は中で useMemo を使って signal を呼び出すだけです
最初に signal を作ってそれ以降は同じ signal を返します
https://github.com/preactjs/signals/blob/%40preact/signals-core%401.3.1/packages/preact/src/index.ts#L335
これだと state の更新が発生しないのでどうやって再レンダリングしてるのでしょうか
見てみると内部的には state の更新をしているようでした
https://github.com/preactjs/signals/blob/%40preact/signals-core%401.3.1/packages/preact/src/index.ts#L149
count.value を更新したときのセッターで computed や effect に通知していますが その中の effect の一つで setState を行う関数が呼び出されていました
しかし それを防ぐこともできるようです
再レンダリングせずシグナルを埋め込んだ場所だけを更新するようです
最近書いた記事で Vue だとこういうことできるのかなと書いたものそのままです
やり方は .value にアクセスせず signal のオブジェクトをそのまま JSX に埋め込むだけです
比較用コードを用意しました
ビルドなく動かせるようするため Preact は JSX の代わりに htm を使っています
上のボタンが CounterRerender コンポーネントで .value を使っています
下のボタンが CounterNoRerender コンポーネントで .value を使っていません
上のボタンは押すたびコンソールに rerender が表示されますが 下のボタンは押しても何も表示されないです
となると困るのは その場でちょっとした変換をしたいケースです
例えば連番系は内部的には 0 から始まることが多いですが 表示上は 1 からにしたいです
みたいなことをすると再レンダリングが必要になります
他にも日付系は Date 型で持っておき 表示するときに toLocaleString() でフォーマットしたいということがあります
こういうときは computed で表示用の値を変数に保持することになります
一つの値を元にちょっとだけ計算した値がいっぱいあるときは computed の変数をいっぱい書くことになって面倒です
そこまで多いならコンポーネントに分けて再レンダリングしたほうがいいかもしれませんけど
React で使う場合は @preact/signals-react を使います
signal や useSignal など Preact と同じものがエクスポートされてるので同じ感じで使えます
中でどういう事をしてるのかなと見てみると 複雑でした
https://github.com/preactjs/signals/blob/%40preact/signals-react%401.3.4/packages/react/runtime/src/auto.ts
コメントでの説明が長すぎですね
いかにも内部用って名前の React のプロパティにアクセスしたりしています
また JSX をラップして props にシグナルを渡したときに内部で自動で .value の値に置き換えたりもしてます
html 関数をラップして ${} の中が Signal なら WatchDirective でラップするよう動作を変更してるようです
変更を検知したら Part の setValue を直接行うので render 関数は呼び出されません
https://github.com/lit/lit/blob/%40lit-labs/preact-signals%401.0.0-pre.0/packages/labs/preact-signals/src/lib/html-tag.ts
https://github.com/lit/lit/blob/%40lit-labs/preact-signals%401.0.0-pre.0/packages/labs/preact-signals/src/lib/watch.ts
これならシグナルのみを使って再レンダリングを完全に不要にすることもできそうです
そうすれば Van みたいな感じで関数の中でシグナルを作成してコンポーネント風にもできそうです
Van を htm と一緒に使うとほぼ同じ見た目です
動作確認用
ボタンを押すとカウントが増えますが コンソールに render が表示されるのは画面が表示される最初の一回だけです
もともと lit は React と違って仮想 DOM は用いない高速な更新方法ですからね
テンプレートリテラルの ${} の位置から可変部分が事前にわかって そこだけを直前の値と比較して変わっていたら更新するという方法です
仮想 DOM の構築と 仮想 DOM 全体の比較という処理がいらないので ${} の数だけ単純な比較をするだけです
比較の結果に差分があれば実際の更新処理を行いますが それはライブラリを問わない必要な更新処理です
lit を使うことによる追加の処理はほぼないです
遅くなる原因があるとすれば再レンダリング中の計算です
React でいう useMemo が役立つ部分です
ただこれはシグナルを使っても computed などで結局計算する必要はあると思います
なのであまりパフォーマンスには影響しないというのは理解できます
すでにシグナルの実装は色々あるようで lit もまた独自に実装するのは相互運用性的に良くないので既存のものを使う方針のようです
今は Preact だけですが Preact のみではなく他のライブラリも対応していくようです
なぜ最初が Preact だったのかは npm にあるパッケージで小さく高速で理解しやすい実装だったからだそうです
特に context や task が labs から正式パッケージになったのは大きいですね
つい最近 lit3.0 のプレリリースが出て そこで Preact のシグナルが使えるようになると発表されていました
lit に導入されるようなものならどんなものか気になるのでちゃんと見てみることにしました
https://preactjs.com/guide/v10/signals/
signal
名前から予想はしてましたが Solid のシグナルに近いものでしたこんな感じで使えるようです
import { signal } from "@preact/signals"
const count = signal(0)
const Counter = () => {
return (
<div>
<button onClick={() => count.value++}>{count.value}</button>
</div>
)
}
これだとシグナルがコンポーネントの外にあって全部の Counter で共有されます
コンポーネントのローカルに値を保持するなら useSignal を使います
import { useSignal } from "@preact/signals"
const Counter = () => {
const count = useSignal(0)
return (
<div>
<button onClick={() => count.value++}>{count.value}</button>
</div>
)
}
useSignal は中で useMemo を使って signal を呼び出すだけです
最初に signal を作ってそれ以降は同じ signal を返します
https://github.com/preactjs/signals/blob/%40preact/signals-core%401.3.1/packages/preact/src/index.ts#L335
これだと state の更新が発生しないのでどうやって再レンダリングしてるのでしょうか
見てみると内部的には state の更新をしているようでした
https://github.com/preactjs/signals/blob/%40preact/signals-core%401.3.1/packages/preact/src/index.ts#L149
count.value を更新したときのセッターで computed や effect に通知していますが その中の effect の一つで setState を行う関数が呼び出されていました
再レンダリングしない
上の例のように .value にアクセスしてそれを JSX に埋め込むと 変更時には内部的な state の更新が起きて再レンダリングされますしかし それを防ぐこともできるようです
再レンダリングせずシグナルを埋め込んだ場所だけを更新するようです
最近書いた記事で Vue だとこういうことできるのかなと書いたものそのままです
やり方は .value にアクセスせず signal のオブジェクトをそのまま JSX に埋め込むだけです
import { useSignal } from "@preact/signals"
const Counter = () => {
const count = useSignal(0)
return (
<div>
<button onClick={() => count.value++}>{count}</button>
</div>
)
}
比較用コードを用意しました
ビルドなく動かせるようするため Preact は JSX の代わりに htm を使っています
<!doctype html>
<meta charset="utf-8" />
<script type="importmap">
{
"imports": {
"preact": "https://unpkg.com/preact@10.18.1/dist/preact.module.js",
"preact/hooks": "https://unpkg.com/preact@10.18.1/hooks/dist/hooks.module.js",
"htm": "https://unpkg.com/htm@3.1.1/dist/htm.module.js",
"htm/preact": "https://unpkg.com/htm@3.1.1/preact/index.module.js",
"@preact/signals-core": "https://unpkg.com/@preact/signals-core@1.5.0/dist/signals-core.module.js",
"@preact/signals": "https://unpkg.com/@preact/signals@1.2.1/dist/signals.module.js"
}
}
</script>
<script type="module">
import { html, render } from "htm/preact"
import { useSignal } from "@preact/signals"
const App = () => {
return html`
<${CounterRerender} />
<${CounterNoRerender} />
`
}
const CounterRerender = () => {
const count = useSignal(0)
console.log("rerender (CounterRerender)")
return html`<button onClick=${() => count.value++}>${count.value}</button>`
}
const CounterNoRerender = () => {
const count = useSignal(0)
console.log("rerender (CounterNoRerender)")
return html`<button onClick=${() => count.value++}>${count}</button>`
}
render(
html`<${App}/>`,
document.getElementById("root")
)
</script>
<div id="root"></div>
上のボタンが CounterRerender コンポーネントで .value を使っています
下のボタンが CounterNoRerender コンポーネントで .value を使っていません
上のボタンは押すたびコンソールに rerender が表示されますが 下のボタンは押しても何も表示されないです
signal をそのまま埋め込む必要あり
この機能を使うには .value を使って生の値にアクセスせずシグナルそのものを埋め込む必要がありますとなると困るのは その場でちょっとした変換をしたいケースです
例えば連番系は内部的には 0 から始まることが多いですが 表示上は 1 からにしたいです
<div>{num.value + 1}</div>
みたいなことをすると再レンダリングが必要になります
他にも日付系は Date 型で持っておき 表示するときに toLocaleString() でフォーマットしたいということがあります
こういうときは computed で表示用の値を変数に保持することになります
import { signal, computed } from "@preact/signals"
const now = signal(new Date())
setInterval(() => {
now.value = new Date()
}, 1000)
const now_text = computed(() => {
return now.value.toLocaleString()
})
const Clock = () => {
return (
<div>now: {now_text}</div>
)
}
一つの値を元にちょっとだけ計算した値がいっぱいあるときは computed の変数をいっぱい書くことになって面倒です
そこまで多いならコンポーネントに分けて再レンダリングしたほうがいいかもしれませんけど
React
ここまでは Preact で書いていましたが React にも対応していますReact で使う場合は @preact/signals-react を使います
signal や useSignal など Preact と同じものがエクスポートされてるので同じ感じで使えます
中でどういう事をしてるのかなと見てみると 複雑でした
https://github.com/preactjs/signals/blob/%40preact/signals-react%401.3.4/packages/react/runtime/src/auto.ts
コメントでの説明が長すぎですね
いかにも内部用って名前の React のプロパティにアクセスしたりしています
また JSX をラップして props にシグナルを渡したときに内部で自動で .value の値に置き換えたりもしてます
lit3.0
lit3.0 でのシグナル対応ですが lit-html が更新されたのかと思いましたが @lit-labs/preact-signals という labs のパッケージになってましたhtml 関数をラップして ${} の中が Signal なら WatchDirective でラップするよう動作を変更してるようです
変更を検知したら Part の setValue を直接行うので render 関数は呼び出されません
https://github.com/lit/lit/blob/%40lit-labs/preact-signals%401.0.0-pre.0/packages/labs/preact-signals/src/lib/html-tag.ts
https://github.com/lit/lit/blob/%40lit-labs/preact-signals%401.0.0-pre.0/packages/labs/preact-signals/src/lib/watch.ts
これならシグナルのみを使って再レンダリングを完全に不要にすることもできそうです
そうすれば Van みたいな感じで関数の中でシグナルを作成してコンポーネント風にもできそうです
const Counter = () => {
const count = signal(0)
return html`<button @click=${() => count.value++}>${count}</button>`
}
Van を htm と一緒に使うとほぼ同じ見た目です
const Counter = () => {
const count = van.state(0)
return html`<button onclick=${() => count.val++}>${count}</button>`
}
動作確認用
<!doctype html>
<meta charset="utf-8" />
<script type="importmap">
{
"imports": {
"lit/": "https://unpkg.com/lit@3.0.0-pre.1/",
"lit-html": "https://unpkg.com/lit-html@3.0.0-pre.1/lit-html.js",
"lit-html/": "https://unpkg.com/lit-html@3.0.0-pre.1/",
"@lit-labs/preact-signals": "https://unpkg.com/@lit-labs/preact-signals@1.0.0-pre.0/index.js",
"@preact/signals-core": "https://unpkg.com/@preact/signals-core@1.5.0/dist/signals-core.module.js"
}
}
</script>
<script type="module">
import { render } from "lit-html"
import { html, signal } from "@lit-labs/preact-signals"
const Counter = () => {
const count = signal(0)
console.log("render")
return html`<button @click=${() => count.value++}>${count}</button>`
}
render(
Counter(),
document.getElementById("root")
)
</script>
<div id="root"></div>
ボタンを押すとカウントが増えますが コンソールに render が表示されるのは画面が表示される最初の一回だけです
パフォーマンス
気になるパフォーマンスですが プレリリースの記事ではあまり向上はしていないそうですもともと lit は React と違って仮想 DOM は用いない高速な更新方法ですからね
テンプレートリテラルの ${} の位置から可変部分が事前にわかって そこだけを直前の値と比較して変わっていたら更新するという方法です
仮想 DOM の構築と 仮想 DOM 全体の比較という処理がいらないので ${} の数だけ単純な比較をするだけです
比較の結果に差分があれば実際の更新処理を行いますが それはライブラリを問わない必要な更新処理です
lit を使うことによる追加の処理はほぼないです
遅くなる原因があるとすれば再レンダリング中の計算です
React でいう useMemo が役立つ部分です
ただこれはシグナルを使っても computed などで結局計算する必要はあると思います
なのであまりパフォーマンスには影響しないというのは理解できます
Preact の理由
Preact のシグナルを使った理由ですが プレリリースの記事に書かれていましたすでにシグナルの実装は色々あるようで lit もまた独自に実装するのは相互運用性的に良くないので既存のものを使う方針のようです
今は Preact だけですが Preact のみではなく他のライブラリも対応していくようです
なぜ最初が Preact だったのかは npm にあるパッケージで小さく高速で理解しやすい実装だったからだそうです
他の lit パッケージ
lit3.0 では 本体に新機能はないものの preact-signals のように別パッケージの更新は色々あるみたいです特に context や task が labs から正式パッケージになったのは大きいですね