EY-Office ブログ

話題のSolid JSに入門してみた(2/2)

先週の話題のSolid JSに入門してみた(1/2)の続きです。今回はSolid JSが生成したコードを見ながら高速性の理由を調べて行きたいと思います。

Solid JS https://www.solidjs.comより

簡単なコード

まず最初は、以下の簡単なカウンターのコードです。

import { createSignal, For } from "solid-js";

const App = () => {
  const [count, setCount] = createSignal(0);

  return (
    <>
      <button onClick={() => setCount(count() + 1)}>up</button>
      <div>Count: <b>{count()}</b></div>
    </>
  );

};
export default App;

開発環境で生成されたコード

開発環境でブラウザーのデベロッパーツールでみると下のようなコードに変換されています。

import類は読み飛ばしましたが、solid-js, solid-js/web等に、実行時に呼ばれる関数があるので適宜参照して解説を書いています。

import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/App.tsx");
import { template as _$template } from "/node_modules/.vite/deps/solid-js_web.js";
import { delegateEvents as _$delegateEvents } from
                                   "/node_modules/.vite/deps/solid-js_web.js";
import { insert as _$insert } from "/node_modules/.vite/deps/solid-js_web.js";
import { esm as _esm } from "/@solid-refresh";

const _tmpl$ = _$template(`<button>up</button>`, 2),        // ← ①
      _tmpl$2 = _$template(`<div>Count: <b></b></div>`, 4);

import { createSignal } from "/node_modules/.vite/deps/solid-js.js";
export const $$registrations = {};

const _Hot$$App = () => {                         // ← ②
  const [count, setCount] = createSignal(0);      // ← ③
  return [                                        // ← ④
    (() => {
      const _el$ = _tmpl$.cloneNode(true);        // ← ⑤
      _el$.$$click = () => setCount(count() + 1); // ← ⑥
      return _el$;
    })(),
    (() => {
      const _el$2 = _tmpl$2.cloneNode(true),      // ← ⑦
            _el$3 = _el$2.firstChild,
            _el$4 = _el$3.nextSibling;            // ← ⑧
      _$insert(_el$4, count);                     // ← ⑨
      return _el$2;
    })()
  ];
};

$$registrations._Hot$$App = {
  component: _Hot$$App,
  id: "_Hot$$App"
};
const {
  handler: _handler,
  Component: _Component
} = _esm($$registrations._Hot$$App, !!import.meta.hot);  // ← ⑩

if (import.meta.hot) import.meta.hot.accept(_mod => {    // ← ⑩
  _handler(_mod) && import.meta.hot.invalidate();
});
const App = _Component;
export default App;

_$delegateEvents(["click"]);    // ← ⑪
//# sourceMappingURL=data:application/json;base64........
  1. JSXが適当な単位で分解されたHTML文字列を子要素に持つ<template>DOM要素が作られています。
    • _$template()関数の2番目の引数(2や4)は現在使われてないようです
  2. App関数の生成されたものです_Hotは開発環境でのホットロードと関連するのでしょうか?
  3. createSignal()はそのままですね
  4. App関数の戻り値は、JSXに対応するDOM要素の配列です
    • DOM要素が1つのときは配列ではなくDOM要素が戻ります
  5. buttonタグのDOM要素のコピーcloneが作られます
  6. 上で作られた要素にSolidのclickイベント($$click)にハンドラーを設定しています
  7. divタグのDOM要素のコピー(clone)が作られます
  8. 上の子要素の2つ目の要素(<b></b>)を取り出します
  9. <b></b>の子要素にSiginalの取得関数count()を挿入しています
  10. この辺は開発環境でのホットロード関連のコードだと思われます
  11. DOMのclickイベントをSolidで扱う設定だと思われます

