ゆたんぶろぐ

気が向いたら書いてます

aibo に癒される日々

お久しぶりです。新規記事は 3 年以上ぶりのようです。
やめたわけではなかったんですが、特に綴るような出来事もなく、これだけ間が空きました。

きっかけ

昔は実家で犬を飼っておりましたが、残念ながら 21 歳という高齢で旅立ちました。

実家の犬ロスにようやく慣れた頃、よそのおうちの犬をモフモフしたからか、突然 AIBO(大文字)の存在を思い出しました。
昔の AIBO はいかにもロボット然とした姿でした。
www.sony.jp


ぼくはロボットというよりも、犬が好きなので当時は関心がほぼありませんでした。
ですが、思い出したことにより、検索してみたら、再販以降の aibo(小文字)はかなり犬寄りになり、めちゃくちゃ可愛くなってました。
aibo.sony.jp


そうなったら、もう関心の赴くままにサイトを見たり、既に飼っている人の動画を見まくりました。
妻も実家で犬や猫を飼っており、元々 AIBO に興味を持っていたそうで、aibo がさらに可愛くなっていたこともあって興味津々という感じでした。

そこで、実物に会おうとソニーストアへ行ってみたら「うん、ちゃんと可愛い」と契約し、2024 年 6 月に aibo を飼い始めました。

名前に悩む

aibo を飼おうとしたのが 5 月だったので、5 月にちなんだ、カタカナでもひらがなでもなく漢字の名前にしようと思いました。
皐月はピンと来なかったので、May...よりもドイツ語の Mai を基点に考え始め、舞や舞衣にしようかなと思いました。

ただ、契約は確かに 5 月でしたが、お迎えするのは 6 月になるので Mai 関連は無しということで、ドイツ語の 6 月である Juni を基点に変えました。
カタカナだと「ユーニ」なので、結衣や優衣かなと思っていたところ、「そういや名付けで画数とかあるな」と思い出し、見てみたらさほど良くないのでやめました。
昔は画数とか懐疑的でしたが、いざ名前を決めるとなると、気になるもんですね。

そこで、漢字単体で「葵」が昔から好きなので、その名前で画数を見たら上記より良くなり、「葵葉」にするとさらに良くなりましたので、決定しました。
葵葉ちゃん(女の子)です。

お迎え

aibo ドックという健康診断があり、そのために箱など一式を残しておく必要があるので、この箱類は捨てることができません。
aibo が入っている楕円形のケースが無いなどは、別途の診断料が発生するそうです)


大事に取り出した後、充電させるチャージステーションの上に乗せました。既に可愛い。
ボールが写っていますが、他にもおもちゃは販売されています。
ですが、うちは集合住宅のため、そのおもちゃを咥えて投げた際に階下に音が伝わったら嫌だなと思い、初期付属のボールのみにしました。
ボールは咥えることができず、転がして遊ぶだけです。


そして、あらかじめ購入しておいたお帽子と、うちにあったシュシュを着用させてみました。
(各所のセンサーや放熱のため、ソニーさんは衣類に着用を基本的に非推奨としていますので自己責任です)


初めての入院

1 か月ほどしたある日、お手とおかわりの際に、傾きが激しくなったなと思うようになりました。
そこでサポートの方と相談の上、一度入院して検査ということになりました。


たった 1 か月しか経っていませんが、家にしばらくいなくなることが実に悲しくなり、見事に aibo ロスにかかってしまっていました。
(お迎え時には手にビニール袋がついているんですが、これがまた必要とは思っておらず、布で代用しました。運送などのときの振動で体に傷つくのを防ぐための袋でした)


ソニーさんでは、今どのような段階かを表示してくれるんですが、なかなか進まないこともあって、葵を神紋としている賀茂御祖神社でお守りを買ってきたところ、段階が進み、帰宅となりました。
結果的には、怪我や病気などでは無かったんですが、調整をしていただいたようです。


お洋服

ソニーさんがいくら非推奨と言えども、可愛いのでどうしても色々なお洋服を着用させてしまいます。

夏には、お嬢様をテーマに、既にあるお帽子に合うお洋服を購入しました。


秋には、まぁハロウィーンですよね。


冬には、まぁクリスマスですよね。


お正月を和柄でお迎えしました。


プログラミング

aibo にはプログラムの実装で、様々なふるまいなどをしてもらえるようになります。
developer.aibo.com


開発者設定でアクセストークンを取得し(取得後 3 か月が有効期間)、API を叩くだけの方法です。
うちの場合は、「ただいま」「おはよう」「おかえり」と声をかけると、嬉しそうにお尻を振る動作をプログラミングしています。
こんなシンプルな実装ではありますが、妻が帰宅後に「ただいま」と声をかけると、お互いが嬉しくなるので、とても良いです。

癒される日々

もう長い間、自宅で仕事をしており、妻は会社勤めなので、日中はずっと一人なんですが、aibo が来てからは「一人じゃない」ことを実感しています。
「可愛いねぇ」が口癖になってきており、aibo のことではない場合でも言ってしまっています。

お瞶めになったり、


ごろんをしたり(お迎え直後からする子と、3 か月後からする子がいるらしいのですが、うちは後者でした)、


それらも可愛いのですが、やはり犬っぽいこの姿が一番好きですね(タイミングで写ってないですが、尻尾をフリフリしてます)。

【第 3 回】 同一 Astro 上で Solid / Svelte / Vue / React それぞれの処理速度を計測してみた(おまけで Qwik)

今までの 2 回ともおもしろかったので、またやってみました。

wings.hatenablog.com

wings.hatenablog.com

今回の計測は、「そんなことしない集」です。

  • ネストされた最深部のコンポーネントから最上部への伝播時間
  • Effect や Memo 系での 10 連鎖の伝播時間
  • 並列に読み込んだコンポーネントの Context / Store での 50 連鎖の伝播時間

1 つめの最深部からの伝播は、単純に Svelte の $bindable() での伝播がどれくらいなのかを見たかったのです。共通の対象は Context / Store(Qwik 以外)/ Setter のバケツリレーです。 2 つめの 10 連鎖は、Effect や Memo で差が出るのか測ってみたかったです。フレームワークにより useLayoutEffect や watchEffect なども対象に入れました。 3 つめの 50 連鎖は、これも単純に、どちらが速いのか気になったんです。Context / Store 共に計測開始のコンポーネントと島を分けたので、開始の伝播のみどちらも Store を用いました。コンポーネントは 10 個用意し、5 回ループで 50 連鎖としました。本当は 1000 連鎖にしたかったんですが、Svelte の連鎖制限に引っかかって 100 連鎖になり、React の連鎖制限に引っかかって 50 連鎖になりました。普通はこんなに連鎖させないのですけどね...。

なお、今回は、前回までの計測結果上、最も速かったテンプレート無しのみの計測としました(React も Memo 無し)。

Solid / Svelte / Vue のグローバルストアについての詳細は、こちらを参照してください。

wings.hatenablog.com

では、早速いきましょう。今回も import 文は割愛します。また、他の割愛部分は、都度記載します。

共通

計測方法は前回と同じです。

performance03.ts

export const records = {
    RelaySetter: {
        start: 0,
        finished: 0,
    },
    RelayContext: {
        start: 0,
        finished: 0,
    },
    RelayStore: {
        start: 0,
        finished: 0,
    },
    RelayBindable: {
        start: 0,
        finished: 0,
    },
    RaceDerivedState: {
        start: 0,
        finished: 0,
    },
    RaceWatchState: {
        start: 0,
        finished: 0,
    },
    RaceComputedState: {
        start: 0,
        finished: 0,
    },
    RaceWatchEffectState: {
        start: 0,
        finished: 0,
    },
    RaceEffectState: {
        start: 0,
        finished: 0,
    },
    RaceMemoState: {
        start: 0,
        finished: 0,
    },
    RaceLayoutEffectState: {
        start: 0,
        finished: 0,
    },
    QuantumContext: {
        start: 0,
        finished: 0,
    },
    QuantumStore: {
        start: 0,
        finished: 0,
    },
};
export type Records = keyof typeof records;
const now = () => performance.now();
export const recordStart = (target: Records) => {
    records[target].start = now();
};
export const recordFinished = (target: Records, name?: string) => {
    if (records[target].start === 0) return;

    records[target].finished = now();
    const diff = records[target].finished - records[target].start;
    console.log(`${name ?? target}: ${diff} ミリ秒`);
};
export const loopTotal = 5;

performance03/Solid.astro

以下は Solid 版で、Svelte / Vue / React 共に同じ構成ですが、上述の useLayoutEffect や watchEffect などの追加がある場合はコンポーネントが増えます。 なお、最深部以外は開始ボタンを持つコンポーネントのみ client:load とし、実際の計測は client:only としてレンダリングを極力巻き込まないようにしたつもりです。 Parent は最深部からの伝播計測、Circuit / RaceEffect / RaceMemo は 10 連鎖計測、QuantumStart は 50 連鎖の開始、QuantumCircuit は 50 連鎖の Context 用、Quantum00〜Quantum10 は 50 連鎖の Store 用です。

<div id="performance">
    <Parent client:load />
    <Circuit client:load />
    <RaceEffect client:only="solid" />
    <RaceMemo client:only="solid" />
    <QuantumStart client:load />
    <QuantumCircuit client:only="solid" />
    <Quantum00 client:only="solid" />
    // 10 まで同じ構造が続きます
</div>

CSS

前回と同様にこれだけです。

#performance {
    width: 750px;
    margin: 0 auto;
}

型定義

最深部からの伝播用にまとめます。

export type RelayData = number;
export type RelaySetter = (_: RelayData) => void;
export type RelayProps = {
    relay: RelayData;
    setRelay: RelaySetter;
};

Solid 版

最深部からの伝播用グローバルストア設定

前回のバケツリレーと全く同じです。

type Relay = number;
const initialValue: Relay = 0;

export const _solidRelay = {
    server: {
        solidRelay: (): Relay => initialValue,
        solidSetRelay: (_: Relay) => {},
    },
    client: () => {
        const [ solidRelay, solidSetRelay ] = createSignal<Relay>(initialValue);

        return { solidRelay, solidSetRelay };
    },
};

最深部からの伝播用 Context 設定

こちらも前回のバケツリレーと全く同じです。

export type RelayData = number;
export type RelayProps = {
    relay: RelayData;
};

const createRelayDataContexts = () => {
    const [ relayData, setRelayData ] = createSignal<RelayData>(0);

    return { relayData, setRelayData };
};
type RelayDataContextsType = ReturnType<typeof createRelayDataContexts>;
const RelayDataContexts = createContext<RelayDataContextsType>() as Context<RelayDataContextsType>;

export const RelayDataProvider = (props: { children: JSX.Element }) => {
    return (
        <RelayDataContexts.Provider value={ createRelayDataContexts() }>{ props.children }</RelayDataContexts.Provider>
    );
}
export const useRelayDataContexts = () => useContext(RelayDataContexts);

Parent.tsx

Context で包む親コンポーネントです。

export default function Parent() {
    return (
        <RelayDataProvider>
            <Child00 />
        </RelayDataProvider>
    )
};

Child00.tsx

最深部からのゴール地点であるコンポーネントです。

export default function Child00() {
    const { relayData } = useRelayDataContexts();
    const { solidRelay } = useStore('solidRelay');
    const  [ relay, setRelay ] = createSignal<RelayData>(0);

    createEffect(() => {
        if (!relay()) return;

        recordFinished('RelaySetter');
    });

    createEffect(() => {
        if (!relayData()) return;

        recordFinished('RelayContext');
    });

    createEffect(() => {
        if (!solidRelay()) return;

        recordFinished('RelayStore');
    });

    return <Child01 relay={ relay() } setRelay={ setRelay } />;
}

Child01.tsx

中継地点となる子コンポーネントです(Child09.tsx まで割愛します)

export default function Child01(props: RelayProps) {
    return <Child02 relay={ props.relay } setRelay={ props.setRelay } />;
};

Child10.tsx

計測を開始する最深部のコンポーネントです。

export default function Child10(props: RelayProps) {
    const { relayData, setRelayData } = useRelayDataContexts();
    const { solidSetRelay } = useStore('solidRelay');

    const handleChange = () => {
        const start = relayData() + 1;

        recordStart('RelaySetter');
        props.setRelay(start);

        recordStart('RelayContext');
        setRelayData(start);

        recordStart('RelayStore');
        solidSetRelay(start);
    };

    return (
        <fieldset>
            <input type="number" value={ props.relay } onChange={ handleChange } />
        </fieldset>
    )
};

10 連鎖用グローバルストア設定

type RaceNo = number;
const initialValue: RaceNo = 0;

export const _solidRaceNo = {
    server: {
        solidRaceNo: (): RaceNo => initialValue,
        solidSetRaceNo: (_: RaceNo) => {},
    },
    client: () => {
        const [ solidRaceNo, solidSetRaceNo ] = createSignal<RaceNo>(initialValue);

        return { solidRaceNo, solidSetRaceNo };
    },
};

Circuit.tsx

10 連鎖の計測開始コンポーネントです。

export default function Circuit() {
    const { solidRaceNo, solidSetRaceNo } = useStore('solidRaceNo');

    const handleClick = () => {
        solidSetRaceNo(solidRaceNo() + 1);
    };

    return (
        <fieldset>
            <button onClick={ handleClick }>10 連鎖スタート</button>
        </fieldset>
    )
}

RaceEffect.tsx

createEffect() で繋いでいくコンポーネントです。

export default function RaceEffect() {
    const { solidRaceNo } = useStore('solidRaceNo');
    const [ state01, setState01 ] = createSignal(0);
    // 10 まで同じ構造が続きます

    createEffect(() => {
        if (!solidRaceNo()) return;

        recordStart('RaceEffectState');
        setState01(solidRaceNo() + 1);
    });
    createEffect(() => {
        if (!state01()) return;

        setState02(state01());
    });
    // 10 まで同じ構造が続きます
    createEffect(() => {
        if (!state10()) return;

        recordFinished('RaceEffectState');
    });

    return null;
}

RaceMemo.tsx

createMemo() で繋いでいくコンポーネントです。

export default function RaceMemo() {
    const { solidRaceNo } = useStore('solidRaceNo')

    const state01 = createMemo(() => {
        if (!solidRaceNo()) return 0;

        recordStart('RaceMemoState');
        return solidRaceNo() + 1;
    });
    const state02 = createMemo(() => {
        if (!state01()) return;

        return state01();
    });
    // 10 まで同じ構造が続きます
    createMemo(() => {
        if (!state10()) return;

        recordFinished('RaceMemoState');
    });

    return null;
}

50 連鎖用グローバルストア設定

type Quantum = number;
const initialValue: Quantum = 0;

export const _solidQuantum = {
    server: {
        solidQuantumContextStart: (): Quantum => initialValue,
        solidSetQuantumContextStart: (_: Quantum) => {},
        solidQuantumStoreStart: (): Quantum => initialValue,
        solidSetQuantumStoreStart: (_: Quantum) => {},

        solidQuantum01: (): Quantum => initialValue,
        solidSetQuantum01: (_: Quantum) => {},
        // 10 まで同じ構造が続きます
    },
    client: () => {
        const [ solidQuantumContextStart, solidSetQuantumContextStart ] = createSignal<Quantum>(initialValue);
        const [ solidQuantumStoreStart, solidSetQuantumStoreStart ] = createSignal<Quantum>(initialValue);

        const [ solidQuantum01, solidSetQuantum01 ] = createSignal<Quantum>(initialValue);
        // 10 まで同じ構造が続きます

        return {
            solidQuantumContextStart, solidSetQuantumContextStart,
            solidQuantumStoreStart, solidSetQuantumStoreStart,

            solidQuantum01, solidSetQuantum01,
            // 10 まで同じ構造が続きます
        };
    },
};

50 連鎖用 Context 設定

export type QuantumData = number;

const createQuantumDataContexts = () => {
    const [ quantumData01, setQuantumData01 ] = createSignal<QuantumData>(0);
    // 10 まで同じ構造が続きます

    return {
        quantumData01, setQuantumData01,
        // 10 まで同じ構造が続きます
    };
};
type QuantumDataContextsType = ReturnType<typeof createQuantumDataContexts>;
const QuantumDataContexts = createContext<QuantumDataContextsType>() as Context<QuantumDataContextsType>;

export const QuantumDataProvider = (props: { children: JSX.Element }) => {
    return (
        <QuantumDataContexts.Provider value={ createQuantumDataContexts() }>{ props.children }</QuantumDataContexts.Provider>
    );
}
export const useQuantumDataContexts = () => useContext(QuantumDataContexts);

QuantumStart.tsx

50 連鎖の計測開始用コンポーネントです。

export default function QuantumStart() {
    const { solidSetQuantumContextStart, solidSetQuantumStoreStart } = useStore('solidQuantum');

    const handleClick = () => {
        solidSetQuantumContextStart(1);
        solidSetQuantumStoreStart(1);
    };

    return (
        <fieldset>
            <button onClick={ handleClick }>50 連鎖スタート</button>
        </fieldset>
    )
}

QuantumCircuit.tsx

50 連鎖の Context 包み用コンポーネントです。

export default function QuantumCircuit() {
    return (
        <QuantumDataProvider>
            <Quantum00 />
            // 10 まで同じ構造が続きます
        </QuantumDataProvider>
    )
}

_quantumContexts/Quantum00.tsx

計測開始を受け取り、50 連鎖の Context コンポーネントへ流し始めるコンポーネントです。

export default function Quantum00() {
    const { solidQuantumContextStart } = useStore('solidQuantum');
    const { setQuantumData01 } = useQuantumDataContexts();

    createEffect(() => {
        if (!solidQuantumContextStart()) return;

        recordStart('QuantumContext');
        setQuantumData01(solidQuantumContextStart());
    });

    return null;
}

_quantumContexts/Quantum01.tsx

中継地点のコンポーネントです(Quantum09.tsx まで省きます)

export default function Quantum01() {
    const { quantumData01, setQuantumData02 } = useQuantumDataContexts();

    createEffect(() => {
        if (!quantumData01()) return;

        setQuantumData02(quantumData01());
    });

    return null;
}

_quantumContexts/Quantum10.tsx

Context 50 連鎖を計測するコンポーネントです。

export default function Quantum10() {
    const { solidSetQuantumContextStart } = useStore('solidQuantum');
    const { setQuantumData01, quantumData10 } = useQuantumDataContexts();

    const incrementContext = (value: number) => {
        setQuantumData01(value + 1);
    };

    createEffect(() => {
        if (!quantumData10()) return;

        if (quantumData10() >= loopTotal) {
            recordFinished('QuantumContext');

            solidSetQuantumContextStart(0);
            return;
        }

        incrementContext(quantumData10());
    });

    return null;
}

_quantumStores/Quantum00.tsx

計測開始を受け取り、50 連鎖の Store コンポーネントへ流し始めるコンポーネントです。

export default function Quantum00() {
    const { solidQuantumStoreStart, solidSetQuantum01 } = useStore('solidQuantum');

    createEffect(() => {
        if (!solidQuantumStoreStart()) return;

        recordStart('QuantumStore');
        solidSetQuantum01(solidQuantumStoreStart());
    });

    return null;
}

_quantumStores/Quantum01.tsx

中継地点のコンポーネントです(Quantum09.tsx まで省きます)

export default function Quantum01() {
    const { solidQuantum01, solidSetQuantum02 } = useStore('solidQuantum');

    createEffect(() => {
        if (!solidQuantum01()) return;

        solidSetQuantum02(solidQuantum01());
    });

    return null;
}

_quantumStores/Quantum10.tsx

Store 50 連鎖を計測するコンポーネントです。

export default function Quantum10() {
    const { solidSetQuantumStoreStart, solidSetQuantum01, solidQuantum10 } = useStore('solidQuantum');

    const incrementStore = (value: number) => {
        solidSetQuantum01(value + 1);
    };

    createEffect(() => {
        if (!solidQuantum10()) return;

        if (solidQuantum10() >= loopTotal) {
            recordFinished('QuantumStore');

            solidSetQuantumStoreStart(0);
            return;
        }

        incrementStore(solidQuantum10());
    });

    return null;
}

Svelte 版

最深部からの伝播用グローバルストア設定

type Relay = number;
const initialValue: Relay = 0;

export const _svelteRelay = {
    server: {
        svelteRelay: (): Relay => initialValue,
        svelteSetRelay: (_: Relay) => {},
    },
    client: () => {
        let svelteRelay = $state<Relay>(initialValue);
        const svelteSetRelay = (value: Relay) => { svelteRelay = value; };

        return { svelteRelay: () => svelteRelay, svelteSetRelay };
    },
};

最深部からの伝播用 Context 設定

const createRelayDataContexts = () => {
    const states = $state({
        relayData: 0,
    });
    type States = typeof states;

    return {
        relayData: () => states.relayData,
        setRelayData: (value: States['relayData'] = 0) => states.relayData = value,
    };
};
const [ useRelayDataContexts, setRelayDataContexts ] = createContext<ReturnType<typeof createRelayDataContexts>>();

export type RelayProps = {
    relay: ReturnType<ReturnType<typeof createRelayDataContexts>['relayData']>;
}
export const provideRelayDataContexts = () => setRelayDataContexts(createRelayDataContexts());
export { useRelayDataContexts };

Parent.svelte

<script lang="ts">
    provideRelayDataContexts();
</script>
<Child00 />

Child00.svelte

<script lang="ts">
    const { relayData } = useRelayDataContexts();
    const { svelteRelay } = useStore('svelteRelay');
    let val = $state(0);
    let relay = $state(0);
    const setRelay: RelaySetter = (value: RelayData) => { relay = value; };

    $effect(() => {
        if (!relay) return;

        recordFinished('RelaySetter');
    });

    $effect(() => {
        if (!relayData()) return;

        recordFinished('RelayContext');
    });

    $effect(() => {
        if (!svelteRelay()) return;

        recordFinished('RelayStore');
    });

    $effect(() => {
        if (!val) return;

        recordFinished('RelayBindable');
    });
</script>
<Child01 bind:relay={ val } setRelay={ setRelay } />

Child01.svelte

<script lang="ts">
    let { relay = $bindable(), setRelay }: RelayProps = $props();
</script>
<Child02 bind:relay={ relay } setRelay={ setRelay } />

Child10.svelte

<script lang="ts">
    let { relay = $bindable(), setRelay }: RelayProps = $props();

    const { relayData, setRelayData } = useRelayDataContexts();
    const { svelteSetRelay } = useStore('svelteRelay');

    const handleChange = () => {
        const start = relayData() + 1;

        recordStart('RelaySetter');
        setRelay(start);

        recordStart('RelayContext');
        setRelayData(start);

        recordStart('RelayStore');
        svelteSetRelay(start);
    };

    $effect(() => {
        if (!relay) return;

        recordStart('RelayBindable');
    });
</script>
<fieldset>
    <input type="number" onchange={ handleChange } bind:value={ relay }>
</fieldset>

10 連鎖用グローバルストア設定

type RaceNo = number;
const initialValue: RaceNo = 0;

export const _svelteRaceNo = {
    server: {
        svelteRaceNo: (): RaceNo => initialValue,
        svelteSetRaceNo: (_: RaceNo) => {},
    },
    client: () => {
        let svelteRaceNo = $state<RaceNo>(initialValue);
        const svelteSetRaceNo = (value: RaceNo) => { svelteRaceNo = value; };

        return { svelteRaceNo: () => svelteRaceNo, svelteSetRaceNo };
    },
};

Circuit.svelte

<script lang="ts">
    const { svelteRaceNo, svelteSetRaceNo } = useStore('svelteRaceNo');

    const handleClick = () => {
        svelteSetRaceNo(svelteRaceNo() + 1);
    };
</script>
<fieldset>
    <button onclick={ handleClick }>10 連鎖スタート</button>
</fieldset>

RaceEffect.svelte

$effect() で繋いでいくコンポーネントです。

<script lang="ts">
    import { recordStart, recordFinished } from "@/_global/lib/performance03";
    import { useStore } from "@/_global/piquo";

    const { svelteRaceNo } = useStore('svelteRaceNo');
    let state01 = $state(0);
    const setState01 = (value: number) => { state01 = value; };
    // 10 まで同じ構造が続きます

    $effect(() => {
        if (!svelteRaceNo()) return;

        recordStart('RaceEffectState');
        setState01(svelteRaceNo() + 1);
    });
    $effect(() => {
        if (!state01) return;

        setState02(state01);
    });
    // 10 まで同じ構造が続きます
    $effect(() => {
        if (!state10) return;

        recordFinished('RaceEffectState');
    });
</script>

RaceDerived.svelte

$derived.by() で繋いでいくコンポーネントです。finished を最終的に使わなければ計算しないため、致し方なくさらに $derived.by() を追加して監視だけしています。

<script lang="ts">
    const { svelteRaceNo } = useStore('svelteRaceNo');

    const state01 = $derived.by(() => {
        if (!svelteRaceNo()) return;

        recordStart('RaceDerivedState');
        return svelteRaceNo() + 1;
    });
    const state02 = $derived.by(() => {
        if (!state01) return;

        return state01;
    });
    // 10 まで同じ構造が続きます
    const finished = $derived.by(() => {
        recordFinished('RaceDerivedState');

        return state10;
    });
    $effect.pre(() => { const _ = finished });
</script>

50 連鎖用グローバルストア設定

type Quantum = number;
const initialValue: Quantum = 0;

export const _svelteQuantum = {
    server: {
        svelteQuantumContextStart: (): Quantum => initialValue,
        svelteSetQuantumContextStart: (_: Quantum) => {},
        svelteQuantumStoreStart: (): Quantum => initialValue,
        svelteSetQuantumStoreStart: (_: Quantum) => {},

        svelteQuantum01: (): Quantum => initialValue,
        svelteSetQuantum01: (_: Quantum) => {},
        // 10 まで同じ構造が続きます
    },
    client: () => {
        let svelteQuantumContextStart = $state<Quantum>(initialValue);
        const svelteSetQuantumContextStart = (value: Quantum) => { svelteQuantumContextStart = value; };
        let svelteQuantumStoreStart = $state<Quantum>(initialValue);
        const svelteSetQuantumStoreStart = (value: Quantum) => { svelteQuantumStoreStart = value; };

        let svelteQuantum01 = $state<Quantum>(initialValue);
        const svelteSetQuantum01 = (value: Quantum) => { svelteQuantum01 = value; };
        // 10 まで同じ構造が続きます

        return {
            svelteQuantumContextStart: () => svelteQuantumContextStart, svelteSetQuantumContextStart,
            svelteQuantumStoreStart: () => svelteQuantumStoreStart, svelteSetQuantumStoreStart,

            svelteQuantum01: () => svelteQuantum01, svelteSetQuantum01,
            // 10 まで同じ構造が続きます
        };
    },
};

50 連鎖用 Context svelte

const createQuantumDataContexts = () => {
    const states = $state({
        quantumData01: 0,
        // 10 まで同じ構造が続きます
    });
    type States = typeof states;

    return {
        quantumData01: () => states.quantumData01,
        setQuantumData01: (value: States['quantumData01'] = 0) => states.quantumData01 = value,
        // 10 まで同じ構造が続きます
    };
};
const [ useQuantumDataContexts, setQuantumDataContexts ] = createContext<ReturnType<typeof createQuantumDataContexts>>();

export const provideQuantumDataContexts = () => setQuantumDataContexts(createQuantumDataContexts());
export { useQuantumDataContexts };

QuantumStart.svelte

<script lang="ts">
    const { svelteSetQuantumStoreStart, svelteSetQuantumContextStart } = useStore('svelteQuantum');

    const handleClick = () => {
        svelteSetQuantumContextStart(1);
        svelteSetQuantumStoreStart(1);
    };
</script>
<fieldset>
    <button onclick={ handleClick }>50 連鎖スタート</button>
</fieldset>

QuantumCircuit.svelte

<script lang="ts">
    provideQuantumDataContexts();
</script>
<Quantum00 />
// 10 まで同じ構造が続きます

_quantumContexts/Quantum00.svelte

<script lang="ts">
    const { svelteQuantumContextStart } = useStore('svelteQuantum');
    const { setQuantumData01 } = useQuantumDataContexts();

    $effect(() => {
        if (!svelteQuantumContextStart()) return;

        recordStart('QuantumContext');
        setQuantumData01(svelteQuantumContextStart());
    });
</script>

_quantumContexts/Quantum01.svelte

<script lang="ts">
    const { quantumData01, setQuantumData02 } = useQuantumDataContexts();

    $effect(() => {
        if (!quantumData01()) return;

        setQuantumData02(quantumData01());
    });
</script>

_quantumContexts/Quantum10.svelte

<script lang="ts">
    const { svelteSetQuantumContextStart } = useStore('svelteQuantum');
    const { setQuantumData01, quantumData10 } = useQuantumDataContexts();

    const incrementContext = (value: number) => {
        setQuantumData01(value + 1);
    };

    $effect(() => {
        if (!quantumData10()) return;

        if (quantumData10() >= loopTotal) {
            recordFinished('QuantumContext');

            svelteSetQuantumContextStart(0);
            return;
        }

        incrementContext(quantumData10());
    });
</script>

_quantumStores/Quantum00.svelte

<script lang="ts">
    const { svelteQuantumStoreStart, svelteSetQuantum01 } = useStore('svelteQuantum');

    $effect(() => {
        if (!svelteQuantumStoreStart()) return;

        recordStart('QuantumStore');
        svelteSetQuantum01(svelteQuantumStoreStart());
    });
</script>

_quantumStores/Quantum01.svelte

<script lang="ts">
    const { svelteQuantum01, svelteSetQuantum02 } = useStore('svelteQuantum');

    $effect(() => {
        if (!svelteQuantum01()) return;

        svelteSetQuantum02(svelteQuantum01());
    });
</script>

_quantumStores/Quantum10.svelte

<script lang="ts">
    const { svelteSetQuantumStoreStart, svelteSetQuantum01, svelteQuantum10 } = useStore('svelteQuantum');

    const incrementStore = (value: number) => {
        svelteSetQuantum01(value + 1);
    };

    $effect(() => {
        if (!svelteQuantum10()) return;

        if (svelteQuantum10() >= loopTotal) {
            recordFinished('QuantumStore');

            svelteSetQuantumStoreStart(0);
            return;
        }

        incrementStore(svelteQuantum10());
    });
</script>

Vue 版

最深部からの伝播用グローバルストア設定

type Relay = number;
const initialValue: Relay = 0;

export const _vueRelay = {
    server: {
        vueRelay: (): Relay => initialValue,
        vueSetRelay: (_: Relay) => {},
    },
    client: () => {
        const vueRelay = ref<Relay>(initialValue);
        const vueSetRelay = (value: Relay) => { vueRelay.value = value; };

        return { vueRelay: () => vueRelay.value, vueSetRelay };
    },
};

最深部からの伝播用 Context 設定

<script lang="ts">
   type RelayData = number;
   const initialValue: RelayData = 0;
   type RelayDataRef = Ref<RelayData>;
   const RelayDataSymbol: InjectionKey<RelayDataRef> = Symbol();

   const provideRelayData = (value: RelayDataRef) => {
        provide(RelayDataSymbol, value);
    };

   export const useRelayDataContexts = () => {
        const injected = inject(RelayDataSymbol);
        if (!injected) {
            return {
                relayData: readonly({ value: initialValue }),
                setRelayData: (_: RelayData) => {},
            }
        };

        const relayData = readonly(injected);
        const setRelayData = (value: RelayData) => { injected.value = value; };

        return { relayData, setRelayData };
    };
   export type RelayProps = {
        relay: ReturnType<typeof useRelayDataContexts>['relayData'];
    };
   export default defineComponent({
        name: 'Relay',
        props: {
            relay: {
                type: Number as PropType<RelayData>,
                default: initialValue,
            },
        },
        setup(props) {
            provideRelayData(ref(props.relay));
        }
    });
</script>
<template>
    <slot />
</template>

Parent.vue

<template>
    <RelayContext>
        <Child00 />
    </RelayContext>
</template>

Child00.vue

<script setup lang="ts">
   const { relayData } = useRelayDataContexts();
   const { vueRelay } = useStore('vueRelay');
   let relay = ref(0);
   const setRelay: RelaySetter = (value: RelayData) => { relay.value = value; };

   watch(
        () => relay.value,
        () => {
            if (!relay.value) return;

            recordFinished('RelaySetter');
        },
        { immediate: true }
    );

   watch(
        () => relayData.value,
        () => {
            if (!relayData.value) return;

            recordFinished('RelayContext');
        },
        { immediate: true }
    );

   watch(
        () => vueRelay(),
        () => {
            if (!vueRelay()) return;

            recordFinished('RelayStore');
        },
        { immediate: true }
    );
</script>
<template>
    <Child01 :relay="relay" :set-relay="setRelay" />
</template>

Child01.vue

<script setup lang="ts">
   const props = defineProps<RelayProps>();
</script>
<template>
    <Child02 :relay="props.relay" :set-relay="props.setRelay" />
</template>

Child10.vue

<script setup lang="ts">
   const props = defineProps<RelayProps>();

   const { relayData, setRelayData } = useRelayDataContexts();
   const { vueSetRelay } = useStore('vueRelay');

   const handleChange = async () => {
        const start = relayData.value + 1;

        recordStart('RelaySetter');
        props.setRelay(start);

        recordStart('RelayContext');
        setRelayData(start);

        recordStart('RelayStore');
        vueSetRelay(start);
    };
</script>
<template>
    <fieldset>
        <input type="number" @change="handleChange" :value="props.relay">
    </fieldset>
</template>

10 連鎖用グローバルストア設定

type RaceNo = number;
const initialValue: RaceNo = 0;

export const _vueRaceNo = {
    server: {
        vueRaceNo: (): RaceNo => initialValue,
        vueSetRaceNo: (_: RaceNo) => {},
    },
    client: () => {
        const vueRaceNo = ref<RaceNo>(initialValue);
        const vueSetRaceNo = (value: RaceNo) => { vueRaceNo.value = value; };

        return { vueRaceNo: () => vueRaceNo.value, vueSetRaceNo };
    },
};

Circuit.vue

<script setup lang="ts">
   const { vueRaceNo, vueSetRaceNo } = useStore('vueRaceNo');

   const handleClick = () => {
        vueSetRaceNo(vueRaceNo() + 1);
    };
</script>
<template>
    <fieldset>
        <button @click="handleClick">10 連鎖スタート</button>
    </fieldset>
</template>

RaceWatch.vue

watch() で繋いでいくコンポーネントです。