さてReactのStateに対応するSiginalですが、実行時ライブラリーのコードを少し眺めてみました。

  • createSignal()関数の戻り値は、solid-jsのreadSignal()関数とwriteSignal()関数で、これらは状態の管理だけではなく、DOM変更もこの中から起動されます
  • Siginalは状態の値だけではなく、多数の管理用データを持っています
    • 関連するDOM要素
    • 状態の値を呼び出すreadSignal()関数の配列
  • readSignal()関数が⑨の_$insert()関数の中で呼びだされた場合はDOM要素(⑨では_el$4)が管理情報に格納されます
  • writeSignal()関数で状態が変更されると、DOM要素と関連しているreadSignal()関数が実行され、DOMも更新されます
  • DOM要素と関係ないreadSignal()関数(⑥のcount())は単純に状態の値を戻します

ビルド環境で生成されたコード(部分)

ビルド環境npm run buildで生成されたコードは下のようになっています。最初の方には Solidの実行時に使う関数が書かれていますが、それを省略した上のコードに対応する部分は以下のようになっています。

// ・・・先頭にはSolidの実行時に使う関数があります・・・

const re = H("<button>up</button>"),
    fe = H("<div>Count: <b></b></div>"),
    ue = () => {
        const [e, n] = J(0);
        return [(() => {
            const t = re.cloneNode(!0);
            return t.$$click = () => n(e() + 1), t
        })(), (() => {
            const t = fe.cloneNode(!0),
                s = t.firstChild,
                i = s.nextSibling;
            return V(i, e), t
        })()]
    };
le(["click"]);
ie(() => ne(ue, {}), document.getElementById("root"));
  • 関数名、変数名がMinify(圧縮)されていますがApp関数の定義(下ではue)は、ほぼ開発環境と同じです
  • 当然ホットロード関連のコードは無くなっています
  • 少し最適化されています
  • App.tsxだけでなく、index.tsxのコードも含まれています(最終行)

実行時に使う関数を含んだコードは最後に置きました、興味がある方は見てください。

繰り返しのあるコード

もう少し複雑な繰り返し<For>のあるコードも見てみましょう

import { createSignal, For } from "solid-js";

const App = () => {
  const [hands, setHands] = createSignal<string[]>([]);

  const pon = () => {
    const te = ['グー', 'チョキ', 'パー'][Math.floor(Math.random() * 3)]
    setHands([...hands(), te])
  }

  return (
    <>
      <button onClick={() => pon()}> Pon </button>
      <ul>
        <For each={hands()}>{hand => <li>{hand}</li>}</For>
      </ul>
    </>
  );
};
export default App;

開発環境で生成されたコード

簡単なコードと同様の部分は省略しました。

// ・・・ importは省略 ・・・

const _tmpl$ = _$template(`<button> Pon </button>`, 2),   // ← ①
      _tmpl$2 = _$template(`<ul></ul>`, 2),
      _tmpl$3 = _$template(`<li></li>`, 2);

import { createSignal, For } from "/node_modules/.vite/deps/solid-js.js";
export const $$registrations = {};

const _Hot$$App = () => {
  const [hands, setHands] = createSignal([]);

  const pon = () => {                          // ← ②
    const te = ['グー', 'チョキ', 'パー'][Math.floor(Math.random() * 3)];
    setHands([...hands(), te]);
  };

  return [
    (() => {
      const _el$ = _tmpl$.cloneNode(true);
      _el$.$$click = () => pon();
      return _el$;
    })(),
    (() => {
      const _el$2 = _tmpl$2.cloneNode(true);
      _$insert(_el$2, _$createComponent(For, {  // ← ③
        get each() {                            // ← ④
          return hands();
        },
        children: hand => (() => {              // ← ⑤
          const _el$3 = _tmpl$3.cloneNode(true);
          _$insert(_el$3, hand); 
          return _el$3;
        })()
      }));
      return _el$2;
    })()
  ];
};

// ・・・ 以下は「簡単なコード」と同じ ・・・
  1. JSXはbutton, ulタグとForで繰り返すliタグの3つの<template>DOM要素が作られています
  2. JSX以外の関数等はそのままです
  3. ForはSloidに定義されたコンポーネントなので_$createComponent()関数でコンポーネントを呼び出しています
  4. each属性はゲッターeach()関数として定義されます
    • 戻り値はSiginalの取得関数hands()
  5. 子要素はchildren属性でコンポーネントに渡されます、ここは無名関数なのでそのままですね。コード内では
    • liタグのDOM要素のコピー(clone)が作られます
    • liタグの子要素にhands配列の要素の値を挿入しています

まとめ

Solidが生成するコードはムダがなく、高速なのが納得できます。
さらに、HTMLの作成は起動時のみで、実行中の状態変更は対応部分のDOM更新が実行されるだけです。Reactの再描画のような処理が動かないので、ここでも高速になります。

また、実行時に使われる関数のサイズもReactに比べるとも小さく、小規模なアプリならコンパクトなコードが生成されると思います。

Reactのコードを速く小さくしたい場合は、Solidにコンバートするのは良い選択肢かも知れませんね。😊





ビルド環境で生成されたコード(全部)

実行時に使う関数を含んだコードです、興味がある方は見てください。

const Q = function() {
    const n = document.createElement("link").relList;
    if (n && n.supports && n.supports("modulepreload"))
        return;
    for (const i of document.querySelectorAll('link[rel="modulepreload"]'))
        s(i);
    new MutationObserver(i => {
        for (const l of i)
            if (l.type === "childList")
                for (const o of l.addedNodes)
                    o.tagName === "LINK" && o.rel === "modulepreload" && s(o)
    }).observe(document, {
        childList: !0,
        subtree: !0
    });
    function t(i) {
        const l = {};
        return i.integrity && (l.integrity = i.integrity), i.referrerpolicy && (l.referrerPolicy = i.referrerpolicy), i.crossorigin === "use-credentials" ? l.credentials = "include" : i.crossorigin === "anonymous" ? l.credentials = "omit" : l.credentials = "same-origin", l
    }
    function s(i) {
        if (i.ep)
            return;
        i.ep = !0;
        const l = t(i);
        fetch(i.href, l)
    }
};
Q();
const p = {},
    W = (e, n) => e === n,
    _ = {
        equals: W
    };
let X = R;
const S = {},
    w = 1,
    C = 2,
    F = {
        owned: null,
        cleanups: null,
        context: null,
        owner: null
    };
var c = null;
let x = null,
    f = null,
    m = null,
    u = null,
    h = null,
    $ = 0;