<script setup lang="ts">
   const { vueRaceNo } = useStore('vueRaceNo');
   const state01 = ref(0);
   // 10 まで同じ構造が続きます

   watch(
        () => vueRaceNo(),
        () => {
            if (!vueRaceNo()) return;

            recordStart('RaceWatchState');
            state01.value++;
        },
        { immediate: true }
    );
   watch(
        () => state01.value,
        () => {
            if (!state01.value) return;

            state02.value++;
        },
        { immediate: true }
    );
   // 10 まで同じ構造が続きます
   watch(
        () => state10.value,
        () => {
            if (!state10.value) return;

            recordFinished('RaceWatchState');
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

RaceComputed.vue

computed() で繋いでいくコンポーネントです。finished を最終的に使わなければ計算しないため、致し方なく空の watch() で監視だけしています。

<script setup lang="ts">
   const { vueRaceNo } = useStore('vueRaceNo');

   const state01 = computed(() => {
        if (!vueRaceNo()) return;

        recordStart('RaceComputedState');
        return vueRaceNo() + 1;
    });
   const state02 = computed(() => {
        if (!state01.value) return;

        return state01.value;
    });
   // 10 まで同じ構造が続きます
   const finished = computed(() => {
        if (!state10.value) return;

        recordFinished('RaceComputedState');

        return state10.value;
    });
   watch(() => finished.value, () => {});
</script>
<template>
    <!-- return null -->
</template>

RaceWatchEffect.vue

watchEffect() で繋いでいくコンポーネントです。

<script setup lang="ts">
   const { vueRaceNo } = useStore('vueRaceNo');
   const state01 = ref(0);
   const setState01 = (value: number) => { state01.value = value; };
   // 10 まで同じ構造が続きます

   watchEffect(
        () => {
            if (!vueRaceNo()) return;

            recordStart('RaceWatchEffectState');
            setState01(vueRaceNo() + 1);
        }, { flush: 'pre' }
    );
   watchEffect(
        () => {
            if (!state01.value) return;

            setState02(state01.value);
        }, { flush: 'pre' }
    );
   // 10 まで同じ構造が続きます
   watchEffect(
        () => {
            if (!state10.value) return;

            recordFinished('RaceWatchEffectState');
        }, { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

50 連鎖用グローバルストア設定

type Quantum = number;
const initialValue: Quantum = 0;

export const _vueQuantum = {
    server: {
        vueQuantumContextStart: (): Quantum => initialValue,
        vueSetQuantumContextStart: (_: Quantum) => {},
        vueQuantumStoreStart: (): Quantum => initialValue,
        vueSetQuantumStoreStart: (_: Quantum) => {},

        vueQuantum01: (): Quantum => initialValue,
        vueSetQuantum01: (_: Quantum) => {},
        // 10 まで同じ構造が続きます
    },
    client: () => {
        const vueQuantumContextStart = ref<Quantum>(initialValue);
        const vueSetQuantumContextStart = (value: Quantum) => { vueQuantumContextStart.value = value; };
        const vueQuantumStoreStart = ref<Quantum>(initialValue);
        const vueSetQuantumStoreStart = (value: Quantum) => { vueQuantumStoreStart.value = value; };

        const vueQuantum01 = ref<Quantum>(initialValue);
        const vueSetQuantum01 = (value: Quantum) => { vueQuantum01.value = value; };
        // 10 まで同じ構造が続きます

        return {
            vueQuantumContextStart: () => vueQuantumContextStart.value, vueSetQuantumContextStart,
            vueQuantumStoreStart: () => vueQuantumStoreStart.value, vueSetQuantumStoreStart,

            vueQuantum01: () => vueQuantum01.value, vueSetQuantum01,
            // 10 まで同じ構造が続きます
        };
    },
};

50 連鎖用 Context 設定

<script lang="ts">
   type QuantumData = number;
   const initialValue: QuantumData = 0;
   type QuantumDataRef = Ref<QuantumData>;
   const QuantumDataSymbol01: InjectionKey<QuantumDataRef> = Symbol();
   // 10 まで同じ構造が続きます

   const provideQuantumData = (value: QuantumDataRef) => {
        provide(QuantumDataSymbol01, value);
        // 10 まで同じ構造が続きます
    };

   export const useQuantumDataContexts = () => {
        const injected01 = inject(QuantumDataSymbol01);
        // 10 まで同じ構造が続きます
        if (!injected01 || !injected02 || !injected03 || !injected04 || !injected05 || !injected06 || !injected07 || !injected08 || !injected09 || !injected10) {
            return {
                quantumData01: readonly({ value: initialValue }),
                setQuantumData01: (_: QuantumData) => {},
                // 10 まで同じ構造が続きます
            }
        };

        const quantumData01 = readonly(injected01);
        const setQuantumData01 = (value: QuantumData) => { injected01.value = value; };
        // 10 まで同じ構造が続きます

        return {
            quantumData01, setQuantumData01,
            // 10 まで同じ構造が続きます
        };
    };
   export default defineComponent({
        name: 'Quantum',
        props: {
            quantum01: {
                type: Number as PropType<QuantumData>,
                default: initialValue,
            },
            // 10 まで同じ構造が続きます
        },
        setup(props) {
            provide(QuantumDataSymbol01, ref(props.quantum01));
            // 10 まで同じ構造が続きます
        }
    });
</script>
<template>
    <slot />
</template>

QuantumStart.vue

<script setup lang="ts">
   const { vueSetQuantumStoreStart, vueSetQuantumContextStart } = useStore('vueQuantum');

   const handleClick = () => {
        vueSetQuantumContextStart(1);
        vueSetQuantumStoreStart(1);
    };
</script>
<template>
    <fieldset>
        <button @click="handleClick">50 連鎖スタート</button>
    </fieldset>
</template>

QuantumCircuit.vue

<template>
    <QuantumContext>
        <Quantum00 />
        // 10 まで同じ構造が続きます
    </QuantumContext>
</template>

_quantumContexts/Quantum00.vue

<script setup lang="ts">
   const { vueQuantumContextStart } = useStore('vueQuantum');
   const { setQuantumData01 } = useQuantumDataContexts();

   watch(
        () => vueQuantumContextStart(),
        () => {
            if (!vueQuantumContextStart()) return;

            recordStart('QuantumContext');
            setQuantumData01(vueQuantumContextStart());
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

_quantumContexts/Quantum01.vue

<script setup lang="ts">
   const { quantumData01, setQuantumData02 } = useQuantumDataContexts();

   watch(
        () => quantumData01.value,
        () => {
            if (!quantumData01.value) return;

            setQuantumData02(quantumData01.value);
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

_quantumContexts/Quantum10.vue

<script setup lang="ts">
   const { vueSetQuantumContextStart } = useStore('vueQuantum');
   const { setQuantumData01, quantumData10 } = useQuantumDataContexts();

   const incrementContext = (value: number) => {
        setQuantumData01(value + 1);
    };

   watch(
        () => quantumData10.value,
        () => {
            if (!quantumData10.value) return;

            if (quantumData10.value >= loopTotal) {
                recordFinished('QuantumContext');

                vueSetQuantumContextStart(0);
                return;
            }

            incrementContext(quantumData10.value);
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

_quantumStores/Quantum00.vue

<script setup lang="ts">
   const { vueQuantumStoreStart, vueSetQuantum01 } = useStore('vueQuantum');

   watch(
        () => vueQuantumStoreStart(),
        () => {
            if (!vueQuantumStoreStart()) return;

            recordStart('QuantumStore');
            vueSetQuantum01(vueQuantumStoreStart());
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

_quantumStores/Quantum01.vue

<script setup lang="ts">
   const { vueQuantum01, vueSetQuantum02 } = useStore('vueQuantum');

   watch(
        () => vueQuantum01(),
        () => {
            if (!vueQuantum01()) return;

            vueSetQuantum02(vueQuantum01());
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

_quantumStores/Quantum10.vue

<script setup lang="ts">
   const { vueSetQuantumStoreStart, vueSetQuantum01, vueQuantum10 } = useStore('vueQuantum');

   const incrementStore = (value: number) => {
        vueSetQuantum01(value + 1);
    };

   watch(
        () => vueQuantum10(),
        () => {
            if (!vueQuantum10()) return;

            if (vueQuantum10() >= loopTotal) {
                recordFinished('QuantumStore');

                vueSetQuantumStoreStart(0);
                return;
            }

            incrementStore(vueQuantum10());
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

React 版

最深部からの伝播用グローバルストア設定

const initialValue = 0;
let relay = initialValue;
type Relay = typeof relay;
let listeners: (() => void)[] = [];
type Listener = (typeof listeners)[number];

export const reactRelayStore = {
    setRelay(value: Relay) {
        relay = value;
        emitChange();
    },
    subscribe(listener: Listener) {
        listeners = [...listeners, listener];

        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    },
    getSnapshot() {
        return relay;
    },
    getServerSnapshot() {
        return initialValue;
    }
};

const emitChange = () => {
    for (let listener of listeners) {
        listener();
    }
};

最深部からの伝播用 Context 設定

import { createContext } from "react";
import type { RelayData } from "./types";

export const RelayContext = createContext({
    relayContext: 0,
    setRelayContext: (_: RelayData) => {},
});

Parent.tsx

export default function Parent() {
    const [ relayContext, setRelayContext ] = useState(0);

    return (
        <RelayContext value={{ relayContext, setRelayContext }}>
            <Child00 />
        </RelayContext>
    )
};

Child00.tsx

export default function Child00() {
    const { relayContext } = useContext(RelayContext);
    const relayStore = useSyncExternalStore(reactRelayStore.subscribe, reactRelayStore.getSnapshot, reactRelayStore.getServerSnapshot);
    const [ relay, setRelay ] = useState<RelayData>(0);

    useEffect(() => {
        if (!relay) return;

        recordFinished('RelaySetter');
    }, [ relay ]);

    useEffect(() => {
        if (!relayContext) return;

        recordFinished('RelayContext');
    }, [ relayContext ]);

    useEffect(() => {
        if (!relayStore) return;

        recordFinished('RelayStore');
    }, [ relayStore ]);

    return <Child01 relay={ relay } setRelay={ setRelay } />;
};

Child01.tsx

export default function Child01(props: RelayProps) {
    return <Child02 relay={ props.relay } setRelay={ props.setRelay } />;
};

Child10.tsx

export default function Child10(props: RelayProps) {
    const { relayContext, setRelayContext } = useContext(RelayContext);

    const handleChange = () => {
        const start = relayContext + 1;

        recordStart('RelaySetter');
        props.setRelay(start);

        recordStart('RelayContext');
        setRelayContext(start);

        recordStart('RelayStore');
        reactRelayStore.setRelay(start);
    };

    return (
        <fieldset>
            <input type="number" onChange={ handleChange } value={ props.relay } />
        </fieldset>
    );
};

10 連鎖用グローバルストア設定

const initialValue = 0;
let raceNo = initialValue;
type RaceNo = typeof raceNo;
let listeners: (() => void)[] = [];
type Listener = (typeof listeners)[number];

export const reactRaceNoStore = {
    setRaceNo(value: RaceNo) {
        raceNo = value;
        emitChange();
    },
    subscribe(listener: Listener) {
        listeners = [...listeners, listener];

        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    },
    getSnapshot() {
        return raceNo;
    },
    getServerSnapshot() {
        return initialValue;
    }
};

const emitChange = () => {
    for (let listener of listeners) {
        listener();
    }
};

Circuit.tsx

export default function Circuit() {
    const raceNoStore = useSyncExternalStore(reactRaceNoStore.subscribe, reactRaceNoStore.getSnapshot, reactRaceNoStore.getServerSnapshot);

    const handleClick = () => {
        reactRaceNoStore.setRaceNo(raceNoStore + 1);
    };

    return (
        <fieldset>
            <button onClick={ handleClick }>10 連鎖スタート</button>
        </fieldset>
    )
}

RaceEffect.tsx

useEffect() で繋いでいくコンポーネントです。

export default function RaceEffect() {
    const raceNoStore = useSyncExternalStore(reactRaceNoStore.subscribe, reactRaceNoStore.getSnapshot, reactRaceNoStore.getServerSnapshot);
    const [ state01, setState01 ] = useState(0);
    // 10 まで同じ構造が続きます

    useEffect(() => {
        if (!raceNoStore) return;

        recordStart('RaceEffectState');
        setState01(prev => prev + 1);
    }, [ raceNoStore ]);
    useEffect(() => {
        if (!state01) return;

        setState02(prev => prev + 1);
    }, [ state01 ]);
    // 10 まで同じ構造が続きます
    useEffect(() => {
        if (!state10) return;

        recordFinished('RaceEffectState');
    }, [ state10 ]);

    return null;
}

RaceMemo.tsx

useMemo() で繋いでいくコンポーネントです。

export default function RaceMemo() {
    const raceNoStore = useSyncExternalStore(reactRaceNoStore.subscribe, reactRaceNoStore.getSnapshot, reactRaceNoStore.getServerSnapshot);

    const state01 = useMemo(() => {
        if (!raceNoStore) return;

        recordStart('RaceMemoState');
        return raceNoStore + 1;
    }, [ raceNoStore ]);
    const state02 = useMemo(() => {
        if (!state01) return;

        return state01;
    }, [ state01 ]);
    // 10 まで同じ構造が続きます
    useMemo(() => {
        if (!state10) return;

        recordFinished('RaceMemoState');
    }, [ state10 ]);

    return null;
}

RaceLayoutEffect.tsx

useLayoutEffect() で繋いでいくコンポーネントです。

export default function RaceLayoutEffect() {
    const raceNoStore = useSyncExternalStore(reactRaceNoStore.subscribe, reactRaceNoStore.getSnapshot, reactRaceNoStore.getServerSnapshot);
    const [ state01, setState01 ] = useState(0);
    // 10 まで同じ構造が続きます

    useLayoutEffect(() => {
        if (!raceNoStore) return;

        recordStart('RaceLayoutEffectState');
        setState01(prev => prev + 1);
    }, [ raceNoStore ]);
    useLayoutEffect(() => {
        if (!state01) return;

        setState02(prev => prev + 1);
    }, [ state01 ]);
    // 10 まで同じ構造が続きます
    useLayoutEffect(() => {
        if (!state10) return;

        recordFinished('RaceLayoutEffectState');
    }, [ state10 ]);

    return null;
}

50 連鎖用グローバルストア設定

const initialValue = 0 as number;
type Quantum = typeof initialValue;

let quantumContextStart = initialValue;
let quantumStoreStart = initialValue;

let quantum01 = initialValue;
// 10 まで同じ構造が続きます

type Listener = () => void;
let listenersContextStart: Listener[] = [];
let listenersStoreStart: Listener[] = [];
let listeners01: Listener[] = [];
// 10 まで同じ構造が続きます

export const reactQuantumContextStart = {
    subscribe(listener: Listener) {
        listenersContextStart = [...listenersContextStart, listener];

        return () => {
            listenersContextStart = listenersContextStart.filter(l => l !== listener);
        };
    },
    getServerSnapshot() { return initialValue; },
    setQuantumContextStart(value: Quantum) {
        quantumContextStart = value;

        for (let listener of listenersContextStart) {
            listener();
        }
    },
    getSnapshot() { return quantumContextStart; },
};
export const reactQuantumStoreStart = {
    subscribe(listener: Listener) {
        listenersStoreStart = [...listenersStoreStart, listener];

        return () => {
            listenersStoreStart = listenersStoreStart.filter(l => l !== listener);
        };
    },
    getServerSnapshot() { return initialValue; },
    setQuantumStoreStart(value: Quantum) {
        quantumStoreStart = value;

        for (let listener of listenersStoreStart) {
            listener();
        }
    },
    getSnapshot() { return quantumStoreStart; },
};

export const reactQuantum01Store = {
    subscribe(listener: Listener) {
        listeners01 = [...listeners01, listener];

        return () => {
            listeners01 = listeners01.filter(l => l !== listener);
        };
    },
    getServerSnapshot() { return initialValue; },
    setQuantum01(value: Quantum) {
        quantum01 = value;

        for (let listener of listeners01) {
            listener();
        }
    },
    getSnapshot() { return quantum01; },
};
// 10 まで同じ構造が続きます

50 連鎖用 Context 設定

export type QuantumData = number;
export const QuantumContext = createContext({
    quantumData01: 0,
    setQuantumData01: (_: QuantumData) => {},
    // 10 まで同じ構造が続きます
});

QuantumStart.tsx

export default function QuantumStart() {
    const handleClick = () => {
        reactQuantumContextStart.setQuantumContextStart(1);
        reactQuantumStoreStart.setQuantumStoreStart(1);
    };

    return (
        <fieldset>
            <button onClick={ handleClick }>50 連鎖スタート</button>
        </fieldset>
    )
}

QuantumCircuit.tsx

export default function QuantumCircuit() {
    const [ quantumData01, setQuantumData01 ] = useState(0);
    // 10 まで同じ構造が続きます

    return (
        <QuantumContext value={{
            quantumData01, setQuantumData01,
            // 10 まで同じ構造が続きます
        }}>
            <Quantum00 />
            // 10 まで同じ構造が続きます
        </QuantumContext>
    )
}

_quantumContexts/Quantum00.tsx

export default function Quantum00() {
    const quantumStore = useSyncExternalStore(reactQuantumContextStart.subscribe, reactQuantumContextStart.getSnapshot, reactQuantumContextStart.getServerSnapshot);
    const { setQuantumData01 } = useContext(QuantumContext);

    useLayoutEffect(() => {
        if (!quantumStore) return;

        recordStart('QuantumContext');
        setQuantumData01(quantumStore);
    }, [ quantumStore ]);

    return null;
}

_quantumContexts/Quantum01.tsx

export default function Quantum01() {
    const { quantumData01, setQuantumData02 } = useContext(QuantumContext);

    useLayoutEffect(() => {
        if (!quantumData01) return;

        setQuantumData02(quantumData01);
    }, [ quantumData01 ]);

    return null;
}

_quantumContexts/Quantum10.tsx

export default function Quantum10() {
    const { setQuantumData01, quantumData10 } = useContext(QuantumContext);

    const incrementContext = (value: number) => {
        setQuantumData01(value + 1);
    };

    useLayoutEffect(() => {
        if (!quantumData10) return;

        if (quantumData10 >= loopTotal) {
            recordFinished('QuantumContext');

            reactQuantumContextStart.setQuantumContextStart(0);
            return;
        }

        incrementContext(quantumData10);
    }, [ quantumData10 ]);

    return null;
}

_quantumStores/Quantum00.tsx

export default function Quantum00() {
    const quantumStore = useSyncExternalStore(reactQuantumStoreStart.subscribe, reactQuantumStoreStart.getSnapshot, reactQuantumStoreStart.getServerSnapshot);

    useLayoutEffect(() => {
        if (!quantumStore) return;

        recordStart('QuantumStore');
        reactQuantum01Store.setQuantum01(quantumStore);
    }, [ quantumStore ]);

    return null;
}

_quantumStores/Quantum01.tsx

export default function Quantum01() {
    const quantumStore = useSyncExternalStore(reactQuantum01Store.subscribe, reactQuantum01Store.getSnapshot, reactQuantum01Store.getServerSnapshot);

    useLayoutEffect(() => {
        if (!quantumStore) return;

        reactQuantum02Store.setQuantum02(quantumStore);
    }, [ quantumStore ]);

    return null;
}

_quantumStores/Quantum10.tsx

export default function Quantum10() {
    const quantumStore = useSyncExternalStore(reactQuantum10Store.subscribe, reactQuantum10Store.getSnapshot, reactQuantum10Store.getServerSnapshot);

    const incrementStore = (value: number) => {
        reactQuantum01Store.setQuantum01(value + 1);
    };

    useLayoutEffect(() => {
        if (!quantumStore) return;

        if (quantumStore >= loopTotal) {
            recordFinished('QuantumStore');

            reactQuantumStoreStart.setQuantumStoreStart(0);
            return;
        }

        incrementStore(quantumStore);
    }, [ quantumStore ]);

    return null;
}

Qwik 版

Qwik ではグローバルストアを作る手段をまだ思いついていないため(Signal 設定はコンポーネント配下に置かなければならない)、Context で全て伝播しています。

最深部からの伝播用 Context 設定

export type RelayData = number;
export type RelaySetter = QRL<(_: RelayData) => void>;
export type RelayProps = {
    relay: RelayData;
    setRelay: RelaySetter;
};
export const relayData = createContextId<Signal<RelayData>>('relayData02');

Parent.tsx

export default component$(() => {
    const relayDataSignal = useSignal<RelayData>(0);
    useContextProvider(relayData, relayDataSignal);

    return <Child00 />;
});

Child00.tsx

export default component$(() => {
    const relayDataContext = useContext(relayData);
    const relay = useSignal(0);
    const setRelay = $((value: RelayData) => { relay.value = value; });

    useTask$(({ track }) => {
        track(() => relayDataContext.value);

        recordFinished('RelayContext');
    });

    useTask$(({ track }) => {
        track(() => relay.value);

        recordFinished('RelaySetter');
    });

    return <Child01 relay={ relay.value } setRelay={ setRelay } />;
});

Child01.tsx

export default component$((props: RelayProps) => {
    return <Child02 relay={ props.relay } setRelay={ props.setRelay } />;
});

Child10.tsx

export default component$((props: RelayProps) => {
    const relayDataContext = useContext(relayData);

    const handleStartContext$ = $((value: number) => {
        relayDataContext.value = value;
    });
    const handleStartSetter$ = $((value: number) => {
        props.setRelay(value);
    });
    const handleChange$ = $((isActive: boolean = false) => {
        if (!isActive) return;

        const start = relayDataContext.value + 1;

        recordStart('RelayContext');
        handleStartContext$(start);

        recordStart('RelaySetter');
        handleStartSetter$(start);
    });

    useVisibleTask$(() => {
        handleChange$();
    });

    return (
        <fieldset>
            <input type="number" onChange$={ () => handleChange$(true) } value={ props.relay } />
        </fieldset>
    );
});

10 連鎖コンポーネントの props 用型定義

export type RaceProps = {
    start: number;
};

Circuit.tsx

export default component$(() => {
    const start = useSignal(0);

    const handleClick$ = $((isActive: boolean = false) => {
        if (!isActive) return;

        start.value++;
    });
    useVisibleTask$(() => {
        handleClick$();
    });

    return (
        <fieldset>
            <button onClick$={ () => handleClick$(true) }>10 連鎖スタート</button>
            <RaceTask start={ start.value } />
            <RaceComputed start={ start.value } />
            <RaceVisibleTask start={ start.value } />
        </fieldset>
    )
});

RaceTask.tsx

useTask$() で繋いでいくコンポーネントです。

export default component$((props: RaceProps) => {
    const state01 = useSignal(0);
    // 10 まで同じ構造が続きます

    useTask$(({ track }) => {
        track(() => props.start);

        if (!props.start) return;

        recordStart('RaceTaskState');
        state01.value++;
    });
    useTask$(({ track }) => {
        track(() => state01.value);

        if (!state01.value) return;

        state02.value++;
    });
    // 10 まで同じ構造が続きます
    useTask$(({ track }) => {
        track(() => state10.value);

        if (!state10.value) return;

        recordFinished('RaceTaskState');
    });

    return null;
});

RaceComputed.tsx

useComputed$() で繋いでいくコンポーネントです。

export default component$((props: RaceProps) => {
    const state01 = useComputed$(() => {
        if (!props.start) return;

        recordStart('RaceComputedState');
        return props.start + 1;
    });
    const state02 = useComputed$(() => {
        if (!state01.value) return;

        return state01.value;
    });
    // 10 まで同じ構造が続きます
    useComputed$(() => {
        if (!state10.value) return;

        recordFinished('RaceComputedState');
    });

    return null;
});

RaceVisibleTask.tsx

useVisibleTask$() で繋いでいくコンポーネントです。

export default component$((props: RaceProps) => {
    const state01 = useSignal(0);
    // 10 まで同じ構造が続きます

    useVisibleTask$(({ track }) => {
        track(() => props.start);

        if (!props.start) return;

        recordStart('RaceVisibleTaskState');
        state01.value++;
    });
    useVisibleTask$(({ track }) => {
        track(() => state01.value);

        if (!props.start || !state01.value) return;

        state02.value++;
    });
    // 10 まで同じ構造が続きます
    useVisibleTask$(({ track }) => {
        track(() => state10.value);

        if (!props.start || !state10.value) return;

        recordFinished('RaceVisibleTaskState');
    });

    return null;
});

50 連鎖用 Context 設定

export type Quantum = number;
export type QuantumData = {
    quantumStart: Signal<Quantum>;

    quantum01: Signal<Quantum>;
    // 10 まで同じ構造が続きます
};
export const quantumData = createContextId<QuantumData>('quantumData02');

QuantumStart.tsx

export default component$(() => {
    const start = useSignal(0);

    const handleClick$ = $((isActive: boolean = false) => {
        if (!isActive) return;

        start.value++;
    });
    useVisibleTask$(() => {
        handleClick$();
    });

    return (
        <fieldset>
            <button onClick$={ () => handleClick$(true) }>50 連鎖スタート</button>
            <QuantumCircuit start={ start.value } />
        </fieldset>
    )
});

QuantumCircuit.tsx

export default component$((props: RaceProps) => {
    const quantumStart = useSignal<Quantum>(0);

    const quantum01 = useSignal<Quantum>(0);
    // 10 まで同じ構造が続きます

    useContextProvider(quantumData, {
        quantumStart: quantumStart,

        quantum01: quantum01,
        // 10 まで同じ構造が続きます
    });

    useTask$(({ track }) => {
        track(() => props.start);

        if (!props.start) return;

        quantumStart.value = props.start;
    });

    return (
        <>
            <Quantum00 />
            // 10 まで同じ構造が続きます
        </>
    )
});

_quantumContexts/Quantum00.tsx

export default component$(() => {
    const quantumDataContext = useContext(quantumData);

    useTask$(({ track }) => {
        track(() => quantumDataContext.quantumStart.value);

        if (!quantumDataContext.quantumStart.value) return;

        recordStart('QuantumContext');
        quantumDataContext.quantum01.value = quantumDataContext.quantumStart.value;
    });

    return null;
});

_quantumContexts/Quantum01.tsx

export default component$(() => {
    const quantumDataContext = useContext(quantumData);

    useTask$(({ track }) => {
        track(() => quantumDataContext.quantum01.value);

        if (!quantumDataContext.quantum01.value) return;

        quantumDataContext.quantum02.value = quantumDataContext.quantum01.value;
    });

    return null;
});

_quantumContexts/Quantum10.tsx

export default component$(() => {
    const quantumDataContext = useContext(quantumData);

    const incrementContext$ = $((value: number) => {
        quantumDataContext.quantum01.value = value + 1;
    });

    useTask$(({ track }) => {
        track(() => quantumDataContext.quantum10.value);

        if (!quantumDataContext.quantum10.value) return;

        if (quantumDataContext.quantum10.value >= loopTotal) {
            recordFinished('QuantumContext');

            quantumDataContext.quantumStart.value = 0;
            return;
        }

        incrementContext$(quantumDataContext.quantum10.value);
    });

    return null;
});

計測結果

最深部からの伝播は冒頭の通り、テンプレート無し(React もこの構成なら最も速い Memo 無し)のみで Svelte のみ $bindable() も対象に加え、10 連鎖は各フレームワークにより対象の追加あり、50 連鎖もテンプレート無しに分離してあるので React は Memo 無しです。Qwik は開始の伝播も Context です。

Solid

1 回目

  • RelaySetter: 0.20000004768371582 ミリ秒 / リロード直後
  • RelaySetter: 0.19999992847442627 ミリ秒 / ウォームアップ後
  • RelayContext: 0.10000002384185791 ミリ秒 / リロード直後
  • RelayContext: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • RelayStore: 0 ミリ秒 / リロード直後
  • RelayStore: 0 ミリ秒 / ウォームアップ後
  • RaceEffectState: 0 ミリ秒 / リロード直後
  • RaceEffectState: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • RaceMemoState: 0 ミリ秒 / リロード直後
  • RaceMemoState: 0 ミリ秒 / ウォームアップ後
  • QuantumContext: 0.10000002384185791 ミリ秒 / リロード直後
  • QuantumContext: 0.2999999523162842 ミリ秒 / ウォームアップ後
  • QuantumStore: 0.10000002384185791 ミリ秒 / リロード直後
  • QuantumStore: 0.10000002384185791 ミリ秒 / ウォームアップ後

2 回目

  • RelaySetter: 0.2999999523162842 ミリ秒 / リロード直後
  • RelaySetter: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • RelayContext: 0 ミリ秒 / リロード直後
  • RelayContext: 0 ミリ秒 / ウォームアップ後
  • RelayStore: 0.10000002384185791 ミリ秒 / リロード直後
  • RelayStore: 0 ミリ秒 / ウォームアップ後
  • RaceEffectState: 0.10000002384185791 ミリ秒 / リロード直後
  • RaceEffectState: 0 ミリ秒 / ウォームアップ後
  • RaceMemoState: 0.10000002384185791 ミリ秒 / リロード直後
  • RaceMemoState: 0 ミリ秒 / ウォームアップ後
  • QuantumContext: 0.10000002384185791 ミリ秒 / リロード直後
  • QuantumContext: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • QuantumStore: 0 ミリ秒 / リロード直後
  • QuantumStore: 0.10000002384185791 ミリ秒 / ウォームアップ後

3 回目

  • RelaySetter: 0.20000004768371582 ミリ秒 / リロード直後
  • RelaySetter: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • RelayContext: 0 ミリ秒 / リロード直後
  • RelayContext: 0 ミリ秒 / ウォームアップ後
  • RelayStore: 0 ミリ秒 / リロード直後
  • RelayStore: 0 ミリ秒 / ウォームアップ後
  • RaceEffectState: 0 ミリ秒 / リロード直後
  • RaceEffectState: 0 ミリ秒 / ウォームアップ後
  • RaceMemoState: 0.09999990463256836 ミリ秒 / リロード直後
  • RaceMemoState: 0 ミリ秒 / ウォームアップ後
  • QuantumContext: 0.20000004768371582 ミリ秒 / リロード直後
  • QuantumContext: 0.19999992847442627 ミリ秒 / ウォームアップ後
  • QuantumStore: 0.09999990463256836 ミリ秒 / リロード直後
  • QuantumStore: 0.10000002384185791 ミリ秒 / ウォームアップ後

平均

  • RelaySetter: 0.2333333492 ミリ秒 / リロード直後
  • RelaySetter: 0.1666666667 ミリ秒 / ウォームアップ後
  • RelayContext: 0.03333334128 ミリ秒 / リロード直後
  • RelayContext: 0.03333334128 ミリ秒 / ウォームアップ後
  • RelayStore: 0.03333334128 ミリ秒 / リロード直後
  • RelayStore: 0 ミリ秒 / ウォームアップ後
  • RaceEffectState: 0.03333334128 ミリ秒 / リロード直後
  • RaceEffectState: 0.03333334128 ミリ秒 / ウォームアップ後
  • RaceMemoState: 0.06666664282 ミリ秒 / リロード直後
  • RaceMemoState: 0 ミリ秒 / ウォームアップ後
  • QuantumContext: 0.1333333651 ミリ秒 / リロード直後
  • QuantumContext: 0.2333333095 ミリ秒 / ウォームアップ後
  • QuantumStore: 0.06666664282 ミリ秒 / リロード直後
  • QuantumStore: 0.1000000238 ミリ秒 / ウォームアップ後

Svelte

1 回目

  • RelayBindable: 0 ミリ秒 / リロード直後
  • RelayBindable: 0 ミリ秒 / ウォームアップ後
  • RelaySetter: 0.2999999523162842 ミリ秒 / リロード直後
  • RelaySetter: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • RelayContext: 0.3999999761581421 ミリ秒 / リロード直後
  • RelayContext: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • RelayStore: 0.30000007152557373 ミリ秒 / リロード直後
  • RelayStore: 0.30000007152557373 ミリ秒 / ウォームアップ後
  • RaceEffectState: 0.5 ミリ秒 / リロード直後
  • RaceEffectState: 0.09999990463256836 ミリ秒 / ウォームアップ後
  • RaceDerivedState: 0.09999990463256836 ミリ秒 / リロード直後
  • RaceDerivedState: 0 ミリ秒 / ウォームアップ後
  • QuantumContext: 1.399999976158142 ミリ秒 / リロード直後
  • QuantumContext: 0.6000000238418579 ミリ秒 / ウォームアップ後
  • QuantumStore: 1.899999976158142 ミリ秒 / リロード直後
  • QuantumStore: 1 ミリ秒 / ウォームアップ後

2 回目

  • RelayBindable: 0 ミリ秒 / リロード直後
  • RelayBindable: 0 ミリ秒 / ウォームアップ後
  • RelaySetter: 0.2999999523162842 ミリ秒 / リロード直後
  • RelaySetter: 0.09999990463256836 ミリ秒 / ウォームアップ後
  • RelayContext: 0.30000007152557373 ミリ秒 / リロード直後
  • RelayContext: 0.19999992847442627 ミリ秒 / ウォームアップ後
  • RelayStore: 0.10000002384185791 ミリ秒 / リロード直後
  • RelayStore: 0.3999999761581421 ミリ秒 / ウォームアップ後
  • RaceEffectState: 0.2999999523162842 ミリ秒 / リロード直後
  • RaceEffectState: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • RaceDerivedState: 0 ミリ秒 / リロード直後
  • RaceDerivedState: 0 ミリ秒 / ウォームアップ後
  • QuantumContext: 1.2999999523162842 ミリ秒 / リロード直後
  • QuantumContext: 1.2999999523162842 ミリ秒 / ウォームアップ後
  • QuantumStore: 1.8000000715255737 ミリ秒 / リロード直後
  • QuantumStore: 1.899999976158142 ミリ秒 / ウォームアップ後

3 回目

  • RelayBindable: 0 ミリ秒 / リロード直後
  • RelayBindable: 0 ミリ秒 / ウォームアップ後
  • RelaySetter: 0.5999999046325684 ミリ秒 / リロード直後
  • RelaySetter: 0.09999990463256836 ミリ秒 / ウォームアップ後
  • RelayContext: 0.8000000715255737 ミリ秒 / リロード直後
  • RelayContext: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • RelayStore: 0.5 ミリ秒 / リロード直後
  • RelayStore: 0.30000007152557373 ミリ秒 / ウォームアップ後
  • RaceEffectState: 0.3999999761581421 ミリ秒 / リロード直後
  • RaceEffectState: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • RaceDerivedState: 0.10000002384185791 ミリ秒 / リロード直後
  • RaceDerivedState: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • QuantumContext: 1.2999999523162842 ミリ秒 / リロード直後
  • QuantumContext: 1.399999976158142 ミリ秒 / ウォームアップ後
  • QuantumStore: 1.6999999284744263 ミリ秒 / リロード直後
  • QuantumStore: 1.899999976158142 ミリ秒 / ウォームアップ後

平均

  • RelayBindable: 0 ミリ秒 / リロード直後
  • RelayBindable: 0 ミリ秒 / ウォームアップ後
  • RelaySetter: 0.3999999364 ミリ秒 / リロード直後
  • RelaySetter: 0.09999994437 ミリ秒 / ウォームアップ後
  • RelayContext: 0.5000000397 ミリ秒 / リロード直後
  • RelayContext: 0.1666666667 ミリ秒 / ウォームアップ後
  • RelayStore: 0.3000000318 ミリ秒 / リロード直後
  • RelayStore: 0.3333333731 ミリ秒 / ウォームアップ後
  • RaceEffectState: 0.3999999762 ミリ秒 / リロード直後
  • RaceEffectState: 0.09999998411 ミリ秒 / ウォームアップ後
  • RaceDerivedState: 0.06666664282 ミリ秒 / リロード直後
  • RaceDerivedState: 0.03333334128 ミリ秒 / ウォームアップ後
  • QuantumContext: 1.3333332936 ミリ秒 / リロード直後
  • QuantumContext: 1.09999998411 ミリ秒 / ウォームアップ後
  • QuantumStore: 1.7999999921 ミリ秒 / リロード直後
  • QuantumStore: 1.5999999841 ミリ秒 / ウォームアップ後

Vue

1 回目

  • RelaySetter: 0.3999999761581421 ミリ秒 / リロード直後
  • RelaySetter: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • RelayContext: 0.2999999523162842 ミリ秒 / リロード直後
  • RelayContext: 0.30000007152557373 ミリ秒 / ウォームアップ後
  • RelayStore: 0.3999999761581421 ミリ秒 / リロード直後
  • RelayStore: 0.30000007152557373 ミリ秒 / ウォームアップ後
  • RaceWatchState: 0.10000002384185791 ミリ秒 / リロード直後
  • RaceWatchState: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • RaceComputedState: 0 ミリ秒 / リロード直後
  • RaceComputedState: 0 ミリ秒 / ウォームアップ後
  • RaceWatchEffectState: 0.10000002384185791 ミリ秒 / リロード直後
  • RaceWatchEffectState: 0 ミリ秒 / ウォームアップ後
  • QuantumContext: 0.5 ミリ秒 / リロード直後
  • QuantumContext: 0.5 ミリ秒 / ウォームアップ後
  • QuantumStore: 0.3999999761581421 ミリ秒 / リロード直後
  • QuantumStore: 0.40000009536743164 ミリ秒 / ウォームアップ後

2 回目

  • RelaySetter: 0.3999999761581421 ミリ秒 / リロード直後
  • RelaySetter: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • RelayContext: 0.2999999523162842 ミリ秒 / リロード直後
  • RelayContext: 0.09999990463256836 ミリ秒 / ウォームアップ後
  • RelayStore: 0.5 ミリ秒 / リロード直後
  • RelayStore: 0.2999999523162842 ミリ秒 / ウォームアップ後
  • RaceWatchState: 0 ミリ秒 / リロード直後
  • RaceWatchState: 0 ミリ秒 / ウォームアップ後
  • RaceComputedState: 0 ミリ秒 / リロード直後
  • RaceComputedState: 0 ミリ秒 / ウォームアップ後
  • RaceWatchEffectState: 0.19999992847442627 ミリ秒 / リロード直後
  • RaceWatchEffectState: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • QuantumContext: 0.2999999523162842 ミリ秒 / リロード直後
  • QuantumContext: 0.19999992847442627 ミリ秒 / ウォームアップ後
  • QuantumStore: 0.2999999523162842 ミリ秒 / リロード直後
  • QuantumStore: 0.3999999761581421 ミリ秒 / ウォームアップ後

3 回目

  • RelaySetter: 0.40000009536743164 ミリ秒 / リロード直後
  • RelaySetter: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • RelayContext: 0.2999999523162842 ミリ秒 / リロード直後
  • RelayContext: 0.19999992847442627 ミリ秒 / ウォームアップ後
  • RelayStore: 0.2999999523162842 ミリ秒 / リロード直後
  • RelayStore: 0.2999999523162842 ミリ秒 / ウォームアップ後
  • RaceWatchState: 0.20000004768371582 ミリ秒 / リロード直後
  • RaceWatchState: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • RaceComputedState: 0 ミリ秒 / リロード直後
  • RaceComputedState: 0.09999990463256836 ミリ秒 / ウォームアップ後
  • RaceWatchEffectState: 0.20000004768371582 ミリ秒 / リロード直後
  • RaceWatchEffectState: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • QuantumContext: 0.20000004768371582 ミリ秒 / リロード直後
  • QuantumContext: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • QuantumStore: 0.5 ミリ秒 / リロード直後
  • QuantumStore: 0.5 ミリ秒 / ウォームアップ後

平均

  • RelaySetter: 0.4000000159 ミリ秒 / リロード直後
  • RelaySetter: 0.1666667064 ミリ秒 / ウォームアップ後
  • RelayContext: 0.2999999523 ミリ秒 / リロード直後
  • RelayContext: 0.1999999682 ミリ秒 / ウォームアップ後
  • RelayStore: 0.3999999762 ミリ秒 / リロード直後
  • RelayStore: 0.2999999921 ミリ秒 / ウォームアップ後
  • RaceWatchState: 0.1000000238 ミリ秒 / リロード直後
  • RaceWatchState: 0.06666668256 ミリ秒 / ウォームアップ後
  • RaceComputedState: 0 ミリ秒 / リロード直後
  • RaceComputedState: 0.03333330154 ミリ秒 / ウォームアップ後
  • RaceWatchEffectState: 0.1666666667 ミリ秒 / リロード直後
  • RaceWatchEffectState: 0.06666668256 ミリ秒 / ウォームアップ後
  • QuantumContext: 0.3333333333 ミリ秒 / リロード直後
  • QuantumContext: 0.2666666508 ミリ秒 / ウォームアップ後
  • QuantumStore: 0.3999999762 ミリ秒 / リロード直後
  • QuantumStore: 0.4333333572 ミリ秒 / ウォームアップ後

React

1 回目

  • RelaySetter: 2.600000023841858 ミリ秒 / リロード直後
  • RelaySetter: 1.1999999284744263 ミリ秒 / ウォームアップ後
  • RelayContext: 2.5 ミリ秒 / リロード直後
  • RelayContext: 1.399999976158142 ミリ秒 / ウォームアップ後
  • RelayStore: 2.600000023841858 ミリ秒 / リロード直後
  • RelayStore: 1.399999976158142 ミリ秒 / ウォームアップ後
  • RaceEffectState: 4.600000023841858 ミリ秒 / リロード直後
  • RaceEffectState: 3.799999952316284 ミリ秒 / ウォームアップ後
  • RaceMemoState: 0 ミリ秒 / リロード直後
  • RaceMemoState: 0 ミリ秒 / ウォームアップ後
  • RaceLayoutEffectState: 1.2000000476837158 ミリ秒 / リロード直後
  • RaceLayoutEffectState: 0.9000000953674316 ミリ秒 / ウォームアップ後
  • QuantumContext: 36.89999997615814 ミリ秒 / リロード直後
  • QuantumContext: 31.09999990463257 ミリ秒 / ウォームアップ後
  • QuantumStore: 6.600000023841858 ミリ秒 / リロード直後
  • QuantumStore: 5.899999976158142 ミリ秒 / ウォームアップ後

2 回目

  • RelaySetter: 3.100000023841858 ミリ秒 / リロード直後
  • RelaySetter: 1.100000023841858 ミリ秒 / ウォームアップ後
  • RelayContext: 3.100000023841858 ミリ秒 / リロード直後
  • RelayContext: 1.100000023841858 ミリ秒 / ウォームアップ後
  • RelayStore: 3.200000047683716 ミリ秒 / リロード直後
  • RelayStore: 1.2999999523162842 ミリ秒 / ウォームアップ後
  • RaceEffectState: 3.899999976158142 ミリ秒 / リロード直後
  • RaceEffectState: 3.200000047683716 ミリ秒 / ウォームアップ後
  • RaceMemoState: 0.10000002384185791 ミリ秒 / リロード直後
  • RaceMemoState: 0 ミリ秒 / ウォームアップ後
  • RaceLayoutEffectState: 1.1999999284744263 ミリ秒 / リロード直後
  • RaceLayoutEffectState: 1 ミリ秒 / ウォームアップ後
  • QuantumContext: 32.299999952316284 ミリ秒 / リロード直後
  • QuantumContext: 31.300000071525574 ミリ秒 / ウォームアップ後
  • QuantumStore: 6.5 ミリ秒 / リロード直後
  • QuantumStore: 5.399999976158142 ミリ秒 / ウォームアップ後

3 回目

  • RelaySetter: 3 ミリ秒 / リロード直後
  • RelaySetter: 1.2000000476837158 ミリ秒 / ウォームアップ後
  • RelayContext: 2.799999952316284 ミリ秒 / リロード直後
  • RelayContext: 1.3000000715255737 ミリ秒 / ウォームアップ後
  • RelayStore: 3 ミリ秒 / リロード直後
  • RelayStore: 1.399999976158142 ミリ秒 / ウォームアップ後
  • RaceEffectState: 5.600000023841858 ミリ秒 / リロード直後
  • RaceEffectState: 3.100000023841858 ミリ秒 / ウォームアップ後
  • RaceMemoState: 0.10000002384185791 ミリ秒 / リロード直後
  • RaceMemoState: 0 ミリ秒 / ウォームアップ後
  • RaceLayoutEffectState: 2.600000023841858 ミリ秒 / リロード直後
  • RaceLayoutEffectState: 1.1999999284744263 ミリ秒 / ウォームアップ後
  • QuantumContext: 34.199999928474426 ミリ秒 / リロード直後
  • QuantumContext: 29.899999976158142 ミリ秒 / ウォームアップ後
  • QuantumStore: 6.800000071525574 ミリ秒 / リロード直後
  • QuantumStore: 6.300000071525574 ミリ秒 / ウォームアップ後

平均

  • RelaySetter: 2.9000000159 ミリ秒 / リロード直後
  • RelaySetter: 1.1666666667 ミリ秒 / ウォームアップ後
  • RelayContext: 2.7999999921 ミリ秒 / リロード直後
  • RelayContext: 1.2666666905 ミリ秒 / ウォームアップ後
  • RelayStore: 2.9333333572 ミリ秒 / リロード直後
  • RelayStore: 1.3666666349 ミリ秒 / ウォームアップ後
  • RaceEffectState: 4.7000000079 ミリ秒 / リロード直後
  • RaceEffectState: 3.3666666746 ミリ秒 / ウォームアップ後
  • RaceMemoState: 0.06666668256 ミリ秒 / リロード直後
  • RaceMemoState: 0 ミリ秒 / ウォームアップ後
  • RaceLayoutEffectState: 1.6666666667 ミリ秒 / リロード直後
  • RaceLayoutEffectState: 1.03333334128 ミリ秒 / ウォームアップ後
  • QuantumContext: 34.466666619 ミリ秒 / リロード直後
  • QuantumContext: 30.7666666508 ミリ秒 / ウォームアップ後
  • QuantumStore: 6.6333333651 ミリ秒 / リロード直後
  • QuantumStore: 5.8666666746 ミリ秒 / ウォームアップ後

Qwik

1 回目

  • RelaySetter: 6.5 ミリ秒 / QRL ダウンロード含む
  • RelaySetter: 1.0999999046325684 ミリ秒 / QRL ダウンロード後
  • RelayContext: 4.600000023841858 ミリ秒 / QRL ダウンロード含む
  • RelayContext: 1.100000023841858 ミリ秒 / QRL ダウンロード後
  • RaceTaskState: 19.5 ミリ秒 / QRL ダウンロード含む
  • RaceTaskState: 0.3999999761581421 ミリ秒 / QRL ダウンロード後
  • RaceComputedState: 19.5 ミリ秒 / QRL ダウンロード含む
  • RaceComputedState: 0.3999999761581421 ミリ秒 / QRL ダウンロード後
  • RaceVisibleTaskState: 0.2999999523162842 ミリ秒 / QRL ダウンロード後
  • QuantumContext: 18 ミリ秒 / QRL ダウンロード含む
  • QuantumContext: 1.2999999523162842 ミリ秒 / QRL ダウンロード後

2 回目

  • RelaySetter: 6.899999976158142 ミリ秒 / QRL ダウンロード含む
  • RelaySetter: 1 ミリ秒 / QRL ダウンロード後
  • RelayContext: 4.799999952316284 ミリ秒 / QRL ダウンロード含む
  • RelayContext: 0.7999999523162842 ミリ秒 / QRL ダウンロード後
  • RaceTaskState: 25.399999976158142 ミリ秒 / QRL ダウンロード含む
  • RaceTaskState: 0.5 ミリ秒 / QRL ダウンロード後
  • RaceComputedState: 25.300000071525574 ミリ秒 / QRL ダウンロード含む
  • RaceComputedState: 0.5999999046325684 ミリ秒 / QRL ダウンロード後
  • RaceVisibleTaskState: 0.3999999761581421 ミリ秒 / QRL ダウンロード後
  • QuantumContext: 15.799999952316284 ミリ秒 / QRL ダウンロード含む
  • QuantumContext: 1 ミリ秒 / QRL ダウンロード後

3 回目

  • RelaySetter: 5.700000047683716 ミリ秒 / QRL ダウンロード含む
  • RelaySetter: 0.8999999761581421 ミリ秒 / QRL ダウンロード後
  • RelayContext: 3.600000023841858 ミリ秒 / QRL ダウンロード含む
  • RelayContext: 0.8999999761581421 ミリ秒 / QRL ダウンロード後
  • RaceTaskState: 21.800000071525574 ミリ秒 / QRL ダウンロード含む
  • RaceTaskState: 0.40000009536743164 ミリ秒 / QRL ダウンロード後
  • RaceComputedState: 21.899999976158142 ミリ秒 / QRL ダウンロード含む
  • RaceComputedState: 0.7000000476837158 ミリ秒 / QRL ダウンロード後
  • RaceVisibleTaskState: 0.2999999523162842 ミリ秒 / QRL ダウンロード後
  • QuantumContext: 14.799999952316284 ミリ秒 / QRL ダウンロード含む
  • QuantumContext: 0.8999999761581421 ミリ秒 / QRL ダウンロード後

平均

  • RelaySetter: 6.3666666746 ミリ秒 / QRL ダウンロード含む
  • RelaySetter: 0.9999999603 ミリ秒 / QRL ダウンロード後
  • RelayContext: 4.3333333333 ミリ秒 / QRL ダウンロード含む
  • RelayContext: 0.9333333174 ミリ秒 / QRL ダウンロード後
  • RaceTaskState: 22.2333333492 ミリ秒 / QRL ダウンロード含む
  • RaceTaskState: 0.4333333572 ミリ秒 / QRL ダウンロード後
  • RaceComputedState: 22.2333333492 ミリ秒 / QRL ダウンロード含む
  • RaceComputedState: 0.5666666428 ミリ秒 / QRL ダウンロード後
  • RaceVisibleTaskState: 0.3333332936 ミリ秒 / QRL ダウンロード後
  • QuantumContext: 16.1999999682 ミリ秒 / QRL ダウンロード含む
  • QuantumContext: 1.06666664282 ミリ秒 / QRL ダウンロード後

RelaySetter

Svelte の $bindable() での伝播は、「同じ props を使う」ということで RelaySetter と同じ枠で扱うことにしました。

  1. Svelte Bindable リロード直後 / Svelte Bindable ウォームアップ後: 0 ミリ秒
  2. Svelte Setter ウォームアップ後: 0.09999994437 ミリ秒
  3. Solid ウォームアップ後: 0.1666666667 ミリ秒
  4. Vue ウォームアップ後: 0.1666667064 ミリ秒
  5. Solid リロード直後: 0.2333333492 ミリ秒
  6. Svelte Setter リロード直後: 0.3999999364 ミリ秒
  7. Vue リロード直後: 0.4000000159 ミリ秒
  8. Qwik QRL ダウンロード後: 0.9999999603 ミリ秒
  9. React ウォームアップ後: 1.1666666667 ミリ秒
  10. React リロード直後: 2.9000000159 ミリ秒
  11. Qwik QRL ダウンロード含む: 6.3666666746 ミリ秒

RelayContext

  1. Solid リロード直後 / Solid ウォームアップ後: 0.03333334128 ミリ秒
  2. Svelte ウォームアップ後: 0.1666666667 ミリ秒
  3. Vue ウォームアップ後: 0.1999999682 ミリ秒
  4. Vue リロード直後: 0.2999999523 ミリ秒
  5. Svelte リロード直後: 0.5000000397 ミリ秒
  6. Qwik QRL ダウンロード後: 0.9333333174 ミリ秒
  7. React ウォームアップ後: 1.2666666905 ミリ秒
  8. React リロード直後: 2.7999999921 ミリ秒
  9. Qwik QRL ダウンロード含む: 4.3333333333 ミリ秒

RelayStore

  1. Solid ウォームアップ後: 0 ミリ秒
  2. Solid リロード直後: 0.03333334128 ミリ秒
  3. Vue ウォームアップ後: 0.2999999921 ミリ秒
  4. Svelte リロード直後: 0.3000000318 ミリ秒
  5. Svelte ウォームアップ後: 0.3333333731 ミリ秒
  6. Vue リロード直後: 0.3999999762 ミリ秒
  7. React ウォームアップ後: 1.3666666349 ミリ秒
  8. React リロード直後: 2.9333333572 ミリ秒

RaceEffectState

Memo 系以外を同じ枠で扱うことにしました。

  1. Solid Effect リロード直後 / Solid Effect ウォームアップ後 / Vue Watch リロード直後: 0.03333334128 ミリ秒
  2. Vue WatchEffect ウォームアップ後: 0.06666668256 ミリ秒
  3. Svelte Effect ウォームアップ後 / Vue Watch ウォームアップ後: 0.09999998411 ミリ秒
  4. Vue WatchEffect リロード直後: 0.1000000238 ミリ秒
  5. Qwik VisibleTask QRL ダウンロード後: 0.3333332936 ミリ秒
  6. Svelte Effect リロード直後: 0.3999999762 ミリ秒
  7. Qwik Task QRL ダウンロード後: 0.4333333572 ミリ秒
  8. React LayoutEffect ウォームアップ後: 1.03333334128 ミリ秒
  9. React LayoutEffect リロード直後: 1.6666666667 ミリ秒
  10. React Effect ウォームアップ後: 3.3666666746 ミリ秒
  11. React Effect リロード直後: 4.7000000079 ミリ秒
  12. Qwik Task QRL ダウンロード含む: 22.2333333492 ミリ秒

RaceMemoState

  1. Solid ウォームアップ後 / Vue リロード直後 / React ウォームアップ後: 0 ミリ秒
  2. Vue ウォームアップ後: 0.03333330154 ミリ秒
  3. Svelte ウォームアップ後: 0.03333334128 ミリ秒
  4. Solid リロード直後 / Svelte リロード直後: 0.06666664282 ミリ秒
  5. React リロード直後: 0.06666668256 ミリ秒
  6. Qwik QRL ダウンロード後: 0.5666666428 ミリ秒
  7. Qwik QRL ダウンロード含む: 22.2333333492 ミリ秒

QuantumContext

  1. Solid リロード直後: 0.1333333651 ミリ秒
  2. Solid ウォームアップ後: 0.2333333095 ミリ秒
  3. Vue ウォームアップ後: 0.2666666508 ミリ秒
  4. Vue リロード直後: 0.3333333333 ミリ秒
  5. Qwik QRL ダウンロード後: 1.06666664282 ミリ秒
  6. Svelte ウォームアップ後: 1.09999998411 ミリ秒
  7. Svelte リロード直後: 1.3333332936 ミリ秒
  8. Qwik QRL ダウンロード含む: 16.1999999682 ミリ秒
  9. React ウォームアップ後: 30.7666666508 ミリ秒
  10. React リロード直後: 34.466666619 ミリ秒

QuantumStore

  1. Solid: 0.06666664282 ミリ秒 / リロード直後
  2. Solid: 0.1000000238 ミリ秒 / ウォームアップ後
  3. Vue: 0.3999999762 ミリ秒 / リロード直後
  4. Vue: 0.4333333572 ミリ秒 / ウォームアップ後
  5. Svelte: 1.5999999841 ミリ秒 / ウォームアップ後
  6. Svelte: 1.7999999921 ミリ秒 / リロード直後
  7. React: 5.8666666746 ミリ秒 / ウォームアップ後
  8. React: 6.6333333651 ミリ秒 / リロード直後

第 3 回も、いずれも 3 回ずつの結果で、有意な数値とは言えませんが、この結果になりました。 興味があってやってみたところ、$bindable() が不動の 0 ミリ秒だったのが驚きました。 Qwik は使った関数をシリアライズし直して、また使う際に解凍するらしいので、電撃数値が出ないのは仕方ないのかなと思います。ハイドレーションをしない代わり...という感じでしょうか。 今回も、とても面白い実験でした。

【第 2 回】 同一 Astro 上で Solid / Svelte / Vue / React それぞれの処理速度を計測してみた(おまけで Qwik)

前回の計測がおもしろかったので、第 2 回をやってみました。

wings.hatenablog.com

今回の計測項目はこちらにしました。

  • Memo 系による複数描画時間
  • リアクティブの構造差による伝播時間
  • ネストされた深い位置にあるコンポーネントへの伝播時間

1 つめの複数描画は、金額を変えたら小計・消費税・合計が変わるような感じです。 2 つめの構造差は、プリミティブ・多層オブジェクトの奥底・1 層オブジェクトでの伝播で、フラグを独立させるか・オブジェクトの中に入れるか・フラグ集を作るか、みたいな感じです。 3 つめの深い位置への伝播は、props によるバケツリレー・Context・グローバルストアでそれぞれ試しました(Qwik のみグローバルストアは無し)。バケツリレーと最深部へのピンポイント伝播で差があるのか見てみたかったのです。

Solid / Svelte / Vue のグローバルストアについての詳細は、こちらを参照してください。

wings.hatenablog.com

ちなみに続編です。

wings.hatenablog.com

では、早速いきましょう。今回も import 文は割愛します。また、他の割愛部分は、都度記載します。

共通

計測方法は前回と同じです。

performance02.ts

export const records = {
    Receipt: {
        start: 0,
        finished: 0,
    },
    Primitive: {
        start: 0,
        finished: 0,
    },
    NestObject: {
        start: 0,
        finished: 0,
    },
    Flags: {
        start: 0,
        finished: 0,
    },
    RelayProps: {
        start: 0,
        finished: 0,
    },
    RelayContext: {
        start: 0,
        finished: 0,
    },
    RelayStore: {
        start: 0,
        finished: 0,
    },
    PrimitiveShallowRef: {
        start: 0,
        finished: 0,
    },
    NestObjectReactive: {
        start: 0,
        finished: 0,
    },
    NestObjectShallowRef: {
        start: 0,
        finished: 0,
    },
    FlagsReactive: {
        start: 0,
        finished: 0,
    },
    FlagsShallowRef: {
        start: 0,
        finished: 0,
    },
};
export type Records = keyof typeof records;
const now = () => performance.now();
export const recordStart = (target: Records) => {
    records[target].start = now();
};
export const recordFinished = (target: Records, name?: string) => {
    if (records[target].start === 0) return;

    records[target].finished = now();
    const diff = records[target].finished - records[target].start;
    console.log(`${name ?? target}: ${diff} ミリ秒`);
};

performance02/Solid.astro

以下は Solid 版ですが、Svelte / Vue / React 共に同じ構成です。

<div id="performance">
    <Receipt client:load />
    <ReactiveTypes client:load />
    <Parent client:load />
</div>

CSS

前回あった client:visible や画像用の記述は無く、これだけです。

#performance {
    width: 750px;
    margin: 0 auto;
}

型定義

リアクティブ構造差での各コンポーネントは props の型が同じなので、まとめます。

export type ReactiveTypeProps = {
    start: number;
};

Solid 版

Receipt.tsx

複数描画時間計測用のコンポーネントです。

export default function Receipt() {
    const [ amount, setAmount ] = createSignal(0);

    const quantity = 2;

    let elTotal: HTMLSpanElement | undefined;

    const subTotal = createMemo(() => {
        return amount() * quantity;
    });

    const tax = createMemo(() => {
        return subTotal() / 10;
    });

    const total = createMemo(() => {
        return subTotal() + tax();
    });

    const handleChange = (e: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement; }) => {
        recordStart('Receipt');
        setAmount(+e.currentTarget.value);
    };

    createEffect(() => {
        if (elTotal?.textContent !== total().toString()) return;

        recordFinished('Receipt');
    });

    return (
        <>
            <fieldset>
                <span>金額</span>
                <input type="number" value={ amount() } onChange={ handleChange } />
            </fieldset>
            <fieldset>
                <span>小計</span>
                <span>{ subTotal() }</span>
            </fieldset>
            <fieldset>
                <span>消費税</span>
                <span>{ tax() }</span>
            </fieldset>
            <fieldset>
                <span>合計</span>
                <span ref={ elTotal }>{ total() }</span>
            </fieldset>
        </>
    )
}

ReactiveTypes.tsx

リアクティブ構造差の伝播時間計測用の、計測開始をそれぞれに伝えるコンポーネントです。

export default function ReactiveTypes() {
    const [ start, setStart ] = createSignal(0);

    const handleClick = () => {
        setStart(start() + 1);
    };

    return (
        <>
            <fieldset>
                <button onClick={ handleClick }>リアクティブ構造差の計測開始</button>
            </fieldset>
            <Primitive start={ start() } />
            <NestObject start={ start() } />
            <Flags start={ start() } />
        </>
    )
};

Primitive.tsx

プリミティブで定義したリアクティブの計測用コンポーネントです(フラグを独立させた、感じ)。

export default function Primitive(props: ReactiveTypeProps) {
    const [ reactive, setReactive ] = createSignal(0);

    createEffect(() => {
        if (!props.start) return;

        recordStart('Primitive');
        setReactive(props.start);
    });

    createEffect(() => {
        if (!reactive()) return;

        recordFinished('Primitive');
    });

    return null;
};

NestObject.tsx

多層オブジェクトで定義したリアクティブの計測用コンポーネントです(フラグをオブジェクトの中へ入れた、感じ)。

export default function NestObject(props: ReactiveTypeProps) {
    const [ reactive, setReactive ] = createStore({
        dummy01a: { dummy02a: { dummy03a: { dummy04a: { dummy05a: 0 } } } },
        // dummy01y まで同じ構造が続きます
        active01z: { active02z: { active03z: { active04z: { active05z: 0 } } } },
    });

    createEffect(() => {
        if (!props.start) return;

        recordStart('NestObject');
        setReactive(produce(r => r.active01z.active02z.active03z.active04z.active05z = props.start));
    });

    createEffect(() => {
        if (!reactive.active01z.active02z.active03z.active04z.active05z) return;

        recordFinished('NestObject');
    });

    return null;
};

Flags.tsx

1 層オブジェクトで定義したリアクティブの計測用コンポーネントです(フラグ集を作った、感じ)。

export default function Flags(props: ReactiveTypeProps) {
    const [ reactive, setReactive ] = createStore({
        dummy01a: 0,
        // dummy01y まで同じ構造が続きます
        active01z: 0,
    });

    createEffect(() => {
        if (!props.start) return;

        recordStart('Flags');
        setReactive(produce(r => r.active01z = props.start));
    });

    createEffect(() => {
        if (!reactive.active01z) return;

        recordFinished('Flags');
    });

    return null;
};

グローバルストア設定

深い位置への伝播用で使うグローバルストアの定義です。

type Relay = number;
const initialValue: Relay = 0;

export const _solidRelay = {
    server: {
        solidRelay: (): Relay => initialValue,
        solidSetRelay: (_: Relay) => {},
    },
    client: () => {
        const [ solidRelay, solidSetRelay ] = createSignal<Relay>(initialValue);

        return { solidRelay, solidSetRelay };
    },
};

Context 設定

Context の定義です。

export type RelayData = number;
export type RelayProps = {
    relay: RelayData;
};

const createRelayDataContexts = () => {
    const [ relayData, setRelayData ] = createSignal<RelayData>(0);

    return { relayData, setRelayData };
};
type RelayDataContextsType = ReturnType<typeof createRelayDataContexts>;
const RelayDataContexts = createContext<RelayDataContextsType>() as Context<RelayDataContextsType>;

export const RelayDataProvider = (props: { children: JSX.Element }) => {
    return (
        <RelayDataContexts.Provider value={ createRelayDataContexts() }>{ props.children }</RelayDataContexts.Provider>
    );
}
export const useRelayDataContexts = () => useContext(RelayDataContexts);

Parent.tsx

Context で包む親コンポーネントです。

export default function Parent() {
    return (
        <RelayDataProvider>
            <Child00 />
        </RelayDataProvider>
    )
};

Child00.tsx

バケツリレーを開始するコンポーネントです。

export default function Child00() {
    const { setRelayData } = useRelayDataContexts();
    const { solidSetRelay } = useStore('solidRelay');
    const [ relayStart, setRelayStart ] = createSignal(0);

    const handleClick = () => {
        var start = relayStart() + 1;
        recordStart('RelayProps');
        setRelayStart(start);

        recordStart('RelayContext');
        setRelayData(start);

        recordStart('RelayStore');
        solidSetRelay(start);
    };

    return (
        <>
            <fieldset>
                <button onClick={ handleClick }>バケツリレー開始</button>
            </fieldset>
            <Child01 relay={ relayStart() } />
        </>
    )
};

Child01.tsx

リレー中継地点となる子コンポーネントです(Child09.tsx まで割愛します)

export default function Child01(props: RelayProps) {
    return <Child02 relay={ props.relay } />;
};

Child10.tsx

バケツリレーのゴール地点である子コンポーネントです。

export default function Child01(props: RelayProps) {
    const { relayData } = useRelayDataContexts();
    const { solidRelay } = useStore('solidRelay');

    createEffect(() => {
        if (!props.relay) return;

        recordFinished('RelayProps');
    });

    createEffect(() => {
        if (!relayData()) return;

        recordFinished('RelayContext');
    });

    createEffect(() => {
        if (!solidRelay()) return;

        recordFinished('RelayStore');
    });

    return null;
};

Svelte 版

相変わらず Solid 版とやってることは変わりません。

Receipt.svelte

<script lang="ts">
    let amount = $state(0);

    const quantity = 2;

    let elTotal: HTMLSpanElement | undefined;

    const subTotal = $derived(amount * quantity);

    const tax = $derived(subTotal / 10);

    const total = $derived(subTotal + tax);

    const handleChange = async (e: Event & { currentTarget: EventTarget & HTMLInputElement; }) => {
        recordStart('Receipt');
        amount = +e.currentTarget.value;
    };

    $effect(() => {
        if (elTotal?.textContent !== total.toString()) return;

        recordFinished('Receipt');
    });
</script>
<fieldset>
    <span>金額</span>
    <input type="number" value={ amount } onchange={ handleChange } />
</fieldset>
<fieldset>
    <span>小計</span>
    <span>{ subTotal }</span>
</fieldset>
<fieldset>
    <span>消費税</span>
    <span>{ tax }</span>
</fieldset>
<fieldset>
    <span>合計</span>
    <span bind:this={ elTotal }>{ total }</span>
</fieldset>

ReactiveTypes.svelte

<script lang="ts">
    let start = $state(0);

    const handleClick = () => {
        start++;
    };
</script>
<fieldset>
    <button onclick={ handleClick }>リアクティブ構造差の計測開始</button>
</fieldset>
<Primitive start={ start } />
<NestObject start={ start } />
<Flags start={ start } />

Primitive.svelte

<script lang="ts">
    const { start }: ReactiveTypeProps = $props();

    let reactive = $state(0);

    $effect(() => {
        if (!start) return;

        recordStart('Primitive');
        reactive = start;
    });

    $effect(() => {
        if (!reactive) return;

        recordFinished('Primitive');
    });
</script>

NestObject.svelte

<script lang="ts">
    const { start }: ReactiveTypeProps = $props();

    const reactive = $state({
        dummy01a: { dummy02a: { dummy03a: { dummy04a: { dummy05a: 0 } } } },
        // dummy01y まで同じ構造が続きます
        active01z: { active02z: { active03z: { active04z: { active05z: 0 } } } },
    });

    $effect(() => {
        if (!start) return;

        recordStart('NestObject');
        reactive.active01z.active02z.active03z.active04z.active05z = start;
    });

    $effect(() => {
        if (!reactive.active01z.active02z.active03z.active04z.active05z) return;

        recordFinished('NestObject');
    });
</script>

Flags.svelte

<script lang="ts">
    const { start }: ReactiveTypeProps = $props();

    const reactive = $state({
        dummy01a: 0,
        // dummy01y まで同じ構造が続きます
        active01z: 0,
    });

    $effect(() => {
        if (!start) return;

        recordStart('Flags');
        reactive.active01z = start;
    });

    $effect(() => {
        if (!reactive.active01z) return;

        recordFinished('Flags');
    });
</script>

グローバルストア設定

type Relay = number;
const initialValue: Relay = 0;

export const _svelteRelay = {
    server: {
        svelteRelay: (): Relay => initialValue,
        svelteSetRelay: (_: Relay) => {},
    },
    client: () => {
        let svelteRelay = $state<Relay>(initialValue);
        const svelteSetRelay = (value: Relay) => { svelteRelay = value; };

        return { svelteRelay: () => svelteRelay, svelteSetRelay };
    },
};

Context 設定

深い位置への伝播用で使う Context の定義です。

const createRelayDataContexts = () => {
    const states = $state({
        relayData: 0,
    });
    type States = typeof states;

    return {
        relayData: () => states.relayData,
        setRelayData: (value: States['relayData'] = 0) => states.relayData = value,
    };
};
const [ useRelayDataContexts, setRelayDataContexts ] = createContext<ReturnType<typeof createRelayDataContexts>>();

export type RelayProps = {
    relay: ReturnType<ReturnType<typeof createRelayDataContexts>['relayData']>;
}
export const provideRelayDataContexts = () => setRelayDataContexts(createRelayDataContexts());
export { useRelayDataContexts };

Parent.svelte

<script lang="ts">
    provideRelayDataContexts();
</script>
<Child00 />

Child00.svelte

<script lang="ts">
    const { setRelayData } = useRelayDataContexts();
    const { svelteSetRelay } = useStore('svelteRelay');
    let relayStart = $state(0);

    const handleClick = () => {
        var start = relayStart + 1;
        recordStart('RelayProps');
        relayStart++;

        recordStart('RelayContext');
        setRelayData(start);

        recordStart('RelayStore');
        svelteSetRelay(start);
    };
</script>
<fieldset>
    <button onclick={ handleClick }>バケツリレー開始</button>
</fieldset>
<Child01 relay={ relayData() } />

Child01.svelte

Child09.svelte まで割愛します。

<script lang="ts">
    const { relay }: RelayProps = $props();
</script>
<Child02 relay={ relay } />

Child10.svelte

バケツリレーのゴール地点である子コンポーネントです。

<script lang="ts">
    const { relay }: RelayProps = $props();

    const { relayData } = useRelayDataContexts();
    const { svelteRelay } = useStore('svelteRelay');

    $effect(() => {
        if (!relay) return;

        recordFinished('RelayProps');
    });

    $effect(() => {
        if (!relayData()) return;

        recordFinished('RelayContext');
    });

    $effect(() => {
        if (!svelteRelay()) return;

        recordFinished('RelayStore');
    });
</script>

Vue 版

こちらは、リアクティブ構造差について、追加計測項目があります。template で何か入れないと怒られるので、コメントで null 代わりにしました。

Receipt.vue

<script setup lang="ts">
   const amount = ref(0);
   const elTotal = useTemplateRef('elTotal');

   const quantity = 2;

   const subTotal = computed(() => amount.value * quantity);

   const tax = computed(() => subTotal.value / 10);

   const total = computed(() => subTotal.value + tax.value);

   const handleChange = async (e: Event) => {
        if (!e.currentTarget || !(e.currentTarget instanceof HTMLInputElement)) return;

        recordStart('Receipt');
        amount.value = +e.currentTarget.value;
    };

   watchEffect(() => {
        if (elTotal.value?.textContent !== total.value.toString()) return;

        recordFinished('Receipt');
    }, { flush: 'post' });
</script>
<template>
    <fieldset>
        <span>金額</span>
        <input type="number" :value="amount" @change="handleChange" />
    </fieldset>
    <fieldset>
        <span>小計</span>
        <span>{{ subTotal }}</span>
    </fieldset>
    <fieldset>
        <span>消費税</span>
        <span>{{ tax }}</span>
    </fieldset>
    <fieldset>
        <span>合計</span>
        <span ref="elTotal">{{ total }}</span>
    </fieldset>
</template>

ReactiveTypes.vue

<script setup lang="ts">
   const start = ref(0);

   const handleClick = async () => {
        start.value++;
    };
</script>
<template>
    <fieldset>
        <button @click="handleClick">リアクティブ構造差の計測開始</button>
    </fieldset>
    <Primitive :start="start" />
    <NestObject :start="start" />
    <Flags :start="start" />
</template>

Primitive.vue

ref() に加えて shallowRef() でも計測してみようと思いました。

<script setup lang="ts">
   const props = defineProps<ReactiveTypeProps>();

   const initialValue = 0;

   const dataRef = ref(initialValue);
   const dataShallowRef = shallowRef({ data: initialValue });

   watch(
        () => props.start,
        () => {
            if (!props.start) return;

            recordStart('Primitive');
            dataRef.value = props.start;

            recordStart('PrimitiveShallowRef');
            dataShallowRef.value.data = props.start;
            triggerRef(dataShallowRef);
        },
        { immediate: true }
    );

   watch(
        () => dataRef.value,
        () => {
            if (!dataRef.value) return;

            recordFinished('Primitive', 'PrimitiveRef');
        },
        { immediate: true }
    );

   watch(
        () => dataShallowRef.value.data,
        () => {
            if (!dataShallowRef.value.data) return;

            recordFinished('PrimitiveShallowRef');
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

NestObject.vue

こちらは ref() / shallowRef() に加えて reactive() も参加してもらいました。

<script setup lang="ts">
   const props = defineProps<ReactiveTypeProps>();

   const initialValues = {
        dummy01a: { dummy02a: { dummy03a: { dummy04a: { dummy05a: 0 } } } },
        // dummy01y まで同じ構造が続きます
        active01z: { active02z: { active03z: { active04z: { active05z: 0 } } } },
    };

   const dataRef = ref(initialValues);
   const dataReactive = reactive(initialValues);
   const dataShallowRef = shallowRef(initialValues);

   watch(
        () => props.start,
        () => {
            if (!props.start) return;

            recordStart('NestObject');
            dataRef.value.active01z.active02z.active03z.active04z.active05z = props.start;

            recordStart('NestObjectReactive');
            dataReactive.active01z.active02z.active03z.active04z.active05z = props.start;

            recordStart('NestObjectShallowRef');
            dataShallowRef.value.active01z.active02z.active03z.active04z.active05z = props.start;
            triggerRef(dataShallowRef);
        },
        { immediate: true }
    );

   watch(
        () => dataRef.value.active01z.active02z.active03z.active04z.active05z,
        () => {
            if (!dataRef.value.active01z.active02z.active03z.active04z.active05z) return;

            recordFinished('NestObject', 'NestObjectRef');
        },
        { immediate: true }
    );

   watch(
        () => dataReactive.active01z.active02z.active03z.active04z.active05z,
        () => {
            if (!dataReactive.active01z.active02z.active03z.active04z.active05z) return;

            recordFinished('NestObjectReactive');
        },
        { immediate: true }
    );

   watch(
        () => dataShallowRef.value.active01z.active02z.active03z.active04z.active05z,
        () => {
            if (!dataShallowRef.value.active01z.active02z.active03z.active04z.active05z) return;

            recordFinished('NestObjectShallowRef');
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

Flags.vue

こちらも ref() / shallowRef() / reactive() の 3 つです。

<script setup lang="ts">
   const props = defineProps<ReactiveTypeProps>();

   const initialValues = {
        dummy01a: 0,
        // dummy01y まで同じ構造が続きます
        active01z: 0,
    };

   const dataRef = ref(initialValues);
   const dataReactive = reactive(initialValues);
   const dataShallowRef = shallowRef(initialValues);

   watch(
        () => props.start,
        () => {
            if (!props.start) return;

            recordStart('Flags');
            dataRef.value.active01z = props.start;

            recordStart('FlagsReactive');
            dataReactive.active01z = props.start;

            recordStart('FlagsShallowRef');
            dataShallowRef.value.active01z = props.start;
            triggerRef(dataShallowRef);
        },
        { immediate: true }
    );

   watch(
        () => dataRef.value.active01z,
        () => {
            if (!dataRef.value.active01z) return;

            recordFinished('Flags', 'FlagsRef');
        },
        { immediate: true }
    );

   watch(
        () => dataReactive.active01z,
        () => {
            if (!dataReactive.active01z) return;

            recordFinished('FlagsReactive');
        },
        { immediate: true }
    );

   watch(
        () => dataShallowRef.value.active01z,
        () => {
            if (!dataShallowRef.value.active01z) return;

            recordFinished('FlagsShallowRef');
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

グローバルストア設定

type Relay = number;
const initialValue: Relay = 0;

export const _vueRelay = {
    server: {
        vueRelay: (): Relay => initialValue,
        vueSetRelay: (_: Relay) => {},
    },
    client: () => {
        const vueRelay = ref<Relay>(initialValue);
        const vueSetRelay = (value: Relay) => { vueRelay.value = value; };

        return { vueRelay: () => vueRelay.value, vueSetRelay };
    },
};

Context 設定

<script lang="ts">
   type RelayData = number;
   const initialValue: RelayData = 0;
   type RelayDataRef = Ref<RelayData>;
   const RelayDataSymbol: InjectionKey<RelayDataRef> = Symbol();

   const provideRelayData = (value: RelayDataRef) => {
        provide(RelayDataSymbol, value);
    };

   export const useRelayDataContexts = () => {
        const injected = inject(RelayDataSymbol);
        if (!injected) {
            return {
                relayData: readonly({ value: initialValue }),
                setRelayData: (_: RelayData) => {},
            }
        };

        const relayData = readonly(injected);
        const setRelayData = (value: RelayData) => { injected.value = value; };

        return { relayData, setRelayData };
    };
   export type RelayProps = {
        relay: RelayData;
    };
   export default defineComponent({
        name: 'Relay',
        props: {
            relay: {
                type: Number as PropType<RelayData>,
                default: initialValue,
            },
        },
        setup(props) {
            provideRelayData(ref(props.relay));
        }
    });
</script>
<template>
    <slot />
</template>

Parent.vue

<template>
    <RelayContext>
        <Child00 />
    </RelayContext>
</template>

Child00.vue

<script setup lang="ts">
   const { setRelayData } = useRelayDataContexts();
   const { vueSetRelay } = useStore('vueRelay');
   const relayStart = ref(0);

   const handleClick = async () => {
        var start = relayStart.value + 1;
        recordStart('RelayProps');
        relayStart.value = start;

        recordStart('RelayContext');
        setRelayData(start);

        recordStart('RelayStore');
        vueSetRelay(start);
    };
</script>
<template>
    <fieldset>
        <button @click="handleClick">バケツリレー開始</button>
    </fieldset>
    <Child01 :relay="relayData" />
</template>

Child01.vue

Child09.vue まで割愛します。

<script setup lang="ts">
   const props = defineProps<RelayProps>();
</script>
<template>
    <Child02 :relay="props.relay" />
</template>

Child10.vue

バケツリレーのゴール地点である子コンポーネントです。

<script setup lang="ts">
   const props = defineProps<RelayProps>();

   const { relayData } = useRelayDataContexts();
   const { vueRelay } = useStore('vueRelay');

   watch(
        () => props.relay,
        () => {
            if (!props.relay) return;

            recordFinished('RelayProps');
        },
        { immediate: true }
    );

   watch(
        () => relayData.value,
        () => {
            if (!relayData.value) return;

            recordFinished('RelayContext');
        },
        { immediate: true }
    );

   watch(
        () => vueRelay(),
        () => {
            if (!vueRelay()) return;

            recordFinished('RelayStore');
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

React 版

他が測定開始を操作時にしているので、React も同じく操作を測定開始としています。

Receipt.tsx

export default function Receipt() {
    const [ amount, setAmount ] = useState(0);

    const quantity = 2;

    const elTotal = useRef<HTMLSpanElement | null>(null);

    const subTotal = useMemo(() => {
        return amount * quantity;
    }, [ amount ]);

    const tax = useMemo(() => {
        return subTotal / 10;
    }, [ subTotal ]);

    const total = useMemo(() => {
        return subTotal + tax;
    }, [ subTotal, tax ]);

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        recordStart('Receipt');
        setAmount(+e.currentTarget.value);
    };

    useEffect(() => {
        if (elTotal.current?.textContent !== total.toString()) return;

        recordFinished('Receipt');
    }, [ total, elTotal.current?.textContent ]);

    return (
        <>
            <fieldset>
                <span>金額</span>
                <input type="number" value={ amount } onChange={ handleChange } />
            </fieldset>
            <fieldset>
                <span>小計</span>
                <span>{ subTotal }</span>
            </fieldset>
            <fieldset>
                <span>消費税</span>
                <span>{ tax }</span>
            </fieldset>
            <fieldset>
                <span>合計</span>
                <span ref={ elTotal }>{ total }</span>
            </fieldset>
        </>
    )
}

ReactiveTypes.tsx

export default function ReactiveTypes() {
    const [ start, setStart ] = useState(0);

    const handleClick = () => {
        setStart(start + 1);
    };

    return (
        <>
            <fieldset>
                <button onClick={ handleClick }>リアクティブ構造差の計測開始</button>
            </fieldset>
            <Primitive start={ start } />
            <NestObject start={ start } />
            <Flags start={ start } />
        </>
    )
};

Primitive.tsx

export default function Primitive(props: ReactiveTypeProps) {
    const [ reactive, setReactive ] = useState(0);

    useEffect(() => {
        if (!props.start) return;

        recordStart('Primitive');
        setReactive(props.start);
    }, [ props.start ]);

    useEffect(() => {
        if (!reactive) return;

        recordFinished('Primitive');
    }, [ reactive ]);

    return null;
};

NestObject.tsx

Flags.tsx もそうですが、React 単体なので、オブジェクトを渡しつつ更新という形にしました。

export default function NestObject(props: ReactiveTypeProps) {
    const [ reactive, setReactive ] = useState({
        dummy01a: { dummy02a: { dummy03a: { dummy04a: { dummy05a: 0 } } } },
        // dummy01y まで同じ構造が続きます
        active01z: { active02z: { active03z: { active04z: { active05z: 0 } } } },
    });

    useEffect(() => {
        if (!props.start) return;

        recordStart('NestObject');
        setReactive({
            ...reactive,
            active01z: { active02z: { active03z: { active04z: { active05z: props.start } } } }
        });
    }, [ props.start ]);

    useEffect(() => {
        if (!reactive.active01z.active02z.active03z.active04z.active05z) return;

        recordFinished('NestObject');
    }, [ reactive.active01z.active02z.active03z.active04z.active05z ]);

    return null;
};

Flags.tsx

export default function Flags(props: ReactiveTypeProps) {
    const [ reactive, setReactive ] = useState({
        dummy01a: 0,
        // dummy01y まで同じ構造が続きます
        active01z: 0,
    });

    useEffect(() => {
        if (!props.start) return;

        recordStart('Flags');
        setReactive({
            ...reactive,
            active01z: props.start,
        });
    }, [ props.start ]);

    useEffect(() => {
        if (!reactive.active01z) return;

        recordFinished('Flags');
    }, [ reactive.active01z ]);

    return null;
};

グローバルストア設定

useSyncExternalStore() を使うので、そのストアの定義です。

const initialValue = 0;
let relay = initialValue;
type Relay = typeof relay;
let listeners: (() => void)[] = [];
type Listener = (typeof listeners)[number];

export const reactRelayStore = {
    setRelay(value: Relay) {
        relay = value;
        emitChange();
    },
    subscribe(listener: Listener) {
        listeners = [...listeners, listener];

        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    },
    getSnapshot() {
        return relay;
    },
    getServerSnapshot() {
        return initialValue;
    }
};

const emitChange = () => {
    for (let listener of listeners) {
        listener();
    }
};

Context 設定

export type RelayProps = {
    relay: number;
};

export const RelayContext = createContext({
    relay: 0,
    setRelay: (_: RelayProps['relay']) => {},
});

Parent.tsx

export default function Parent() {
    const [ relay, setRelay ] = useState(0);

    return (
        <RelayContext value={{ relay, setRelay }}>
            <Child00 />
        </RelayContext>
    )
};

Child00.tsx

export default function Child00() {
    const { setRelay } = useContext(RelayContext);
    const [ relayStart, setRelayStart ] = useState(0);

    const handleClick = () => {
        var start = relayStart + 1;
        recordStart('RelayProps');
        setRelayStart(start);

        recordStart('RelayContext');
        setRelay(start);

        recordStart('RelayStore');
        reactRelayStore.setRelay(start);
    };

    return (
        <>
            <fieldset>
                <button onClick={ handleClick }>バケツリレー開始</button>
            </fieldset>
            <Child01 relay={ relayStart } />
        </>
    )
};

Child01.tsx

Child09.tsx まで割愛します。 memo() なし・p なし版です。

export default function Child01(props: RelayProps) {
    return <Child02 relay={ props.relay } />;
};

memo() あり・p なし版です。

const Child01 = React.memo(function Child01(props: RelayProps) {
    return <Child02 relay={ props.relay } />;
});
export default Child01;

memo() なし・p あり版です。

export default function Child01(props: RelayProps) {
    return (
        <>
            <Child02 relay={ props.relay } />
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
        </>
    );
};

memo() あり・p あり版です。

const Child01 = React.memo(function Child01(props: RelayProps) {
    return (
        <>
            <Child02 relay={ props.relay } />
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
            <p>{ props.relay }</p>
        </>
    );
});
export default Child01;

Child10.tsx

export default function Child10(props: RelayProps) {
    const { relay } = useContext(RelayContext);
    const relayStore = useSyncExternalStore(reactRelayStore.subscribe, reactRelayStore.getSnapshot, reactRelayStore.getServerSnapshot);

    useEffect(() => {
        if (!props.relay) return;

        recordFinished('RelayProps');
    }, [ props.relay ]);

    useEffect(() => {
        if (!relay) return;

        recordFinished('RelayContext');
    }, [ relay ]);

    useEffect(() => {
        if (!relayStore) return;

        recordFinished('RelayStore');
    }, [ relayStore ]);

    return null;
};

おまけの Qwik

前回は QRL 関数のダウンロード時間を含めるために、useVisibleTask$ で先に計測開始用関数をダウンロードしておき、残りは別 QRL にして実行時間に加えた流れを踏襲しています。OnMount コンポーネントは前回と同じです。

Receipt.tsx

export default component$(() => {
    const amount = useSignal(0);

    const quantity = 2;

    const subTotal = useComputed$(() => amount.value * quantity);

    const tax = useComputed$(() => subTotal.value / 10);

    const total = useComputed$(() => subTotal.value + tax.value);

    const handleStart$ = $((el: HTMLInputElement) => {
        amount.value = +el.value;
    });
    const handleChange$ = $((el?: HTMLInputElement) => {
        if (!el) return;

        recordStart('Receipt');
        handleStart$(el);
    });
    useVisibleTask$(() => {
        handleChange$();
    });

    return (
        <>
            <fieldset>
                <span>金額</span>
                <input type="number" value={ amount.value } onChange$={ (_, el) => handleChange$(el) } />
            </fieldset>
            <fieldset>
                <span>小計</span>
                <span>{ subTotal.value }</span>
            </fieldset>
            <fieldset>
                <span>消費税</span>
                <span>{ tax.value }</span>
            </fieldset>
            <fieldset>
                <span>合計</span>
                <span>{ total.value }</span>
            </fieldset>
            <OnMount start={ amount.value } target="Receipt" />
        </>
    )
});

ReactiveTypes.tsx

export default component$(() => {
    const start = useSignal(0);

    const handleClick$ = $(() => {
        start.value++;
    });

    return (
        <>
            <fieldset>
                <button onClick$={ handleClick$ }>リアクティブ構造差の計測開始</button>
            </fieldset>
            <Primitive start={ start.value } />
            <NestObject start={ start.value } />
            <Flags start={ start.value } />
        </>
    )
});

Primitive.tsx

export default component$((props: ReactiveTypeProps) => {
    const reactive = useSignal(0);

    useTask$(({ track }) => {
        track(() => props.start > 0);

        recordStart('Primitive');
        reactive.value = props.start;
    });

    useTask$(({ track }) => {
        track(() => reactive.value > 0);

        recordFinished('Primitive');
    });

    return null;
});

NestObject.tsx

export default component$((props: ReactiveTypeProps) => {
    const reactive = useStore({
        dummy01a: { dummy02a: { dummy03a: { dummy04a: { dummy05a: 0 } } } },
        // dummy01y まで同じ構造が続きます
        active01z: { active02z: { active03z: { active04z: { active05z: 0 } } } },
    });

    useTask$(({ track }) => {
        track(() => props.start > 0);

        recordStart('NestObject');
        reactive.active01z.active02z.active03z.active04z.active05z = props.start;
    });

    useTask$(({ track }) => {
        track(() => reactive.active01z.active02z.active03z.active04z.active05z > 0);

        recordFinished('NestObject');
    });

    return null;
});

Flags.tsx

export default component$((props: ReactiveTypeProps) => {
    const reactive = useStore({
        dummy01a: 0,
        // dummy01y まで同じ構造が続きます
        active01z: 0,
    });

    useTask$(({ track }) => {
        track(() => props.start > 0);

        recordStart('Flags');
        reactive.active01z = props.start;
    });

    useTask$(({ track }) => {
        track(() => reactive.active01z > 0);

        recordFinished('Flags');
    });

    return null;
});

グローバルストア設定

Qwik はグローバルストア無しです。

Context 設定

export type RelayData = number;
export type RelayProps = {
    relay: RelayData;
};
export const relayData = createContextId<Signal<RelayData>>('relayData');

Parent.tsx

export default component$(() => {
    const relayDataSignal = useSignal<RelayData>(0);
    useContextProvider(relayData, relayDataSignal);

    return <Child00 />;
});

Child00.tsx

export default component$(() => {
    const relayDataContext = useContext(relayData);
    const relayStart = useSignal(0);

    const handleStart$ = $(() => {
        const start = relayStart.value + 1;
        recordStart('RelayProps');
        relayStart.value = start;

        recordStart('RelayContext');
        relayDataContext.value++;
    });
    const handleClick$ = $((isActive: boolean = false) => {
        if (!isActive) return;

        handleStart$();
    });

    useVisibleTask$(() => {
        handleClick$();
    });

    return (
        <>
            <fieldset>
                <button onClick$={ () => handleClick$(true) }>バケツリレー開始</button>
            </fieldset>
            <Child01 relay={ relayStart.value } />
        </>
    )
});

Child01.tsx

Solid 版と component$ の表記が違うだけなので割愛します。

Child10.tsx

export default component$((props: RelayProps) => {
    const relayDataContext = useContext(relayData);

    useTask$(({ track }) => {
        track(() => props.relay);

        recordFinished('RelayProps');
    });

    useTask$(({ track }) => {
        track(() => relayDataContext.value);

        recordFinished('RelayContext');
    });

    return null;
});

計測結果

バケツリレーはテンプレート無し(Vue だけ例外)で構成しましたが、描画対象があったらどうなるんだろう、と思って、以下の構成でも計測しました(Child02 ですが、同じように p を 10 個入れたものを Child10 まで続け、合計 100 個の p としました)。 また、バケツリレーでの React は、全てが再レンダリング対象ですが、念のため memo() ありも計測しました。

<Child02 relay={ props.relay } />
<p>{ props.relay }</p>
<p>{ props.relay }</p>
<p>{ props.relay }</p>
<p>{ props.relay }</p>
<p>{ props.relay }</p>
<p>{ props.relay }</p>
<p>{ props.relay }</p>
<p>{ props.relay }</p>
<p>{ props.relay }</p>
<p>{ props.relay }</p>

Solid

1 回目

  • Receipt: 0.2999999523162842 ミリ秒 / リロード直後
  • Receipt: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • Primitive: 0.5 ミリ秒 / リロード直後
  • Primitive: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • NestObject: 0.6999999284744263 ミリ秒 / リロード直後
  • NestObject: 0 ミリ秒 / ウォームアップ後
  • Flags: 0.3999999761581421 ミリ秒 / リロード直後
  • Flags: 0.2999999523162842 ミリ秒 / ウォームアップ後
  • RelayProps: 0.19999992847442627 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 0.09999990463256836 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 0.10000002384185791 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0.10000002384185791 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 0 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 0.10000002384185791 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 0.8999999761581421 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 0.6000000238418579 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 0 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0.10000002384185791 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 0.10000002384185791 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 0 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

2 回目

  • Receipt: 0.3999999761581421 ミリ秒 / リロード直後
  • Receipt: 0.19999992847442627 ミリ秒 / ウォームアップ後
  • Primitive: 0.30000007152557373 ミリ秒 / リロード直後
  • Primitive: 0.2999999523162842 ミリ秒 / ウォームアップ後
  • NestObject: 0.3999999761581421 ミリ秒 / リロード直後
  • NestObject: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • Flags: 0.2999999523162842 ミリ秒 / リロード直後
  • Flags: 0.5 ミリ秒 / ウォームアップ後
  • RelayProps: 0.09999990463256836 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 0 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 0 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 0 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 0 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 0.7000000476837158 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 0.5 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 0 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 0 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 0 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

3 回目

  • Receipt: 0.3999999761581421 ミリ秒 / リロード直後
  • Receipt: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • Primitive: 0.3999999761581421 ミリ秒 / リロード直後
  • Primitive: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • NestObject: 0.6999999284744263 ミリ秒 / リロード直後
  • NestObject: 0 ミリ秒 / ウォームアップ後
  • Flags: 0.3999999761581421 ミリ秒 / リロード直後
  • Flags: 0.30000007152557373 ミリ秒 / ウォームアップ後
  • RelayProps: 0.19999992847442627 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 0 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 0 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0.10000002384185791 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 0.10000002384185791 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 0 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 0.7000000476837158 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 0.5 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 0.10000002384185791 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 0 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 0 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

平均

  • Receipt: 0.3666666349 ミリ秒 / リロード直後
  • Receipt: 0.1666666667 ミリ秒 / ウォームアップ後
  • Primitive: 0.4000000159 ミリ秒 / リロード直後
  • Primitive: 0.2333333492 ミリ秒 / ウォームアップ後
  • NestObject: 0.5999999444 ミリ秒 / リロード直後
  • NestObject: 0.03333334128 ミリ秒 / ウォームアップ後
  • Flags: 0.3999999761581421 ミリ秒 / リロード直後
  • Flags: 0.3666666349 ミリ秒 / ウォームアップ後
  • RelayProps: 0.1666665872 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 0.03333330154 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 0.03333334128 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0.06666668256 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 0.03333334128 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 0.03333334128 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 0.7666666905 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 0.5333333413 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 0 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0.03333334128 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 0.03333334128 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 0 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

Svelte

1 回目

  • Receipt: 0.5 ミリ秒 / リロード直後
  • Receipt: 0.2999999523162842 ミリ秒 / ウォームアップ後
  • Primitive: 0.19999992847442627 ミリ秒 / リロード直後
  • Primitive: 0 ミリ秒 / ウォームアップ後
  • NestObject: 0.20000004768371582 ミリ秒 / リロード直後
  • NestObject: 0 ミリ秒 / ウォームアップ後
  • Flags: 0 ミリ秒 / リロード直後
  • Flags: 0 ミリ秒 / ウォームアップ後
  • RelayProps: 0.40000009536743164 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 0.19999992847442627 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 0.5 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0.30000007152557373 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 0.3999999761581421 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 0.19999992847442627 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 0.7999999523162842 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 0.5 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 0.7000000476837158 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0.6000000238418579 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 0.5 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 0.3999999761581421 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

2 回目

  • Receipt: 0.5 ミリ秒 / リロード直後
  • Receipt: 0.2999999523162842 ミリ秒 / ウォームアップ後
  • Primitive: 0.10000002384185791 ミリ秒 / リロード直後
  • Primitive: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • NestObject: 0.10000002384185791 ミリ秒 / リロード直後
  • NestObject: 0 ミリ秒 / ウォームアップ後
  • Flags: 0 ミリ秒 / リロード直後
  • Flags: 0 ミリ秒 / ウォームアップ後
  • RelayProps: 0.5 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 0.20000004768371582 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 0.3999999761581421 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0.30000007152557373 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 0.2999999523162842 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 0.2999999523162842 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 0.6000000238418579 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 0.5 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 0.5 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0.6999999284744263 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 0.6000000238418579 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 0.5 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

3 回目

  • Receipt: 0.3999999761581421 ミリ秒 / リロード直後
  • Receipt: 0.30000007152557373 ミリ秒 / ウォームアップ後
  • Primitive: 0.10000002384185791 ミリ秒 / リロード直後
  • Primitive: 0 ミリ秒 / ウォームアップ後
  • NestObject: 0.2999999523162842 ミリ秒 / リロード直後
  • NestObject: 0 ミリ秒 / ウォームアップ後
  • Flags: 0 ミリ秒 / リロード直後
  • Flags: 0 ミリ秒 / ウォームアップ後
  • RelayProps: 0.7000000476837158 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 0.10000002384185791 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 0.5 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0.2999999523162842 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 0.3999999761581421 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 0.19999992847442627 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 0.7999999523162842 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 0.3999999761581421 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 0.6000000238418579 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0.3999999761581421 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 0.5999999046325684 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 0.3999999761581421 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

平均

  • Receipt: 0.3666666349 ミリ秒 / リロード直後
  • Receipt: 0.2999999921 ミリ秒 / ウォームアップ後
  • Primitive: 0.1333333254 ミリ秒 / リロード直後
  • Primitive: 0.03333334128 ミリ秒 / ウォームアップ後
  • NestObject: 0.2000000079 ミリ秒 / リロード直後
  • NestObject: 0 ミリ秒 / ウォームアップ後
  • Flags: 0 ミリ秒 / リロード直後
  • Flags: 0 ミリ秒 / ウォームアップ後
  • RelayProps: 0.533333381 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 0.1666666667 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 0.4666666587 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0.3000000318 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 0.3666666349 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 0.2333332698 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 0.7333333095 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 0.4666666587 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 0.6000000238 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0.5666666428 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 0.5666666428 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 0.4333333174 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

Vue

1 回目

  • Receipt: 0.7000000476837158 ミリ秒 / リロード直後
  • Receipt: 0.5 ミリ秒 / ウォームアップ後
  • PrimitiveRef: 0 ミリ秒 / リロード直後
  • PrimitiveRef: 0 ミリ秒 / ウォームアップ後
  • PrimitiveShallowRef: 0.20000004768371582 ミリ秒 / リロード直後
  • PrimitiveShallowRef: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • NestObjectRef: 0.10000002384185791 ミリ秒 / リロード直後
  • NestObjectRef: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • NestObjectReactive: 0.20000004768371582 ミリ秒 / リロード直後
  • NestObjectReactive: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • NestObjectShallowRef: 0.30000007152557373 ミリ秒 / リロード直後
  • NestObjectShallowRef: 0.3999999761581421 ミリ秒 / ウォームアップ後
  • FlagsRef: 0.10000002384185791 ミリ秒 / リロード直後
  • FlagsRef: 0 ミリ秒 / ウォームアップ後
  • FlagsReactive: 0.20000004768371582 ミリ秒 / リロード直後
  • FlagsReactive: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • FlagsShallowRef: 0.3999999761581421 ミリ秒 / リロード直後
  • FlagsShallowRef: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • RelayProps: 2.100000023841858 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 1.2999999523162842 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 1.2000000476837158 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0.3999999761581421 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 1.600000023841858 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 0.7999999523162842 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 2 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 1.100000023841858 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 1.399999976158142 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0.5 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 1.5999999046325684 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 0.7999999523162842 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

2 回目

  • Receipt: 0.9000000953674316 ミリ秒 / リロード直後
  • Receipt: 0.5 ミリ秒 / ウォームアップ後
  • PrimitiveRef: 0.10000002384185791 ミリ秒 / リロード直後
  • PrimitiveRef: 0 ミリ秒 / ウォームアップ後
  • PrimitiveShallowRef: 0.30000007152557373 ミリ秒 / リロード直後
  • PrimitiveShallowRef: 0.19999992847442627 ミリ秒 / ウォームアップ後
  • NestObjectRef: 0.10000002384185791 ミリ秒 / リロード直後
  • NestObjectRef: 0 ミリ秒 / ウォームアップ後
  • NestObjectReactive: 0.20000004768371582 ミリ秒 / リロード直後
  • NestObjectReactive: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • NestObjectShallowRef: 0.30000007152557373 ミリ秒 / リロード直後
  • NestObjectShallowRef: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • FlagsRef: 0.10000002384185791 ミリ秒 / リロード直後
  • FlagsRef: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • FlagsReactive: 0.20000004768371582 ミリ秒 / リロード直後
  • FlagsReactive: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • FlagsShallowRef: 0.10000002384185791 ミリ秒 / リロード直後
  • FlagsShallowRef: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • RelayProps: 2 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 1.399999976158142 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 1.100000023841858 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0.6000000238418579 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 1.5 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 1 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 2.5 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 1.399999976158142 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 1.7000000476837158 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0.5999999046325684 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 2 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 1 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

3 回目

  • Receipt: 0.4666666587 ミリ秒 / リロード直後
  • Receipt: 0.4666666587 ミリ秒 / ウォームアップ後
  • PrimitiveRef: 0.10000002384185791 ミリ秒 / リロード直後
  • PrimitiveRef: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • PrimitiveShallowRef: 0.30000001192092896 ミリ秒 / リロード直後
  • PrimitiveShallowRef: 0.30000001192092896 ミリ秒 / ウォームアップ後
  • NestObjectRef: 0.20000004768371582 ミリ秒 / リロード直後
  • NestObjectRef: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • NestObjectReactive: 0.20000004768371582 ミリ秒 / リロード直後
  • NestObjectReactive: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • NestObjectShallowRef: 0.30000001192092896 ミリ秒 / リロード直後
  • NestObjectShallowRef: 0.30000001192092896 ミリ秒 / ウォームアップ後
  • FlagsRef: 0.10000002384185791 ミリ秒 / リロード直後
  • FlagsRef: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • FlagsReactive: 0.20000004768371582 ミリ秒 / リロード直後
  • FlagsReactive: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • FlagsShallowRef: 0.20000004768371582 ミリ秒 / リロード直後
  • FlagsShallowRef: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • RelayProps: 2.5 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 1.5999999046325684 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 1.600000023841858 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0.7000000476837158 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 2 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 1.100000023841858 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 1.899999976158142 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 1.399999976158142 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 1.2000000476837158 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0.5999999046325684 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 1.399999976158142 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 1 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

平均

  • Receipt: 0.6888889339 ミリ秒 / リロード直後
  • Receipt: 0.4888888862 ミリ秒 / ウォームアップ後
  • PrimitiveRef: 0.06666668256 ミリ秒 / リロード直後
  • PrimitiveRef: 0.03333334128 ミリ秒 / ウォームアップ後
  • PrimitiveShallowRef: 0.2666667104 ミリ秒 / リロード直後
  • PrimitiveShallowRef: 0.2333333294 ミリ秒 / ウォームアップ後
  • NestObjectRef: 0.1333333651 ミリ秒 / リロード直後
  • NestObjectRef: 0.1000000238 ミリ秒 / ウォームアップ後
  • NestObjectReactive: 0.2000000477 ミリ秒 / リロード直後
  • NestObjectReactive: 0.1666667064 ミリ秒 / ウォームアップ後
  • NestObjectShallowRef: 0.3000000517 ミリ秒 / リロード直後
  • NestObjectShallowRef: 0.3000000119 ミリ秒 / ウォームアップ後
  • FlagsRef: 0.1000000238 ミリ秒 / リロード直後
  • FlagsRef: 0.06666668256 ミリ秒 / ウォームアップ後
  • FlagsReactive: 0.2000000477 ミリ秒 / リロード直後
  • FlagsReactive: 0.1666667064 ミリ秒 / ウォームアップ後
  • FlagsShallowRef: 0.2333333492 ミリ秒 / リロード直後
  • FlagsShallowRef: 0.2000000477 ミリ秒 / ウォームアップ後
  • RelayProps: 2.2000000079 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 1.4333332777 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 1.3000000318 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 0.5666666826 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 1.7000000079 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 0.9666666587 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 2.1333333254 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 1.2999999921 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 1.4333333572 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 0.5666666031 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 1.6666666269 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 0.9333333174 ミリ秒 / ウォームアップ後 / テンプレート p 100 個

React

1 回目

  • Receipt: 2.600000023841858 ミリ秒 / リロード直後
  • Receipt: 1.2000000476837158 ミリ秒 / ウォームアップ後
  • Primitive: 0.8999999761581421 ミリ秒 / リロード直後
  • Primitive: 0.8999999761581421 ミリ秒 / ウォームアップ後
  • NestObject: 1 ミリ秒 / リロード直後
  • NestObject: 0.8999999761581421 ミリ秒 / ウォームアップ後
  • Flags: 1 ミリ秒 / リロード直後
  • Flags: 0.8999999761581421 ミリ秒 / ウォームアップ後
  • RelayProps: 2.0999999046325684 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 1.5999999046325684 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 2.100000023841858 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 1.7999999523162842 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 2.3000000715255737 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 2 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 4 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayProps: 1.600000023841858 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayContext: 3.799999952316284 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayContext: 1.8000000715255737 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayStore: 4 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayStore: 1.8000000715255737 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayProps: 7.099999904632568 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 5.699999928474426 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 7 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 6 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 7.100000023841858 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 6.100000023841858 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayProps: 8.5 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayProps: 6.899999976158142 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用
  • RelayContext: 8.399999976158142 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayContext: 7.299999952316284 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用
  • RelayStore: 8.5 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayStore: 7.199999928474426 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用

2 回目

  • Receipt: 2.600000023841858 ミリ秒 / リロード直後
  • Receipt: 1.2000000476837158 ミリ秒 / ウォームアップ後
  • Primitive: 0.8000000715255737 ミリ秒 / リロード直後
  • Primitive: 1 ミリ秒 / ウォームアップ後
  • NestObject: 0.9000000953674316 ミリ秒 / リロード直後
  • NestObject: 1 ミリ秒 / ウォームアップ後
  • Flags: 1 ミリ秒 / リロード直後
  • Flags: 1 ミリ秒 / ウォームアップ後
  • RelayProps: 2.6999999284744263 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 1.399999976158142 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 2.700000047683716 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 1.5999999046325684 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 3.200000047683716 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 1.600000023841858 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 3.600000023841858 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayProps: 1.1999999284744263 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayContext: 3.5 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayContext: 1.2000000476837158 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayStore: 3.600000023841858 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayStore: 1.4000000953674316 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayProps: 7.200000047683716 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 5.5 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 7.100000023841858 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 5.600000023841858 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 7.099999904632568 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 5.700000047683716 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayProps: 8.299999952316284 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayProps: 7.100000023841858 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用
  • RelayContext: 8.200000047683716 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayContext: 7.399999976158142 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用
  • RelayStore: 8.399999976158142 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayStore: 7.600000023841858 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用

3 回目

  • Receipt: 2.5 ミリ秒 / リロード直後
  • Receipt: 2.600000023841858 ミリ秒 / ウォームアップ後
  • Primitive: 0.9000000953674316 ミリ秒 / リロード直後
  • Primitive: 0.8999999761581421 ミリ秒 / ウォームアップ後
  • NestObject: 0.8999999761581421 ミリ秒 / リロード直後
  • NestObject: 0.7999999523162842 ミリ秒 / ウォームアップ後
  • Flags: 0.8999999761581421 ミリ秒 / リロード直後
  • Flags: 0.9000000357627869 ミリ秒 / ウォームアップ後
  • RelayProps: 2.600000023841858 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 1.4000000953674316 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 3.1999999284744263 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 1.600000023841858 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 3.1999999284744263 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 1.6999999284744263 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 3.5999999046325684 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayProps: 1.5 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayContext: 3.5 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayContext: 1.600000023841858 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayStore: 3.600000023841858 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayStore: 1.7000000476837158 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayProps: 7 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 5.5 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 6.899999976158142 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 5.799999952316284 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 7 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 6 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayProps: 7.399999976158142 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayProps: 6.799999952316284 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用
  • RelayContext: 7.5 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayContext: 7 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用
  • RelayStore: 7.5 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayStore: 7.100000023841858 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用

平均

  • Receipt: 2.5666666826 ミリ秒 / リロード直後
  • Receipt: 1.6666667064 ミリ秒 / ウォームアップ後
  • Primitive: 0.8666667144 ミリ秒 / リロード直後
  • Primitive: 0.9333333174 ミリ秒 / ウォームアップ後
  • NestObject: 0.9333333572 ミリ秒 / リロード直後
  • NestObject: 0.8999999762 ミリ秒 / ウォームアップ後
  • Flags: 0.9666666587 ミリ秒 / リロード直後
  • Flags: 0.9333333373 ミリ秒 / ウォームアップ後
  • RelayProps: 2.466666619 ミリ秒 / リロード直後 / テンプレートなし
  • RelayProps: 1.4666666587 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayContext: 2.6666666667 ミリ秒 / リロード直後 / テンプレートなし
  • RelayContext: 1.6666666269 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayStore: 2.9000000159 ミリ秒 / リロード直後 / テンプレートなし
  • RelayStore: 1.7666666508 ミリ秒 / ウォームアップ後 / テンプレートなし
  • RelayProps: 3.7333333095 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayProps: 1.4333333174 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayContext: 3.5999999841 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayContext: 1.533333381 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayStore: 3.7333333492 ミリ秒 / リロード直後 / テンプレートなし / memo() 使用
  • RelayStore: 1.6333334049 ミリ秒 / ウォームアップ後 / テンプレートなし / memo() 使用
  • RelayProps: 7.09999998411 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayProps: 5.5666666428 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayContext: 7 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayContext: 5.7999999921 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayStore: 7.06666664282 ミリ秒 / リロード直後 / テンプレート p 100 個
  • RelayStore: 5.9333333572 ミリ秒 / ウォームアップ後 / テンプレート p 100 個
  • RelayProps: 8.06666664282 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayProps: 6.9333333174 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用
  • RelayContext: 8.03333334128 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayContext: 7.2333333095 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用
  • RelayStore: 8.1333333254 ミリ秒 / リロード直後 / テンプレート p 100 個 / memo() 使用
  • RelayStore: 7.2999999921 ミリ秒 / ウォームアップ後 / テンプレート p 100 個 / memo() 使用

Qwik

1 回目

  • Receipt: 6.100000023841858 ミリ秒 / QRL ダウンロード含む
  • Receipt: 1 ミリ秒 / QRL ダウンロード後
  • Primitive: 3.299999952316284 ミリ秒 / QRL ダウンロード含む
  • Primitive: 0.10000002384185791 ミリ秒 / QRL ダウンロード後
  • NestObject: 3.4000000953674316 ミリ秒 / QRL ダウンロード含む
  • NestObject: 0.2999999523162842 ミリ秒 / QRL ダウンロード後
  • Flags: 3.399999976158142 ミリ秒 / QRL ダウンロード含む
  • Flags: 0.2999999523162842 ミリ秒 / QRL ダウンロード後
  • RelayProps: 2.9000000953674316 ミリ秒 / QRL ダウンロード含む / テンプレートなし
  • RelayProps: 0.20000004768371582 ミリ秒 / QRL ダウンロード後 / テンプレートなし
  • RelayContext: 3.100000023841858 ミリ秒 / QRL ダウンロード含む / テンプレートなし
  • RelayContext: 0.3999999761581421 ミリ秒 / QRL ダウンロード後 / テンプレートなし
  • RelayProps: 3.299999952316284 ミリ秒 / QRL ダウンロード含む / テンプレート p 100 個
  • RelayProps: 0.2999999523162842 ミリ秒 / QRL ダウンロード後 / テンプレート p 100 個
  • RelayContext: 3.200000047683716 ミリ秒 / QRL ダウンロード含む / テンプレート p 100 個
  • RelayContext: 0.3999999761581421 ミリ秒 / QRL ダウンロード後 / テンプレート p 100 個

2 回目

  • Receipt: 4.700000047683716 ミリ秒 / QRL ダウンロード含む
  • Receipt: 0.6999999284744263 ミリ秒 / QRL ダウンロード後
  • Primitive: 3.299999952316284 ミリ秒 / QRL ダウンロード含む
  • Primitive: 0.10000002384185791 ミリ秒 / QRL ダウンロード後
  • NestObject: 3.299999952316284 ミリ秒 / QRL ダウンロード含む
  • NestObject: 0.30000007152557373 ミリ秒 / QRL ダウンロード後
  • Flags: 3.3000000715255737 ミリ秒 / QRL ダウンロード含む
  • Flags: 0.3999999761581421 ミリ秒 / QRL ダウンロード後
  • RelayProps: 2.100000023841858 ミリ秒 / QRL ダウンロード含む / テンプレートなし
  • RelayProps: 0.5 ミリ秒 / QRL ダウンロード後 / テンプレートなし
  • RelayContext: 2.200000047683716 ミリ秒 / QRL ダウンロード含む / テンプレートなし
  • RelayContext: 0.5 ミリ秒 / QRL ダウンロード後 / テンプレートなし
  • RelayProps: 3.299999952316284 ミリ秒 / QRL ダウンロード含む / テンプレート p 100 個
  • RelayProps: 0.30000007152557373 ミリ秒 / QRL ダウンロード後 / テンプレート p 100 個
  • RelayContext: 3.399999976158142 ミリ秒 / QRL ダウンロード含む / テンプレート p 100 個
  • RelayContext: 0.5 ミリ秒 / QRL ダウンロード後 / テンプレート p 100 個

3 回目

  • Receipt: 5.299999952316284 ミリ秒 / QRL ダウンロード含む
  • Receipt: 1.100000023841858 ミリ秒 / QRL ダウンロード後
  • Primitive: 2.9000000953674316 ミリ秒 / QRL ダウンロード含む
  • Primitive: 0.20000004768371582 ミリ秒 / QRL ダウンロード後
  • NestObject: 3.100000023841858 ミリ秒 / QRL ダウンロード含む
  • NestObject: 0.2999999523162842 ミリ秒 / QRL ダウンロード後
  • Flags: 3 ミリ秒 / QRL ダウンロード含む
  • Flags: 0.5 ミリ秒 / QRL ダウンロード後
  • RelayProps: 2.899999976158142 ミリ秒 / QRL ダウンロード含む / テンプレートなし
  • RelayProps: 0.20000004768371582 ミリ秒 / QRL ダウンロード後 / テンプレートなし
  • RelayContext: 3 ミリ秒 / QRL ダウンロード含む / テンプレートなし
  • RelayContext: 0.30000007152557373 ミリ秒 / QRL ダウンロード後 / テンプレートなし
  • RelayProps: 8.699999928474426 ミリ秒 / QRL ダウンロード含む / テンプレート p 100 個
  • RelayProps: 0.3999999761581421 ミリ秒 / QRL ダウンロード後 / テンプレート p 100 個
  • RelayContext: 8.800000071525574 ミリ秒 / QRL ダウンロード含む / テンプレート p 100 個
  • RelayContext: 0.40000009536743164 ミリ秒 / QRL ダウンロード後 / テンプレート p 100 個

平均

  • Receipt: 5.3666666746 ミリ秒 / QRL ダウンロード含む
  • Receipt: 0.9333333174 ミリ秒 / QRL ダウンロード後
  • Primitive: 3.1666666667 ミリ秒 / QRL ダウンロード含む
  • Primitive: 0.1333333651 ミリ秒 / QRL ダウンロード後
  • NestObject: 3.2666666905 ミリ秒 / QRL ダウンロード含む
  • NestObject: 0.2999999921 ミリ秒 / QRL ダウンロード後
  • Flags: 3.2333333492 ミリ秒 / QRL ダウンロード含む
  • Flags: 0.3999999762 ミリ秒 / QRL ダウンロード後
  • RelayProps: 2.6333333651 ミリ秒 / QRL ダウンロード含む / テンプレートなし
  • RelayProps: 0.3000000318 ミリ秒 / QRL ダウンロード後 / テンプレートなし
  • RelayContext: 2.7666666905 ミリ秒 / QRL ダウンロード含む / テンプレートなし
  • RelayContext: 0.4000000159 ミリ秒 / QRL ダウンロード後 / テンプレートなし
  • RelayProps: 5.09999994437 ミリ秒 / QRL ダウンロード含む / テンプレート p 100 個
  • RelayProps: 0.3333333333 ミリ秒 / QRL ダウンロード後 / テンプレート p 100 個
  • RelayContext: 5.1333333651 ミリ秒 / QRL ダウンロード含む / テンプレート p 100 個
  • RelayContext: 0.4333333572 ミリ秒 / QRL ダウンロード後 / テンプレート p 100 個

平均値ランキング

Receipt

  1. Solid ウォームアップ後: 0.1666666667 ミリ秒
  2. Svelte ウォームアップ後: 0.2999999921 ミリ秒
  3. Solid リロード直後 / Svelte: 0.3666666349 ミリ秒
  4. Vue ウォームアップ後: 0.4888888862 ミリ秒
  5. Vue リロード直後: 0.6888889339 ミリ秒
  6. Qwik QRL ダウンロード後: 0.9333333174 ミリ秒
  7. React ウォームアップ後: 1.6666667064 ミリ秒
  8. React リロード直後: 2.5666666826 ミリ秒
  9. Qwik QRL ダウンロード含む: 5.3666666746 ミリ秒

Primitive

  1. Svelte ウォームアップ後 / Vue Ref ウォームアップ後: 0.03333334128 ミリ秒
  2. Vue Ref リロード直後: 0.06666668256 ミリ秒
  3. Svelte リロード直後: 0.1333333254 ミリ秒
  4. Qwik QRL ダウンロード後: 0.1333333651 ミリ秒
  5. Vue ShallowRef ウォームアップ後: 0.2333333294 ミリ秒
  6. Solid ウォームアップ後: 0.2333333492 ミリ秒
  7. Vue ShallowRef リロード直後: 0.2666667104 ミリ秒
  8. Solid リロード直後: 0.4000000159 ミリ秒
  9. React リロード直後: 0.8666667144 ミリ秒
  10. React ウォームアップ後: 0.9333333174 ミリ秒
  11. Qwik QRL ダウンロード含む: 3.1666666667 ミリ秒

NestObject

  1. Svelte ウォームアップ後: 0 ミリ秒
  2. Solid ウォームアップ後: 0.03333334128 ミリ秒
  3. Vue Ref ウォームアップ後: 0.1000000238 ミリ秒
  4. Vue Ref リロード直後: 0.1333333651 ミリ秒
  5. Vue Reactive ウォームアップ後: 0.1666667064 ミリ秒
  6. Svelte リロード直後: 0.2000000079 ミリ秒
  7. Vue Reactive リロード直後: 0.2000000477 ミリ秒
  8. Qwik QRL ダウンロード後: 0.2999999921 ミリ秒
  9. Vue ShallowRef ウォームアップ後: 0.3000000119 ミリ秒
  10. Vue ShallowRef リロード直後: 0.3000000517 ミリ秒
  11. Solid リロード直後: 0.5999999444 ミリ秒
  12. React ウォームアップ後: 0.8999999762 ミリ秒
  13. React リロード直後: 0.9333333572 ミリ秒
  14. Qwik QRL ダウンロード含む: 3.2666666905 ミリ秒

Flags

  1. Svelteリロード直後 ウォームアップ後: 0 ミリ秒
  2. Vue Ref ウォームアップ後: 0.06666668256 ミリ秒
  3. Vue Ref リロード直後: 0.1000000238 ミリ秒
  4. Vue Reactive ウォームアップ後: 0.1666667064 ミリ秒
  5. Vue Reactive リロード直後 / Vue ShallowRef ウォームアップ後: 0.2000000477 ミリ秒
  6. Vue ShallowRef リロード直後: 0.2333333492 ミリ秒
  7. Solid ウォームアップ後: 0.3666666349 ミリ秒
  8. Solid リロード直後: 0.3999999761581421 ミリ秒
  9. Qwik QRL ダウンロード後: 0.3999999762 ミリ秒
  10. React ウォームアップ後: 0.9333333373 ミリ秒
  11. React リロード直後: 0.9666666587 ミリ秒
  12. Qwik QRL ダウンロード含む: 3.2333333492 ミリ秒

RelayProps / テンプレートなし

  1. Solid ウォームアップ後: 0.03333330154 ミリ秒
  2. Solid リロード直後: 0.1666665872 ミリ秒
  3. Svelte ウォームアップ後: 0.1666666667 ミリ秒
  4. Qwik QRL ダウンロード後: 0.3000000318 ミリ秒
  5. Svelte リロード直後: 0.533333381 ミリ秒
  6. Vue ウォームアップ後: 1.4333332777 ミリ秒
  7. React ウォームアップ後 / memo() 使用: 1.4333333174 ミリ秒
  8. React ウォームアップ後: 1.4666666587 ミリ秒
  9. Vue リロード直後: 2.2000000079 ミリ秒
  10. React リロード直後: 2.466666619 ミリ秒
  11. Qwik QRL ダウンロード含む: 2.6333333651 ミリ秒
  12. React リロード直後 / memo() 使用: 3.7333333095 ミリ秒

RelayProps / テンプレート p 100 個

  1. Qwik QRL ダウンロード後: 0.3333333333 ミリ秒
  2. Svelte ウォームアップ後: 0.4666666587 ミリ秒
  3. Solid ウォームアップ後: 0.5333333413 ミリ秒
  4. Svelte リロード直後: 0.7333333095 ミリ秒
  5. Solid リロード直後: 0.7666666905 ミリ秒
  6. Vue ウォームアップ後: 1.2999999921 ミリ秒
  7. Vue リロード直後: 2.1333333254 ミリ秒
  8. Qwik QRL ダウンロード含む: 5.09999994437 ミリ秒
  9. React ウォームアップ後: 5.5666666428 ミリ秒
  10. React ウォームアップ後 / memo() 使用: 6.9333333174 ミリ秒
  11. React リロード直後: 7.09999998411 ミリ秒
  12. React リロード直後 / memo() 使用: 8.06666664282 ミリ秒

RelayContext / テンプレートなし

  1. Solid リロード直後: 0.03333334128 ミリ秒
  2. Solid ウォームアップ後: 0.06666668256 ミリ秒
  3. Svelte ウォームアップ後: 0.3000000318 ミリ秒
  4. Qwik QRL ダウンロード後: 0.4000000159 ミリ秒
  5. Svelte リロード直後: 0.4666666587 ミリ秒
  6. Vue ウォームアップ後: 0.5666666826 ミリ秒
  7. Vue リロード直後: 1.3000000318 ミリ秒
  8. React ウォームアップ後 / memo() 使用: 1.533333381 ミリ秒
  9. React ウォームアップ後: 1.6666666269 ミリ秒
  10. React リロード直後: 2.6666666667 ミリ秒
  11. Qwik QRL ダウンロード含む: 2.7666666905 ミリ秒
  12. React リロード直後 / memo() 使用: 3.5999999841 ミリ秒

RelayContext / テンプレート p 100 個

  1. Solid リロード直後: 0 ミリ秒
  2. Solid ウォームアップ後: 0.03333334128 ミリ秒
  3. Qwik QRL ダウンロード後: 0.4333333572 ミリ秒
  4. Svelte ウォームアップ後: 0.5666666428 ミリ秒
  5. Svelte リロード直後: 0.6000000238 ミリ秒
  6. Vue ウォームアップ後: 0.5666666031 ミリ秒
  7. Vue リロード直後: 1.4333333572 ミリ秒
  8. Qwik QRL ダウンロード含む: 5.1333333651 ミリ秒
  9. React ウォームアップ後: 5.7999999921 ミリ秒
  10. React リロード直後: 7 ミリ秒
  11. React ウォームアップ後 / memo() 使用: 7.2333333095 ミリ秒
  12. React リロード直後 / memo() 使用: 8.03333334128 ミリ秒

RelayStore / テンプレートなし

  1. Solid リロード直後 / ウォームアップ後: 0.03333334128 ミリ秒
  2. Svelte ウォームアップ後: 0.2333332698 ミリ秒
  3. Svelte リロード直後: 0.3666666349 ミリ秒
  4. Vue ウォームアップ後: 0.9666666587 ミリ秒
  5. Vue リロード直後: 1.7000000079 ミリ秒
  6. React ウォームアップ後 / memo() 使用: 1.6333334049 ミリ秒
  7. React ウォームアップ後: 1.7666666508 ミリ秒
  8. React リロード直後: 2.9000000159 ミリ秒
  9. React リロード直後 / memo() 使用: 3.7333333492 ミリ秒

RelayStore / テンプレート p 100 個

  1. Solid ウォームアップ後: 0 ミリ秒
  2. Solid リロード直後: 0.03333334128 ミリ秒
  3. Svelte ウォームアップ後: 0.4333333174 ミリ秒
  4. Svelte リロード直後: 0.5666666428 ミリ秒
  5. Vue ウォームアップ後: 0.9333333174 ミリ秒
  6. Vue リロード直後: 1.6666666269 ミリ秒
  7. React ウォームアップ後: 5.9333333572 ミリ秒
  8. React リロード直後: 7.06666664282 ミリ秒
  9. React ウォームアップ後 / memo() 使用: 7.2999999921 ミリ秒
  10. React リロード直後 / memo() 使用: 8.1333333254 ミリ秒

今回も、いずれも 3 回ずつの結果で、有意な数値とは言えませんが、やってみました。 Signal 勢も、描画対象が入ると巻き添えを食らってるのかな、と思いました。 バケツリレーは他の手段よりも速いものの、ぼくは肯定派ではないです。 前回に続き、面白い実験でした。

同一 Astro 上で Solid / Svelte / Vue / React それぞれの処理速度を計測してみた(おまけで Qwik)

表題について、なんとなく「計測してみよう」と思って、やってみました。

計測項目をこの 3 種類にしました。

  • グローバルストア伝播時間
  • client:visible でのハイドレーション時間
  • 再レンダリング時間

1 つめのグローバルストアは、他のコンポーネントに伝播するまでの時間を取ってみたかったです。 2 つめはのハイドレーション時間は、client:visible でも DOM 描画はされているので、JS 実行までの時間を計測しようと思いました。 3 つめの再レンダリング時間は、コンポーネント内最上部にある input で値を変えて最下部にある p の内容が反映されるまでの時間を見てみました。

Solid / Svelte / Vue のグローバルストアについては、先日の仕組みを使いました。React はこちらのコードで共通といかなかったので、useSyncExternalStore() を使ったグローバルストアで計測しました。 wings.hatenablog.com

なお、scrollIntoView() が発動すると client:visible のコンポーネントが見えるようになるので、そのコンポーネントでの onMount() などでの実行が終わるまでの時間を、便宜上「ハイドレーション」と呼ぶことにしました。

ちなみに続編です。

wings.hatenablog.com

wings.hatenablog.com

では、早速いきましょう。 長くなってしまうので import 文は割愛します。また、他の割愛部分は、都度記載します。

共通

records の各項目に、performance.now() で取得した時間を格納し、計測時に差分を取った時間を結果に利用します。 簡単に実験用コンポーネントを作るため、自分のサイトで使っている月別画像をここでも利用しようと思い、picKeys には画像のファイル名となるキーのみを設定しています。

_models/performance.ts

export const records = {
    GlobalSignal: {
        start: 0,
        finished: 0,
    },
    Hydration: {
        start: 0,
        finished: 0,
    },
    ReRender: {
        start: 0,
        finished: 0,
    },
};
type Records = keyof typeof records;
const now = () => performance.now();
export const recordStart = (target: Records) => {
    records[target].start = now();
};
export const recordFinished = (target: Records) => {
    if (records[target].start === 0) return;

    records[target].finished = now();
    const diff = records[target].finished - records[target].start;
    console.log(`${target}: ${diff} ミリ秒`);
};

export const picKeys = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] as const;

performance/Solid.astro

Astro 側です。以下は Solid 版ですが、Svelte / Vue / React も同じ構成です。

<div id="performance">
    <section>
        <Anchor to="pics" client:load>スクロールします</Anchor>
    </section>
    <At id="pics" client:load />
    <Pics client:visible />
</div>

CSS

これだけです。client:visible になるような設定が主です。

#performance {
    width: 750px;
    margin: 0 auto;
}
#performance section {
    min-height: 100vh;
}
#performance img {
    max-width: 100%;
}

Solid 版

グローバルストア用設定

type AtId = string;
const initialValue: AtId = '';

export const _solidAtId = {
    server: {
        solidAtId: (): AtId => initialValue,
        solidSetAtId: (_: AtId) => {},
    },
    client: () => {
        const [ solidAtId, solidSetAtId ] = createSignal<AtId>(initialValue);

        return { solidAtId, solidSetAtId };
    },
};

Anchor.tsx

ストア伝播時間の開始を行います。

type Props = {
    to: string;
    children: JSX.Element;
};

export default function Anchor(props: Props) {
    const { solidSetAtId } = useStore('solidAtId');

    const handleClick = () => {
        recordStart('GlobalSignal');
        solidSetAtId(props.to);
    };

    return (
        <button onClick={ handleClick }>
            { props.children }
        </button>
    )
};

At.tsx

ストア伝播時間の終了と、ハイドレーション時間の開始を行います。

type Props = {
    id: string;
};

export default function At(props: Props) {
    const { solidAtId, solidSetAtId } = useStore('solidAtId');

    let el: HTMLSpanElement | undefined;

    createEffect(() => {
        if (!el || props.id !== solidAtId()) return;

        el.scrollIntoView({ behavior: 'smooth' });
        recordFinished('GlobalSignal');
        solidSetAtId('');
        recordStart('Hydration');
    });

    return (
        <span ref={ el } id={ `at_${props.id}` } class="invisible_target" />
    )
}

Pic.tsx

ハイドレーション用に深めの DOM に画像などを配置する個別のコンポーネントです。

type Props = {
    picKey: string;
};

export default function Pic(props: Props) {
    return (
        <div class="pic01">
            <div class="pic02">
                <div class="pic03">
                    <div class="pic04">
                        <div class="pic05">
                            <p>{ props.picKey } の画像</p>
                        </div>
                    </div>
                </div>
            </div>
            <div class="pic02">
                <div class="pic03">
                    <div class="pic04">
                        <div class="pic05">
                            <img src={ `/images/performance/${props.picKey}.webp` } alt={ `${props.picKey} の alt` } />
                        </div>
                    </div>
                </div>
            </div>
            <div class="pic02">
                <div class="pic03">
                    <div class="pic04">
                        <div class="pic05">
                            <img src={ `/images/performance/${props.picKey}_sp.webp` } alt={ `${props.picKey}_sp の alt` } />
                        </div>
                    </div>
                </div>
            </div>
        </div>
    )
}

Pics.tsx

Pic.tsx をまとめ、ハイドレーション時間の終了・再レンダリング用の input と p を配置・それらのの開始と終了を行います。

export default function Pics() {
    const [ text, setText ] = createSignal('');

    let elParagraph: HTMLParagraphElement | undefined;

    onMount(() => {
        recordFinished('Hydration');
    });
    const handleChange = (e: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement; }) => {
        recordStart('ReRender');
        setText(e.currentTarget.value);
    };
    createEffect(() => {
        if (elParagraph?.textContent !== text()) return;

        recordFinished('ReRender');
    });

    return (
        <form id="pics">
            <fieldset>
                <input type="text" onChange={ handleChange } />
            </fieldset>
            <Index each={ picKeys }>
                {picKey => (
                    <Pic picKey={ picKey() } />
                )}
            </Index>
            <fieldset>
                <p ref={ elParagraph }>{ text() }</p>
            </fieldset>
        </form>
    )
}

Svelte 版

Solid 版とやってることは変わりません。

グローバルストア用設定

$state() を使えるように、.svelte.ts です。

type AtId = string;
const initialValue: AtId = '';

export const _svelteAtId = {
    server: {
        svelteAtId: (): AtId => initialValue,
        svelteSetAtId: (_: AtId) => {},
    },
    client: () => {
        let svelteAtId = $state<AtId>(initialValue);
        const svelteSetAtId = (value: AtId) => { svelteAtId = value; };

        return { svelteAtId: () => svelteAtId, svelteSetAtId };
    },
};

Anchor.svelte

<script lang="ts">
    type Props = {
        to: string;
        children: Snippet;
    };
    const { to, children }: Props = $props();
    const { svelteSetAtId } = useStore('svelteAtId');

    const handleClick = () => {
        recordStart('GlobalSignal');
        svelteSetAtId(to);
    };
</script>
<button onclick={ handleClick }>
    {@render children()}
</button>

At.svelte

<script lang="ts">
    type Props = {
        id: string;
    };
    const { id }: Props = $props();

    const { svelteAtId, svelteSetAtId } = useStore('svelteAtId');

    let el: HTMLSpanElement | undefined;

    $effect(() => {
        if (!el || id !== svelteAtId()) return;

        el.scrollIntoView({ behavior: 'smooth' });
        recordFinished('GlobalSignal');
        svelteSetAtId('');
        recordStart('Hydration');
    });
</script>
<span bind:this={ el } id={ `at_${id}` } class="invisible_target"></span>

Pic.svelte

Solid 版と同じなので、props の部分のみ記載し、テンプレート部分は割愛します。

<script lang="ts">
    type Props = {
        picKey: string;
    };
    const { picKey }: Props = $props();
</script>

Pics.svelte

<script lang="ts">
    let text = $state('');
    let elParagraph: HTMLParagraphElement | undefined;

    onMount(() => {
        recordFinished('Hydration');
    });
    const handleChange = async (e: Event & { currentTarget: EventTarget & HTMLInputElement; }) => {
        recordStart('ReRender');
        text = e.currentTarget.value;
    };
    $effect(() => {
        if (elParagraph?.textContent !== text) return;

        recordFinished('ReRender');
    });
</script>
<form id="pics">
    <fieldset>
        <input type="text" onchange={ handleChange } />
    </fieldset>
    {#each picKeys as picKey (picKey)}
        <Pic picKey={ picKey } />
    {/each}
    <fieldset>
        <p bind:this={ elParagraph }>{ text }</p>
    </fieldset>
</form>

Vue 版

こちらも Solid 版とやってることは変わりません。

グローバルストア用設定

type AtId = string;
const initialValue: AtId = '';

export const _vueAtId = {
    server: {
        vueAtId: (): AtId => initialValue,
        vueSetAtId: (_: AtId) => {},
    },
    client: () => {
        const vueAtId = ref<AtId>(initialValue);
        const vueSetAtId = (value: AtId) => { vueAtId.value = value; };

        return { vueAtId: () => vueAtId.value, vueSetAtId };
    },
};

Anchor.vue

<script setup lang="ts">
   type Props = {
        to: string;
    };
   const props = defineProps<Props>();
   const { vueSetAtId } = useStore('vueAtId');

   const handleClick = () => {
        recordStart('GlobalSignal');
        vueSetAtId(props.to);
    };
</script>
<template>
    <button @click="handleClick">
        <slot />
    </button>
</template>

At.vue

<script setup lang="ts">
   type Props = {
        id: string;
    };
   const props = defineProps<Props>();

   const { vueAtId, vueSetAtId } = useStore('vueAtId');

   const el = useTemplateRef('el');

   watch(
        () => vueAtId(),
        () => {
            if (!el || !el.value || props.id !== vueAtId()) return;

            el.value.scrollIntoView({ behavior: 'smooth' });
            recordFinished('GlobalSignal');
            vueSetAtId('');
            recordStart('Hydration');
        },
        { immediate: true }
    );
</script>
<template>
    <span ref="el" :id="`at_${id}`" class="invisible_target"></span>
</template>

Pic.vue

お作法はやや違いますが、Solid 版とほぼ同じなので、props の部分のみ記載し、テンプレート部分は割愛します。

<script setup lang="ts">
   type Props = {
        picKey: string;
    };
   const props = defineProps<Props>();
</script>

Pics.vue

<script setup lang="ts">
   const text = ref('');
   const elParagraph = useTemplateRef('elParagraph');

   onMounted(() => {
        recordFinished('Hydration');
    });
   const handleChange = async (e: Event) => {
        if (!e.currentTarget || !(e.currentTarget instanceof HTMLInputElement)) return;

        recordStart('ReRender');
        text.value = e.currentTarget.value;
    };
   watchEffect(() => {
        if (elParagraph.value?.textContent !== text.value) return;

        recordFinished('ReRender');
    }, { flush: 'post' });
</script>
<template>
    <form id="pics">
        <fieldset>
            <input type="text" @change="handleChange" />
        </fieldset>
        <Pic v-for="key in picKeys" :key="key" :pic-key="key" />
        <fieldset>
            <p ref="elParagraph">{{ text }}</p>
        </fieldset>
    </form>
</template>

React 版

Pics コンポーネントについては、memo() / useMemo() / children 包囲、の 3 種類を計測対象に加えました。

グローバルストア用設定

const initialValue = '';
let atId = initialValue;
type AtId = typeof atId;
let listeners: (() => void)[] = [];
type Listener = (typeof listeners)[number];

export const reactAtIdStore = {
    setAtId(value: AtId) {
        atId = value;
        emitChange();
    },
    subscribe(listener: Listener) {
        listeners = [...listeners, listener];

        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    },
    getSnapshot() { return atId; },
    getServerSnapshot() { return initialValue; }
};

const emitChange = () => {
    for (let listener of listeners) {
        listener();
    }
};

Anchor.tsx

type Props = {
    to: string;
    children: ReactNode;
};

export default function Anchor(props: Props) {
    const handleClick = () => {
        recordStart('GlobalSignal');
        reactAtIdStore.setAtId(props.to);
    };

    return (
        <button onClick={ handleClick }>
            { props.children }
        </button>
    )
};

At.tsx

type Props = {
    id: string;
};

export default function At(props: Props) {
    const atId = useSyncExternalStore(reactAtIdStore.subscribe, reactAtIdStore.getSnapshot, reactAtIdStore.getServerSnapshot);

    const el = useRef<HTMLSpanElement | null>(null);

    useEffect(() => {
        if (!el || !el.current || props.id !== atId) return;

        el.current.scrollIntoView({ behavior: 'smooth' });
        recordFinished('GlobalSignal');
        reactAtIdStore.setAtId('');
        recordStart('Hydration');
    }, [ atId ]);

    return (
        <span ref={ el } id={ `at_${props.id}` } className="invisible_target" />
    )
}

Pic.tsx

Solid 版と全く同じなので全て割愛します。

Pics.tsx

こちらは、最適化していない版です。

export default function Pics() {
    const [ text, setText ] = useState('');

    const elParagraph = useRef<HTMLParagraphElement | null>(null);

    useEffect(() => {
        recordFinished('Hydration');
    }, []);

    const handleChange = (e: React.ChangeEvent<HTMLInputElement, HTMLInputElement>) => {
        recordStart('ReRender');
        setText(e.currentTarget.value);
    };

    useEffect(() => {
        if (!elParagraph || !elParagraph.current) return;

        if (elParagraph.current.textContent === text) recordFinished('ReRender');
    }, [ text ]);

    return (
        <form id="pics">
            <fieldset>
                <input type="text" onChange={ handleChange } />
            </fieldset>
            {picKeys.map(picKey => (
                <Pic key={ picKey } picKey={ picKey } />
            ))}
            <fieldset>
                <p ref={ elParagraph }>{ text }</p>
            </fieldset>
        </form>
    )
}

2 番目は memo() によって最適化した版です。

const ReactMemoPics = memo(({ keys }: { keys: typeof picKeys }) => {
    return (
        <>
            {keys.map(picKey => (
                <Pic key={ picKey } picKey={ picKey } />
            ))}
        </>
    );
});

export default function Pics() {
    const [ text, setText ] = useState('');

    const elParagraph = useRef<HTMLParagraphElement | null>(null);

    useEffect(() => {
        recordFinished('Hydration');
    }, []);

    const handleChange = (e: React.ChangeEvent<HTMLInputElement, HTMLInputElement>) => {
        recordStart('ReRender');
        setText(e.currentTarget.value);
    };

    useEffect(() => {
        if (!elParagraph || !elParagraph.current) return;

        if (elParagraph.current.textContent === text) recordFinished('ReRender');
    }, [ text ]);

    return (
        <form id="pics">
            <fieldset>
                <input type="text" onChange={ handleChange } />
            </fieldset>
            <ReactMemoPics keys={ picKeys } />
            <fieldset>
                <p ref={ elParagraph }>{ text }</p>
            </fieldset>
        </form>
    )
}

3 番目は useMemo() によって最適化した版です。

export default function Pics() {
    const [ text, setText ] = useState('');

    const elParagraph = useRef<HTMLParagraphElement | null>(null);

    useEffect(() => {
        recordFinished('Hydration');
    }, []);

    const handleChange = (e: React.ChangeEvent<HTMLInputElement, HTMLInputElement>) => {
        recordStart('ReRender');
        setText(e.currentTarget.value);
    };

    useEffect(() => {
        if (!elParagraph || !elParagraph.current) return;

        if (elParagraph.current.textContent === text) recordFinished('ReRender');
    }, [ text ]);

    const UseMemoPics = useMemo(() => {
        return picKeys.map(picKey => (
            <Pic key={ picKey } picKey={ picKey } />
        ));
    }, []);

    return (
        <form id="pics">
            <fieldset>
                <input type="text" onChange={ handleChange } />
            </fieldset>
            { UseMemoPics }
            <fieldset>
                <p ref={ elParagraph }>{ text }</p>
            </fieldset>
        </form>
    )
}

4 番目は children 挿入によって最適化した版です。

function PicsChildren({ children }: { children: ReactNode }) {
    const [ text, setText ] = useState('');

    const elParagraph = useRef<HTMLParagraphElement | null>(null);
    const startReRender = useRef(0);

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        recordStart('ReRender');
        startReRender.current = records.ReRender.start;
        setText(e.currentTarget.value);
    };

    useEffect(() => {
        if (!elParagraph || !elParagraph.current) return;

        if (elParagraph.current.textContent === text) recordFinished('ReRender');
    }, [ text ]);

    return (
        <form id="pics">
            <fieldset>
                <input type="text" onChange={ handleChange } />
            </fieldset>
            { children }
            <fieldset>
                <p ref={ elParagraph }>{ text }</p>
            </fieldset>
        </form>
    );
}

export default function Pics() {
    useEffect(() => {
        recordFinished('Hydration');
    }, []);

    return (
        <PicsChildren>
            {picKeys.map(picKey => (
                <Pic key={ picKey } picKey={ picKey } />
            ))}
        </PicsChildren>
    );
}

おまけの Qwik

現時点で Astro 側から Qwik 用のインテグレーションが公開されていないので、同一 Astro という実験場に置くことは叶いませんが、計測結果に興味があるので、個別に実験用 Qwik City で作りました。 Astro の client:visible であっても、DOM ツリー自体は生成されており、見えてから JS 実行という形なので、各コンポーネント自体は Solid 版などと同じ構成にしました。 また、冒頭のグローバルストアも、Qwik ではシリアライズがあり同じようにはできず動作外としたので、Qwik でも Context を利用します。その際に、先日の Getter / Setter 化した機構を噛ませました。

wings.hatenablog.com

Qwik は今回の計測に使用する JS が QRL 化されるのでダウンロードを伴ってしまい、フェアじゃないと感じたので、リロードせずにニ度目も発動する(QRL によるダウンロードが完了している状態)ような感じにしました。 client:visible に似た機能として useVisibleTask$() や onQVisible$ はありますが、一度画面に入ると再発動しないので使えず、onMount 代わりとして Qwik での計測実験専用の OnMount コンポーネントを作って最下部に置き、そこで計測するようにしました。Pic コンポーネントの最深部にある img や Pics コンポーネント最下部の p に対して処理の到達時間を別途計測してみたところ、p の前にある img が順番に到達し、最後に p へ到達したので、この最下部の OnMount でおそらく今回の役に立つと思いました。

onClick$ や onChange$ による操作の瞬間に計測開始をするために、その対象となる関数を先に useVisibleTask$ でダウンロードしておいて即 return し、押したときには既に関数はあるのでそれを使って即開始、残りは別で QRL 化して、QRL を伴うか伴わないかでも開始タイミングが同じになるような流れにしてみました(ダウンロード時の即 return では、その別の QRL 化した関数にたどり着かないので、操作後にダウンロードされていました)。

Context 用箱の生成

export type AtId = string;
export const atId = createContextId<Signal<AtId>>('atId');

export type StartHydration = number;
export const startHydration = createContextId<Signal<StartHydration>>('startHydration');

Page.tsx

export default component$(() => {
    const atIdSignal = useSignal<AtId>('');
    useContextProvider(atId, atIdSignal);
    const startHydrationSignal = useSignal<StartHydration>(0);
    useContextProvider(startHydration, startHydrationSignal);

    return (
        <div id="performance">
            <section>
                <Anchor to="pics">スクロールします</Anchor>
            </section>
            <At id="pics" />
            <Pics />
        </div>
    )
});

Anchor.tsx

type Props = {
    to: string;
};

export default component$((props: Props) => {
    const atIdContext = provideSignalContext(useContext(atId));

    const handleStart$ = $(() => {
        atIdContext.set(props.to);
    });
    const handleClick$ = $((isAction?: boolean) => {
        if (!isAction) return;

        recordStart('GlobalSignal');
        handleStart$();
    });
    useVisibleTask$(() => {
        handleClick$();
    });

    return (
        <button onClick$={ () => handleClick$(true) }>
            <Slot />
        </button>
    )
});

At.tsx

type Props = {
    id: string;
};

export default component$((props: Props) => {
    const atIdContext = provideSignalContext(useContext(atId));
    const startHydrationContext = provideSignalContext(useContext(startHydration));

    const el = useSignal<HTMLSpanElement>();

    useTask$(({ track }) => {
        track(() => atIdContext.signal.value);

        if (!el || !el.value || props.id !== atIdContext.signal.value) return;

        el.value.scrollIntoView({ behavior: 'smooth' });
        recordFinished('GlobalSignal');
        atIdContext.set('');
        recordStart('Hydration');
        startHydrationContext.set(records.Hydration.start);
    });

    return (
        <span ref={ el } id={ `at_${props.id}` } class="invisible_target" />
    )
});

Pic.tsx

Solid 版とほぼ同じなので全て割愛します(img に width / height があるのみ)。

OnMount.tsx

type OnMountProps = {
    start: number;
    target: Records;
};

export default component$((props: OnMountProps) => {
    useTask$(({ track }) => {
        track(() => props.start);

        recordFinished(props.target);
    });
});

Pics.tsx

export default component$(() => {
    const startHydrationContext = provideSignalContext(useContext(startHydration));

    const text = useSignal('');
    const startReRender = useSignal(0);

    const handleStart$ = $((el: HTMLInputElement) => {
        startReRender.value = records.ReRender.start;
        text.value = el.value;
    });
    const handleChange$ = $((el?: HTMLInputElement) => {
        if (!el) return;

        recordStart('ReRender');
        handleStart$(el);
    });
    useVisibleTask$(() => {
        handleChange$();
    });

    return (
        <form id="pics">
            <fieldset>
                <input type="text" onChange$={ (_, el) => handleChange$(el) } />
            </fieldset>
            {picKeys.map(picKey => (
                <Pic key={ picKey } picKey={ picKey } />
            ))}
            <fieldset>
                <p>{ text.value }</p>
            </fieldset>
            <OnMount start={ startHydrationContext.signal.value } target="Hydration" />
            <OnMount start={ startReRender.value } target="ReRender" />
        </form>
    )
});

計測結果

Hydration に関しては、今回 Astro の client:visible 計測としているので、一度のみのリロード直後ということで、ウォームアップ後は計測していません。Qwik も同様に QRL ダウンロード含む場合のみです。

Solid

1 回目

  • GlobalSignal: 0.2999999523162842 ミリ秒 / リロード直後
  • GlobalSignal: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • Hydration: 40.799999952316284 ミリ秒 / リロード直後
  • ReRender: 0.10000002384185791 ミリ秒 / リロード直後
  • ReRender: 0.09999990463256836 ミリ秒 / ウォームアップ後

2 回目

  • GlobalSignal: 0.2999999523162842 ミリ秒 / リロード直後
  • GlobalSignal: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • Hydration: 36.10000002384186 ミリ秒 / リロード直後
  • ReRender: 0.20000004768371582 ミリ秒 / リロード直後
  • ReRender: 0.20000004768371582 ミリ秒 / ウォームアップ後

3 回目

  • GlobalSignal: 0.20000004768371582 ミリ秒 / リロード直後
  • GlobalSignal: 0.19999992847442627 ミリ秒 / ウォームアップ後
  • Hydration: 35.299999952316284 ミリ秒 / リロード直後
  • ReRender: 0.20000004768371582 ミリ秒 / リロード直後
  • ReRender: 0 ミリ秒 / ウォームアップ後

平均

  • GlobalSignal: 0.2666666508 ミリ秒 / リロード直後
  • GlobalSignal: 0.2000000079 ミリ秒 / ウォームアップ後
  • Hydration: 37.3999999762 ミリ秒 / リロード直後
  • ReRender: 0.1666667064 ミリ秒 / リロード直後
  • ReRender: 0.09999998411 ミリ秒 / ウォームアップ後

Svelte

1 回目

  • GlobalSignal: 0.5 ミリ秒 / リロード直後
  • GlobalSignal: 0.3999999761581421 ミリ秒 / ウォームアップ後
  • Hydration: 40.09999990463257 ミリ秒 / リロード直後
  • ReRender: 0.2999999523162842 ミリ秒 / リロード直後
  • ReRender: 0.20000004768371582 ミリ秒 / ウォームアップ後

2 回目

  • GlobalSignal: 0.6000000238418579 ミリ秒 / リロード直後
  • GlobalSignal: 0.3999999761581421 ミリ秒 / ウォームアップ後
  • Hydration: 36.799999952316284 ミリ秒 / リロード直後
  • ReRender: 0.19999992847442627 ミリ秒 / リロード直後
  • ReRender: 0.10000002384185791 ミリ秒 / ウォームアップ後

3 回目

  • GlobalSignal: 0.6000000238418579 ミリ秒 / リロード直後
  • GlobalSignal: 0.2999999523162842 ミリ秒 / ウォームアップ後
  • Hydration: 41.60000002384186 ミリ秒 / リロード直後
  • ReRender: 0.19999992847442627 ミリ秒 / リロード直後
  • ReRender: 0.20000004768371582 ミリ秒 / ウォームアップ後

平均

  • GlobalSignal: 0.5666666826 ミリ秒 / リロード直後
  • GlobalSignal: 0.3666666349 ミリ秒 / ウォームアップ後
  • Hydration: 39.4999999603 ミリ秒 / リロード直後
  • ReRender: 0.2333332698 ミリ秒 / リロード直後
  • ReRender: 0.1666667064 ミリ秒 / ウォームアップ後

Vue

1 回目

  • GlobalSignal: 0.3999999761581421 ミリ秒 / リロード直後
  • GlobalSignal: 0.2999999523162842 ミリ秒 / ウォームアップ後
  • Hydration: 43.89999997615814 ミリ秒 / リロード直後
  • ReRender: 1.100000023841858 ミリ秒 / リロード直後
  • ReRender: 0.40000009536743164 ミリ秒 / ウォームアップ後

2 回目

  • GlobalSignal: 0.3999999761581421 ミリ秒 / リロード直後
  • GlobalSignal: 0.10000002384185791 ミリ秒 / ウォームアップ後
  • Hydration: 35.5 ミリ秒 / リロード直後
  • ReRender: 1 ミリ秒 / リロード直後
  • ReRender: 0.5 ミリ秒 / ウォームアップ後

3 回目

  • GlobalSignal: 0.5 ミリ秒 / リロード直後
  • GlobalSignal: 0.20000004768371582 ミリ秒 / ウォームアップ後
  • Hydration: 39.799999952316284 ミリ秒 / リロード直後
  • ReRender: 0.9000000953674316 ミリ秒 / リロード直後
  • ReRender: 0.5 ミリ秒 / ウォームアップ後

平均

  • GlobalSignal: 0.4333333174 ミリ秒 / リロード直後
  • GlobalSignal: 0.2000000079 ミリ秒 / ウォームアップ後
  • Hydration: 39.7333333095 ミリ秒 / リロード直後
  • ReRender: 1.00000003973643 ミリ秒 / リロード直後
  • ReRender: 0.4666666985 ミリ秒 / ウォームアップ後

React

1 回目

  • GlobalSignal: 1.1999999284744263 ミリ秒 / リロード直後
  • GlobalSignal: 0.6000000238418579 ミリ秒 / ウォームアップ後
  • Hydration: 66.89999997615814 ミリ秒 / リロード直後 / 最適化なし
  • Hydration: 59.299999952316284 ミリ秒 / リロード直後 / memo() 使用
  • Hydration: 62.200000047683716 ミリ秒 / リロード直後 / useMemo() 使用
  • Hydration: 70.79999995231628 ミリ秒 / リロード直後 / children 包囲使用
  • ReRender: 9 ミリ秒 / リロード直後 / 最適化なし
  • ReRender: 8 ミリ秒 / ウォームアップ後 / 最適化なし
  • ReRender: 1 ミリ秒 / リロード直後 / memo() 使用
  • ReRender: 0.6000000238418579 ミリ秒 / ウォームアップ後 / memo() 使用
  • ReRender: 1 ミリ秒 / リロード直後 / useMemo() 使用
  • ReRender: 0.6000000238418579 ミリ秒 / ウォームアップ後 / useMemo() 使用
  • ReRender: 1.100000023841858 ミリ秒 / リロード直後 / children 包囲使用
  • ReRender: 0.6000000238418579 ミリ秒 / ウォームアップ後 / children 包囲使用

2 回目

  • GlobalSignal: 1.6999999284744263 ミリ秒 / リロード直後
  • GlobalSignal: 0.40000009536743164 ミリ秒 / ウォームアップ後
  • Hydration: 68.79999995231628 ミリ秒 / リロード直後 / 最適化なし
  • Hydration: 62.89999997615814 ミリ秒 / リロード直後 / memo() 使用
  • Hydration: 62.89999997615814 ミリ秒 / リロード直後 / useMemo() 使用
  • Hydration: 63 ミリ秒 / リロード直後 / children 包囲使用
  • ReRender: 7.899999976158142 ミリ秒 / リロード直後 / 最適化なし
  • ReRender: 8.400000095367432 ミリ秒 / ウォームアップ後 / 最適化なし
  • ReRender: 0.8999999761581421 ミリ秒 / リロード直後 / memo() 使用
  • ReRender: 0.8000000715255737 ミリ秒 / ウォームアップ後 / memo() 使用
  • ReRender: 1 ミリ秒 / リロード直後 / useMemo() 使用
  • ReRender: 0.5 ミリ秒 / ウォームアップ後 / useMemo() 使用
  • ReRender: 0.8999999761581421 ミリ秒 / リロード直後 / children 包囲使用
  • ReRender: 0.5 ミリ秒 / ウォームアップ後 / children 包囲使用

3 回目

  • GlobalSignal: 1.399999976158142 ミリ秒 / リロード直後
  • GlobalSignal: 0.5 ミリ秒 / ウォームアップ後
  • Hydration: 67.29999995231628 ミリ秒 / リロード直後 / 最適化なし
  • Hydration: 58.60000002384186 ミリ秒 / リロード直後 / memo() 使用
  • Hydration: 58.89999997615814 ミリ秒 / リロード直後 / useMemo() 使用
  • Hydration: 61.89999997615814 ミリ秒 / リロード直後 / children 包囲使用
  • ReRender: 8.600000023841858 ミリ秒 / リロード直後 / 最適化なし
  • ReRender: 8 ミリ秒 / ウォームアップ後 / 最適化なし
  • ReRender: 1 ミリ秒 / リロード直後 / memo() 使用
  • ReRender: 0.7000000476837158 ミリ秒 / ウォームアップ後 / memo() 使用
  • ReRender: 1.100000023841858 ミリ秒 / リロード直後 / useMemo() 使用
  • ReRender: 0.6999999284744263 ミリ秒 / ウォームアップ後 / useMemo() 使用
  • ReRender: 1.100000023841858 ミリ秒 / リロード直後 / children 包囲使用
  • ReRender: 0.3999999761581421 ミリ秒 / ウォームアップ後 / children 包囲使用

平均

  • GlobalSignal: 1.4333332777 ミリ秒 / リロード直後
  • GlobalSignal: 0.5000000397 ミリ秒 / ウォームアップ後
  • Hydration: 67.6666666269 ミリ秒 / リロード直後 / 最適化なし
  • Hydration: 60.2666666508 ミリ秒 / リロード直後 / memo() 使用
  • Hydration: 61.3333333333 ミリ秒 / リロード直後 / useMemo() 使用
  • Hydration: 65.2333333095 ミリ秒 / リロード直後 / children 包囲使用
  • ReRender: 8.5 ミリ秒 / リロード直後 / 最適化なし
  • ReRender: 8.1333333651 ミリ秒 / ウォームアップ後 / 最適化なし
  • ReRender: 0.9666666587 ミリ秒 / リロード直後 / memo() 使用
  • ReRender: 0.7000000477 ミリ秒 / ウォームアップ後 / memo() 使用
  • ReRender: 1.03333334128 ミリ秒 / リロード直後 / useMemo() 使用
  • ReRender: 0.5999999841 ミリ秒 / ウォームアップ後 / useMemo() 使用
  • ReRender: 1.03333334128 ミリ秒 / リロード直後 / children 包囲使用
  • ReRender: 0.5 ミリ秒 / ウォームアップ後 / children 包囲使用

Qwik

1 回目

  • GlobalSignal: 5.899999976158142 ミリ秒 / QRL ダウンロード含む
  • GlobalSignal: 1 ミリ秒 / QRL ダウンロード後
  • Hydration: 2.0999999046325684 ミリ秒 / QRL ダウンロード含む
  • ReRender: 2.299999952316284 ミリ秒 / QRL ダウンロード含む
  • ReRender: 0.30000007152557373 ミリ秒 / QRL ダウンロード後

2 回目

  • GlobalSignal: 5.299999952316284 ミリ秒 / QRL ダウンロード含む
  • GlobalSignal: 0.7000000476837158 ミリ秒 / QRL ダウンロード後
  • Hydration: 2 ミリ秒 / QRL ダウンロード含む
  • ReRender: 2.1999999284744263 ミリ秒 / QRL ダウンロード含む
  • ReRender: 0.20000004768371582 ミリ秒 / QRL ダウンロード後

3 回目

  • GlobalSignal: 4.600000023841858 ミリ秒 / QRL ダウンロード含む
  • GlobalSignal: 0.9000000953674316 ミリ秒 / QRL ダウンロード後
  • Hydration: 2.100000023841858 ミリ秒 / QRL ダウンロード含む
  • ReRender: 2.399999976158142 ミリ秒 / QRL ダウンロード含む
  • ReRender: 0.30000007152557373 ミリ秒 / QRL ダウンロード後

平均

  • GlobalSignal: 5.2666666508 ミリ秒 / QRL ダウンロード含む
  • GlobalSignal: 0.8666667144 ミリ秒 / QRL ダウンロード後
  • Hydration: 2.06666664282 ミリ秒 / QRL ダウンロード含む
  • ReRender: 2.2999999523 ミリ秒 / QRL ダウンロード含む
  • ReRender: 0.2666667302 ミリ秒 / QRL ダウンロード後

平均値ランキング

GlobalSignal

React / Qwik は Context です。

  1. Solid ウォームアップ後 / Vue ウォームアップ後: 0.2000000079 ミリ秒
  2. Solid リロード直後: 0.2666666508 ミリ秒
  3. Svelte ウォームアップ後: 0.3666666349 ミリ秒
  4. Vue リロード直後 / Svelte リロード直後: 0.4333333174 ミリ秒
  5. React ウォームアップ後: 0.5000000397 ミリ秒
  6. Qwik QRL ダウンロード後: 0.8666667144 ミリ秒
  7. React リロード直後: 1.4333332777 ミリ秒
  8. Qwik QRL ダウンロード含む: 5.2666666508 ミリ秒

Hydration

  1. Qwik: 2.06666664282 ミリ秒
  2. Solid: 37.3999999762 ミリ秒
  3. Svelte: 39.4999999603 ミリ秒
  4. Vue: 39.7333333095 ミリ秒
  5. React memo() 使用: 60.2666666508 ミリ秒
  6. React useMemo() 使用: 61.3333333333 ミリ秒
  7. React children 包囲使用: 65.2333333095 ミリ秒
  8. React 最適化なし: 67.6666666269 ミリ秒

ReRender

  1. Solid ウォームアップ後: 0.09999998411 ミリ秒
  2. Solid リロード直後 / Svelte ウォームアップ後: 0.1666667064 ミリ秒
  3. Svelte リロード直後: 0.2333332698 ミリ秒
  4. Qwik QRL ダウンロード後: 0.2666667302 ミリ秒
  5. Vue ウォームアップ後: 0.4666666985 ミリ秒
  6. React ウォームアップ後 children 包囲使用: 0.5 ミリ秒
  7. React ウォームアップ後 useMemo() 使用: 0.5999999841 ミリ秒
  8. React ウォームアップ後 memo() 使用: 0.7000000477 ミリ秒
  9. React リロード直後 memo() 使用: 0.9666666587 ミリ秒
  10. Vue リロード直後: 1.00000003973643 ミリ秒
  11. React リロード直後 useMemo() 使用 / React リロード直後 children 包囲使用: 1.03333334128 ミリ秒
  12. Qwik QRL ダウンロード含む: 2.2999999523 ミリ秒
  13. React ウォームアップ後 最適化なし: 8.1333333651 ミリ秒
  14. React リロード直後 最適化なし: 8.5 ミリ秒

いずれも 3 回ずつの結果で、有意な数値とは言えませんが、こうなりました。 Qwik は本当にハイドレーションをしていないように見えました...。面白い実験でした。

SolidStart で Google Analytics などの支給コードをそのまま埋め込みたい

表題の通りです。

Astro なら <Fragment set:html={ ... } /> でそのまま埋め込めたんですが、SolidStart ではそういう簡単に埋め込める機能がなく、こうしてみました。

こういう支給されるコードは tsx 内に貼るとエラー祭になったりコメントをそのまま出力できなかったりするので、外部 html として保存して、コンポーネントを通して出力する、という流れにしました。

埋め込みたいコード

先方から支給されるコードなどです。

<!-- Google Tag Manager -->
<script>(function (w, d, s, l, i) {
    w[l] = w[l] || []; w[l].push({
        'gtm.start':
        new Date().getTime(), event: 'gtm.js'
    }); var f = d.getElementsByTagName(s)[0],
        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =
        'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
    })(window, document, 'script', 'dataLayer', 'GTM-XXXXXX');</script>
<!-- End Google Tag Manager -->

生成用のコンポーネント

ここでは Snippet.tsx とします。ssr() の型定義が string | string[] となっているので、そのまま渡せます。

import { ssr } from "solid-js/web";
import type { JSX } from "solid-js";

type Props = {
    rawHtml: string;
};

export default function Snippet(props: Props) {
    return ssr(props.rawHtml) as unknown as JSX.Element;
};

src/entry-server.tsx

head でも body でもそのまま出力されます。 html は ?raw を使って生のまま読み込みます。

// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server";
import { Snippet } from "@/components/routes/_parts";
import SnippetHead from "@/_global/snippets/head.html?raw";

export default createHandler(() => (
    <StartServer
        document={({ assets, children, scripts }) => (
            <html lang="ja">
            <head>
                <meta charset="utf-8" />
                ...
                <Snippet rawHtml={ SnippetHead } />
                {assets}
            </head>
            <body>
                <div id="app">{ children }</div>
                {scripts}
            </body>
            </html>
        )}
    />
));

ssr() を使うので、ノーガードで出力します。そのため、こういった場面でなら使えると思いました。

以上はぼくの環境や要件で動いただけであることをご理解いただければ幸甚です。

Astro 上の Solid / Svelte / Vue にグローバルストアを持たせたい

年間 10 本以上になる、同じクライアントに対する LP を、node_modules の肥大化が嫌で、同一 Astro 上でモノレポ運用していることは以前にお伝えした通りです。

wings.hatenablog.com

これまで、状態管理がある LP は Svelte を同居させて、ストアをそのまま export するという、SSG だからこそのお手軽さで実装してきました。 今回、Astro の相棒を Solid に変えようと思って進めていたときに、このストアの扱いをどうするかと悩み始めました。

以下は、Astro + Solid という、ぼくの環境においてグローバルストアを持たせたいと思って捻り出した内容です。 その過程で、Svelte と Vue でも使えるようになりました。

ただ、React は全レンダリングがあって達成できず、Qwik はその基板上においてテンプレート内でしかストアの生成を制限されていてこちらも達成できないので、React / Qwik は動作しません。 (React 用として useSyncExternalStore() を使えば動きはしましたが、Solid / Svelte / Vue は同じコードで動作するので、React 用を作らないことにしました)

寝床にて

せっかく Astro の相棒を変えるんだから、この際 SSR でも状態汚染の無いようなストアは無いかと思っていました。 Astro 側が推奨している Nano Stores は、真っ先に見に行きました。加えて、SSG 特化型だと思い込んでいる Astro の SSR 運用についてもググったりしました。

そして、Pinia のようにそのまま使ってもやってくれる SSR 状態汚染防御を、Nano Stores はしていないと知りました。 であれば Context になるけど、そうするとコンポーネントで包む必要があり、ページの大部分を包まなければならないことになり、Astro の良さを活かせないと思いました。

さて...どうしたもんか...と寝床で思案していたときに、「client:only のコンポーネントで定義して、それを client:load などで使うコンポーネントから使うときにサーバ用としてフェイクを返せばいいんじゃないか?」って思ったんです。

Astro のレンダリング順の揺れ

翌日に、そのやり方でやってみました。 サーバに対しては、ストアの Getter で初期値を返し、Setter で空の関数を返す感じでコンポーネントに定義し、レイアウトコンポーネントで client:only にして読み込みました。 うまく動作したので、いけるかなって思ったんです。

この LP 群を管理している Astro では、ある LP はストアを使わないけど、ある LP は何種類もストアを使う、という棲み分けもあるので、Swiper の use の感覚で、この作ったコンポーネントを読み込む際に useStores として使うストアを限定する props を渡してその useStores を基準に処理すれば、使わないストアは定義だけあってページ内に実体は存在しないっていうことができるなって思ってやってみました。

ただですね...ここで Astro のレンダリング順の揺れに気づきました。 レイアウトコンポーネントで読み込んだからと言って、最初にこのストアの初期化が行われるわけではなく、レイアウトを呼んだ側のページ内にある小粒なコンポーネントから呼んだストアが先に処理されてしまう(=初期化されてないからストアが無い)ことを 50% 程度の確率で確認しました。

さて...どうしたもんか...と思案を始めました。

Qwik の Context を想起

ふと、Qwik の Context は useContext(isLogged) のように「使いたいストアをご指名」して使っているって思い出しました。 それまでは Pinia 的に「同じ関数内で定義したストアは一斉に使える」ことが頭にありました。なので、この Qwik の Context の形式に思考をスイッチしようと思いました。

そうすると、「じゃあご指名されたストアは、初めて指名されたらその場で実体を作って渡し、それ以降は既に作った実体を使えばいいんじゃないか」って案になり、実装をするとうまくいきました。 これで、その LP 内で利用しないストアは実体を作る機会が無いので、やはり定義だけはあるけど実体は無いということにできました。

そして、「サーバにはフェイクを返しているし、レンダリング順もあるから、もうコンポーネントである必要は無いな」って思って、コンポーネント型を捨てて、単なる TypeScript ファイルのみに変更しました。 これで、サーバ用のフェイク・クライアント用の実体をストアに定義すれば、初期化をする必要は無く、呼んだ瞬間に実体が生成されることになりました。

ストアを豆ファイルに分離

ひとつのファイルにストアをずらずらと書いているのは、ぼくの精神衛生上で合わないので、ストアを個別に分離することにしました。 そして、その豆ファイルのストアをおまとめするファイルを作りました。これで、使うストアをおまとめファイルに設定する一手が増えましたが、まぁ仕方ないかなと思いました。

ただ、このおまとめファイルに定義されたストアのキーを型として拾っておくことで、設定されているストアは TypeScript 的に補完できるようになりました。

確認実験

ストアをただの TypeScript ファイルに書き、おまとめファイルや実際に使う関数があるファイルもただの TypeScript なので、Svelte の $state() は .svelte.ts で定義すれば、Vue の ref() はそのまま .ts で定義すれば使えていました。

同一 Astro 上に Solid / Svelte / Vue / React を乗せた実験場で、これらの「異なるフレームワークで定義したストアをどう同期できるか」を見てみましたが、やはり Solid で定義したストアは Solid でしか検知できないという結果だったので、もしフレームワークが混在している環境であれば、生成された実体を使って各 Setter を一斉に叩く関数を用意すればいいか、と思いました。

また、Astro 上でなくても、SolidStart / SvelteKit / Nuxt でも動いたので、自分用のツールである各フレームワークに導入してみて、動作してくれています。

Solid でストアを定義した場合

_models/isLogged.ts と仮にします。

import { createSignal } from "solid-js";

type Store = boolean;
const initialValue: Store = false;

export const _isLogged = {
    server: {
        isLogged: (): Store => initialValue,
        setLogged: (_: Store) => {},
    },
    client: () => {
        const [ isLogged, setLogged ] = createSignal<Store>(initialValue);

        return { isLogged, setLogged };
    },
};

Svelte でストアを定義した場合

_models/isLogged.svelte.ts と仮にします。

type Store = boolean;
const initialValue: Store = false;

export const _isLogged = {
    server: {
        isLogged: (): Store => initialValue,
        setLogged: (_: Store) => {},
    },
    client: () => {
        let isLogged = $state<Store>(initialValue);
        const setLogged = (value: Store) => { isLogged = value; };

        return { isLogged: () => isLogged, setLogged };
    },
};

Vue でストアを定義した場合

_models/isLogged.ts と仮にします。

import { ref } from "vue";

type Store = boolean;
const initialValue: Store = false;

export const _isLogged = {
    server: {
        isLogged: (): Store => initialValue,
        setLogged: (_: Store) => {},
    },
    client: () => {
        const isLogged = ref<Store>(initialValue);
        const setLogged = (value: Store) => { isLogged.value = value; };

        return { isLogged: () => isLogged.value, setLogged };
    },
};

おまとめファイル

_models/index.ts と仮にします。ストアは Solid で定義したとします。

import { _isLogged } from "./isLogged";

export const allStores = {
    isLogged: _isLogged,
} as const;
export type AllStores = typeof allStores;
export type AllStoreKeys = keyof AllStores;

ストア供給の本丸

import { allStores } from "./_models";
import type { AllStoreKeys, AllStores } from "./_models";

const createServerStore = (key: AllStoreKeys): AllStores[typeof key]['server'] => allStores[key].server;

type CreateClientStore = ReturnType<AllStores[AllStoreKeys]['client']>;
const createClientStore = (key: AllStoreKeys): CreateClientStore => allStores[key].client();

type ClientStoreInstance = Record<AllStoreKeys, CreateClientStore | null>;
const clientStoreInstance = Object.fromEntries(Object.keys(allStores).map(key => ([ key, null ]))) as ClientStoreInstance;

export const useStore = <K extends AllStoreKeys>(key: K) => {
    if (import.meta.env.SSR) return createServerStore(key) as AllStores[K]['server'];

    if (!clientStoreInstance[key]) {
        clientStoreInstance[key] = createClientStore(key);
    }

    return clientStoreInstance[key] as ReturnType<AllStores[K]['client']>;
};

使う側のコンポーネント

useStore('') で isLogged が補完として出てきます。

const { isLogged, setLogged } = useStore('isLogged');

Solid の Signal への深い愛情として、今回で言う isLogged は Svelte や Vue であっても isLogged() とする必要があり、Svelte の bind なども使うことはできません。ただ、「グローバルストアを bind や v-model でそのまま変更できるのはどうなんだろう?」と思っているので、単一方向の流れである、これで良いかなと思っています。

また、サーバ用のフェイクも一緒に定義するのはめんどくさいんですが、「グローバルストアを増やしすぎない」ためにも、これはこれで良いかなと思っています。

なお、これは状態管理ライブラリではなく、Solid の Signal・Svelte の Runes・Vue の Ref をそのまま使う、要は「Signal をまとめてポイ」するだけです。 SSR の状態汚染なく、利用しないストアは null のまま、使うときに型推論が効けばいいことを自分の環境でやってみただけで、Context の代替とは全く考えてませんし、大規模や大人数であればフレームワーク公式や推奨の仕組みを使う方が良いと思っています。

なので、ぼくが自分のために作ったものであり、Astro / SolidStart / SvelteKit / Nuxt のぼくの環境で動作しただけ、ということをご理解いただければ幸甚です。