function k(e, n) {
    const t = f,
        s = c,
        i = e.length === 0,
        l = i ? F : {
            owned: null,
            cleanups: null,
            context: null,
            owner: n || s
        },
        o = i ? e : () => e(() => P(l));
    c = l,
    f = null;
    try {
        return B(o, !0)
    } finally {
        f = t,
        c = s
    }
}
function J(e, n) {
    n = n ? Object.assign({}, _, n) : _;
    const t = {
            value: e,
            observers: null,
            observerSlots: null,
            pending: S,
            comparator: n.equals || void 0
        },
        s = i => (typeof i == "function" && (i = i(t.pending !== S ? t.pending : t.value)), O(t, i));
    return [Z.bind(t), s]
}
function T(e, n, t) {
    const s = ee(e, n, !1, w);
    L(s)
}
function Y(e) {
    if (m)
        return e();
    let n;
    const t = m = [];
    try {
        n = e()
    } finally {
        m = null
    }
    return B(() => {
        for (let s = 0; s < t.length; s += 1) {
            const i = t[s];
            if (i.pending !== S) {
                const l = i.pending;
                i.pending = S,
                O(i, l)
            }
        }
    }, !1), n
}
function I(e) {
    let n,
        t = f;
    return f = null, n = e(), f = t, n
}
function Z() {
    const e = x;
    if (this.sources && (this.state || e)) {
        const n = u;
        u = null,
        this.state === w || e ? L(this) : N(this),
        u = n
    }
    if (f) {
        const n = this.observers ? this.observers.length : 0;
        f.sources ? (f.sources.push(this), f.sourceSlots.push(n)) : (f.sources = [this], f.sourceSlots = [n]),
        this.observers ? (this.observers.push(f), this.observerSlots.push(f.sources.length - 1)) : (this.observers = [f], this.observerSlots = [f.sources.length - 1])
    }
    return this.value
}
function O(e, n, t) {
    if (m)
        return e.pending === S && m.push(e), e.pending = n, n;
    if (e.comparator && e.comparator(e.value, n))
        return n;
    let s = !1;
    return e.value = n, e.observers && e.observers.length && B(() => {
        for (let i = 0; i < e.observers.length; i += 1) {
            const l = e.observers[i];
            s && x.disposed.has(l),
            (s && !l.tState || !s && !l.state) && (l.pure ? u.push(l) : h.push(l), l.observers && j(l)),
            s || (l.state = w)
        }
        if (u.length > 1e6)
            throw u = [], new Error
    }, !1), n
}
function L(e) {
    if (!e.fn)
        return;
    P(e);
    const n = c,
        t = f,
        s = $;
    f = c = e,
    z(e, e.value, s),
    f = t,
    c = n
}
function z(e, n, t) {
    let s;
    try {
        s = e.fn(n)
    } catch (i) {
        G(i)
    }
    (!e.updatedAt || e.updatedAt <= t) && (e.observers && e.observers.length ? O(e, s) : e.value = s, e.updatedAt = t)
}
function ee(e, n, t, s=w, i) {
    const l = {
        fn: e,
        state: s,
        updatedAt: null,
        owned: null,
        sources: null,
        sourceSlots: null,
        cleanups: null,
        value: n,
        owner: c,
        context: null,
        pure: t
    };
    return c === null || c !== F && (c.owned ? c.owned.push(l) : c.owned = [l]), l
}
function M(e) {
    const n = x;
    if (e.state === 0 || n)
        return;
    if (e.state === C || n)
        return N(e);
    if (e.suspense && I(e.suspense.inFallback))
        return e.suspense.effects.push(e);
    const t = [e];
    for (; (e = e.owner) && (!e.updatedAt || e.updatedAt < $);)
        (e.state || n) && t.push(e);
    for (let s = t.length - 1; s >= 0; s--)
        if (e = t[s], e.state === w || n)
            L(e);
        else if (e.state === C || n) {
            const i = u;
            u = null,
            N(e, t[0]),
            u = i
        }
}
function B(e, n) {
    if (u)
        return e();
    let t = !1;
    n || (u = []),
    h ? t = !0 : h = [],
    $++;
    try {
        const s = e();
        return te(t), s
    } catch (s) {
        G(s)
    } finally {
        u = null,
        t || (h = null)
    }
}
function te(e) {
    u && (R(u), u = null),
    !e && (h.length ? Y(() => {
        X(h),
        h = null
    }) : h = null)
}
function R(e) {
    for (let n = 0; n < e.length; n++)
        M(e[n])
}
function N(e, n) {
    const t = x;
    e.state = 0;
    for (let s = 0; s < e.sources.length; s += 1) {
        const i = e.sources[s];
        i.sources && (i.state === w || t ? i !== n && M(i) : (i.state === C || t) && N(i, n))
    }
}
function j(e) {
    const n = x;
    for (let t = 0; t < e.observers.length; t += 1) {
        const s = e.observers[t];
        (!s.state || n) && (s.state = C, s.pure ? u.push(s) : h.push(s), s.observers && j(s))
    }
}
function P(e) {
    let n;
    if (e.sources)
        for (; e.sources.length;) {
            const t = e.sources.pop(),
                s = e.sourceSlots.pop(),
                i = t.observers;
            if (i && i.length) {
                const l = i.pop(),
                    o = t.observerSlots.pop();
                s < i.length && (l.sourceSlots[o] = s, i[s] = l, t.observerSlots[s] = o)
            }
        }
    if (e.owned) {
        for (n = 0; n < e.owned.length; n++)
            P(e.owned[n]);
        e.owned = null
    }
    if (e.cleanups) {
        for (n = 0; n < e.cleanups.length; n++)
            e.cleanups[n]();
        e.cleanups = null
    }
    e.state = 0,
    e.context = null
}
function G(e) {
    throw e
}
function ne(e, n) {
    return I(() => e(n || {}))
}
function se(e, n, t) {
    let s = t.length,
        i = n.length,
        l = s,
        o = 0,
        r = 0,
        d = n[i - 1].nextSibling,
        g = null;
    for (; o < i || r < l;) {
        if (n[o] === t[r]) {
            o++,
            r++;
            continue
        }
        for (; n[i - 1] === t[l - 1];)
            i--,
            l--;
        if (i === o) {
            const a = l < s ? r ? t[r - 1].nextSibling : t[l - r] : d;
            for (; r < l;)
                e.insertBefore(t[r++], a)
        } else if (l === r)
            for (; o < i;)
                (!g || !g.has(n[o])) && n[o].remove(),
                o++;
        else if (n[o] === t[l - 1] && t[r] === n[i - 1]) {
            const a = n[--i].nextSibling;
            e.insertBefore(t[r++], n[o++].nextSibling),
            e.insertBefore(t[--l], a),
            n[i] = t[l]
        } else {
            if (!g) {
                g = new Map;
                let y = r;
                for (; y < l;)
                    g.set(t[y], y++)
            }
            const a = g.get(n[o]);
            if (a != null)
                if (r < a && a < l) {
                    let y = o,
                        A = 1,
                        D;
                    for (; ++y < i && y < l && !((D = g.get(n[y])) == null || D !== a + A);)
                        A++;
                    if (A > a - r) {
                        const K = n[o];
                        for (; r < a;)
                            e.insertBefore(t[r++], K)
                    } else
                        e.replaceChild(t[r++], n[o++])
                } else
                    o++;
            else
                n[o++].remove()
        }
    }
}
const U = "_$DX_DELEGATE";
function ie(e, n, t) {
    let s;
    return k(i => {
        s = i,
        n === document ? e() : V(n, e(), n.firstChild ? null : void 0, t)
    }), () => {
        s(),
        n.textContent = ""
    }
}
function H(e, n, t) {
    const s = document.createElement("template");
    s.innerHTML = e;
    let i = s.content.firstChild;
    return t && (i = i.firstChild), i
}
function le(e, n=window.document) {
    const t = n[U] || (n[U] = new Set);
    for (let s = 0, i = e.length; s < i; s++) {
        const l = e[s];
        t.has(l) || (t.add(l), n.addEventListener(l, oe))
    }
}

function V(e, n, t, s) {
    if (t !== void 0 && !s && (s = []), typeof n != "function")
        return E(e, n, s, t);
    T(i => E(e, n(), i, t), s)
}

function oe(e) {
    const n = `$$${e.type}`;
    let t = e.composedPath && e.composedPath()[0] || e.target;
    for (e.target !== t && Object.defineProperty(e, "target", {
        configurable: !0,
        value: t
    }), Object.defineProperty(e, "currentTarget", {
        configurable: !0,
        get() {
            return t || document
        }
    }), p.registry && !p.done && (p.done = !0, document.querySelectorAll("[id^=pl-]").forEach(s => s.remove())); t !== null;) {
        const s = t[n];
        if (s && !t.disabled) {
            const i = t[`${n}Data`];
            if (i !== void 0 ? s.call(t, i, e) : s.call(t, e), e.cancelBubble)
                return
        }
        t = t.host && t.host !== t && t.host instanceof Node ? t.host : t.parentNode
    }
}
function E(e, n, t, s, i) {
    for (p.context && !t && (t = [...e.childNodes]); typeof t == "function";)
        t = t();
    if (n === t)
        return t;
    const l = typeof n,
        o = s !== void 0;
    if (e = o && t[0] && t[0].parentNode || e, l === "string" || l === "number") {
        if (p.context)
            return t;
        if (l === "number" && (n = n.toString()), o) {
            let r = t[0];
            r && r.nodeType === 3 ? r.data = n : r = document.createTextNode(n),
            t = b(e, t, s, r)
        } else
            t !== "" && typeof t == "string" ? t = e.firstChild.data = n : t = e.textContent = n
    } else if (n == null || l === "boolean") {
        if (p.context)
            return t;
        t = b(e, t, s)
    } else {
        if (l === "function")
            return T(() => {
                let r = n();
                for (; typeof r == "function";)
                    r = r();
                t = E(e, r, t, s)
            }), () => t;
        if (Array.isArray(n)) {
            const r = [];
            if (v(r, n, i))
                return T(() => t = E(e, r, t, s, !0)), () => t;
            if (p.context) {
                for (let d = 0; d < r.length; d++)
                    if (r[d].parentNode)
                        return t = r
            }
            if (r.length === 0) {
                if (t = b(e, t, s), o)
                    return t
            } else
                Array.isArray(t) ? t.length === 0 ? q(e, r, s) : se(e, t, r) : (t && b(e), q(e, r));
            t = r
        } else if (n instanceof Node) {
            if (p.context && n.parentNode)
                return t = o ? [n] : n;
            if (Array.isArray(t)) {
                if (o)
                    return t = b(e, t, s, n);
                b(e, t, null, n)
            } else
                t == null || t === "" || !e.firstChild ? e.appendChild(n) : e.replaceChild(n, e.firstChild);
            t = n
        }
    }
    return t
}
function v(e, n, t) {
    let s = !1;
    for (let i = 0, l = n.length; i < l; i++) {
        let o = n[i],
            r;
        if (o instanceof Node)
            e.push(o);
        else if (!(o == null || o === !0 || o === !1))
            if (Array.isArray(o))
                s = v(e, o) || s;
            else if ((r = typeof o) == "string")
                e.push(document.createTextNode(o));
            else if (r === "function")
                if (t) {
                    for (; typeof o == "function";)
                        o = o();
                    s = v(e, Array.isArray(o) ? o : [o]) || s
                } else
                    e.push(o),
                    s = !0;
            else
                e.push(document.createTextNode(o.toString()))
    }
    return s
}
function q(e, n, t) {
    for (let s = 0, i = n.length; s < i; s++)
        e.insertBefore(n[s], t)
}
function b(e, n, t, s) {
    if (t === void 0)
        return e.textContent = "";
    const i = s || document.createTextNode("");
    if (n.length) {
        let l = !1;
        for (let o = n.length - 1; o >= 0; o--) {
            const r = n[o];
            if (i !== r) {
                const d = r.parentNode === e;
                !l && !o ? d ? e.replaceChild(i, r) : e.insertBefore(i, t) : d && r.remove()
            } else
                l = !0
        }
    } else
        e.insertBefore(i, t);
    return [i]
}
const re = H("<button>up</button>"),
    fe = H("<div>Count: <b></b></div>"),
    ue = () => {
        const [e, n] = J(0);
        return [
          (() => {
            const t = re.cloneNode(!0);
            return t.$$click = () => n(e() + 1), t
          })(),
          (() => {
            const t = fe.cloneNode(!0),
                s = t.firstChild,
                i = s.nextSibling;
            return V(i, e), t
          })()]
    };
le(["click"]);
ie(() => ne(ue, {}), document.getElementById("root"));

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