$derived - Svelte 5の計算プロパティをTypeScriptで完全マスター

$derivedルーンは、他のリアクティブな値から自動的に計算される値を作成します。依存する値が変更されると、派生値も自動的に再計算されます。

この記事で学べること

  • $derivedの基本的な使い方と TypeScript 型推論
  • $derived.by()で複雑な計算ロジックを実装する
  • 配列のフィルタリング・ソート・グルーピングのパターン
  • 自動メモ化によるパフォーマンス最適化
  • $derivedのオーバーライド機能(Svelte 5.25+)
  • React useMemo / Vue computed との違い
React 開発者の方へ

$derivedは React のuseMemoと似ていますが、依存配列を指定する必要がありません。Svelte が自動的に依存関係を追跡するため、依存配列の管理ミスによるバグから解放されます。

基本的な使い方

$derivedの最も基本的な使い方は、既存のリアクティブな値から新しい値を計算することです。 依存関係は自動的に追跡され、必要な時だけ再計算されます。

シンプルな計算

2倍: 0

2乗: 0

偶数: true

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

  // countが変更されると自動的に再計算
  let doubled = $derived(count * 2);
  let squared = $derived(count ** 2);
  let isEven = $derived(count % 2 === 0);

  function increment() {
    count++;
  }
</script>

<button onclick={increment}>カウント: {count}</button>
<p>2倍: {doubled}</p>
<p>2乗: {squared}</p>
<p>偶数: {isEven}</p>
svelte
Click fold/expand code
Vue や React との比較
  • Vue の computed と同じ概念
  • React の useMemo に似ているが、依存関係の指定が不要
  • 自動的に依存関係を追跡し、必要な時だけ再計算

複数の依存関係

$derivedは複数のリアクティブな値に依存する計算もサポートします。 どの依存値が変更されても、派生値は自動的に更新されます。

フルネーム: 山田 太郎

表示名: 山田 太郎様

文字数: 5

<script lang="ts">
  let firstName = $state('太郎');
  let lastName = $state('山田');
  let separator = $state(' ');

  // 複数の値に依存する派生値
  let fullName = $derived(
    lastName + separator + firstName
  );

  // さらに派生値から派生
  let displayName = $derived(
    `${fullName}`
  );

  let nameLength = $derived(fullName.length);
</script>

<input bind:value={firstName} placeholder="" />
<input bind:value={lastName} placeholder="" />
<select bind:value={separator}>
  <option value=" ">スペース</option>
  <option value="">中点</option>
  <option value="">なし</option>
</select>

<p>フルネーム: {fullName}</p>
<p>表示名: {displayName}</p>
<p>文字数: {nameLength}</p>
svelte
Click fold/expand code

$derived.by - 複雑な計算ロジック

単純な式では表現しにくい複雑な計算や、複数のステップが必要な処理には、 $derived.by()を使用します。

$derived vs $derived.by の使い分け
  • $derived(式) - 単純な 1 行の式(count * 2items.lengthなど)
  • $derived.by(() => { ... }) - 複数ステートメントや複雑なロジック

商品数: 10点

小計: ¥239,000

割引: -¥11,950

割引後: ¥227,050

税額: ¥22,705

合計: ¥249,755

<script lang="ts">
  interface Product {
    id: number;
    name: string;
    price: number;
    quantity: number;
  }

  let products = $state<Product[]>([
    { id: 1, name: 'ノートPC', price: 100000, quantity: 2 },
    { id: 2, name: 'マウス', price: 3000, quantity: 5 },
    { id: 3, name: 'キーボード', price: 8000, quantity: 3 }
  ]);

  let taxRate = $state(0.1);
  let discountRate = $state(0.05);

  // $derived.by で複雑な計算
  let summary = $derived.by(() => {
    const subtotal = products.reduce((sum, product) => {
      return sum + product.price * product.quantity;
    }, 0);

    const discount = subtotal * discountRate;
    const afterDiscount = subtotal - discount;
    const tax = afterDiscount * taxRate;
    const total = afterDiscount + tax;

    return {
      subtotal,
      discount,
      afterDiscount,
      tax,
      total,
      itemCount: products.reduce((sum, p) => sum + p.quantity, 0)
    };
  });
</script>

<div class="summary">
  <p>商品数: {summary.itemCount}</p>
  <p>小計: ¥{summary.subtotal?.toLocaleString() ?? 0}</p>
  <p>割引: -¥{summary.discount?.toLocaleString() ?? 0}</p>
  <p>割引後: ¥{summary.afterDiscount?.toLocaleString() ?? 0}</p>
  <p>税額: ¥{summary.tax?.toLocaleString() ?? 0}</p>
  <p>合計: ¥{summary.total?.toLocaleString() ?? 0}</p>
</div>
svelte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Click fold/expand code

配列とオブジェクトの処理

実際のアプリケーションでは、配列やオブジェクトのデータを加工することが多くあります。 $derivedを使えば、フィルタリング、ソート、グルーピングなどの処理を効率的に実装できます。

フィルタリングとソート

<script lang="ts">
  interface Task {
    id: number;
    title: string;
    completed: boolean;
    priority: 'low' | 'medium' | 'high';
    dueDate: Date;
  }

  let tasks = $state<Task[]>([
    // タスクデータ
  ]);

  let showCompleted = $state(false);
  let sortBy = $state<'priority' | 'dueDate'>('priority');
  let searchQuery = $state('');

  // フィルタリングされたタスク(複雑なロジックには$derived.byを使用)
  let filteredTasks = $derived.by(() => {
    let result = tasks;

    // 完了タスクのフィルタ
    if (!showCompleted) {
      result = result.filter(t => !t.completed);
    }

    // 検索フィルタ
    if (searchQuery) {
      const query = searchQuery.toLowerCase();
      result = result.filter(t =>
        t.title.toLowerCase().includes(query)
      );
    }

    // ソート
    result = [...result].sort((a, b) => {
      if (sortBy === 'priority') {
        const priorityOrder = { high: 0, medium: 1, low: 2 };
        return priorityOrder[a.priority] - priorityOrder[b.priority];
      } else {
        return a.dueDate.getTime() - b.dueDate.getTime();
      }
    });

    return result;
  });

  // 統計情報
  let stats = $derived.by(() => {
    const total = tasks.length;
    const completed = tasks.filter(t => t.completed).length;
    const pending = total - completed;
    const highPriority = tasks.filter(
      t => t.priority === 'high' && !t.completed
    ).length;

    return { total, completed, pending, highPriority };
  });
</script>
svelte

グルーピング

データを特定のキーでグループ化することも、$derivedで簡単に実現できます。 Map や Object を使って、カテゴリごとの集計や整理が可能です。

<script lang="ts">
  interface Item {
    category: string;
    name: string;
    value: number;
  }

  let items = $state<Item[]>([
    { category: '食品', name: 'りんご', value: 100 },
    { category: '食品', name: 'バナナ', value: 80 },
    { category: '家電', name: 'テレビ', value: 50000 },
    { category: '家電', name: '冷蔵庫', value: 80000 },
    { category: '衣類', name: 'シャツ', value: 3000 }
  ]);

  // カテゴリごとにグループ化(複雑なロジックには$derived.byを使用)
  let groupedItems = $derived.by(() => {
    const groups = new Map<string, Item[]>();

    for (const item of items) {
      if (!groups.has(item.category)) {
        groups.set(item.category, []);
      }
      groups.get(item.category)!.push(item);
    }

    return groups;
  });

  // カテゴリごとの合計
  let categoryTotals = $derived.by(() => {
    const totals = new Map<string, number>();

    for (const [category, categoryItems] of groupedItems) {
      const total = categoryItems.reduce(
        (sum, item) => sum + item.value, 0
      );
      totals.set(category, total);
    }

    return totals;
  });
</script>
svelte

$derived vs $derived.by の使い分け

$derived$derived.byは異なる用途に最適化されています。

<script lang="ts">
  let count = $state(0);
  let items = $state<string[]>(['apple', 'banana', 'cherry']);
  let searchTerm = $state('');

  // ✅ $derived - 単純な式に使用
  let doubled = $derived(count * 2);
  let itemCount = $derived(items.length);
  let hasItems = $derived(items.length > 0);

  // ✅ $derived.by - 複雑なロジックに使用
  let searchResults = $derived.by(() => {
    if (!searchTerm) return items;

    const term = searchTerm.toLowerCase();
    return items.filter(item =>
      item.toLowerCase().includes(term)
    );
  });
</script>
svelte
よくある間違い
// ❌ 間違い:複雑なロジックに $derived を使用
let filtered = $derived(() => {
	// 複数行のロジック...
});

// ✅ 正しい:$derived.by を使用
let filtered = $derived.by(() => {
	// 複数行のロジック...
});
typescript

$derived のオーバーライド(Svelte 5.25+)

Svelte 5.25 以降では、$derivedで作成した値を一時的にオーバーライドできるようになりました。 これは、ユーザー入力で派生値を一時的に上書きしたい場合に便利です。

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

  // 通常は count * 2 を返す
  let doubled = $derived(count * 2);

  function overrideValue() {
    // 一時的にオーバーライド(Svelte 5.25+)
    doubled = 100;
  }

  function resetToCalculated() {
    // count を変更すると、派生値が再計算される
    count = count;
  }
</script>

<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onclick={() => count++}>Increment</button>
<button onclick={overrideValue}>Override to 100</button>
svelte
オーバーライドの動作
  • オーバーライドされた値は、依存する状態が変更されるまで維持されます
  • 依存状態が変更されると、派生値は再計算されオーバーライドは解除されます
  • フォーム入力の一時的な上書きなどに便利です

非同期処理との組み合わせ

$derived自体は同期的な計算のみをサポートしますが、非同期処理の結果を扱うパターンは非常に重要です。 ここでは、検索需要の多い「非同期データの派生」について詳しく解説します。

非同期派生値の注意

$derivedは同期的に値を返す必要があります。非同期処理には$effectを組み合わせて使用します。

やってはいけないパターン

// ❌ これは動作しません - dataはPromiseになってしまう
let data = $derived(async () => {
  const res = await fetch('/api/data');
  return res.json();
});

// ❌ $derived.byでも同様
let data = $derived.by(async () => {
  const res = await fetch('/api/data');
  return res.json();
});
typescript

パターン1: $effect + $state の組み合わせ(基本)

最も基本的なパターンです。URLを同期的に派生させ、データ取得は$effectで行います。

<script lang="ts">
  interface User {
    id: number;
    name: string;
    email: string;
  }

  let userId = $state(1);
  let userData = $state<User | null>(null);
  let loading = $state(false);
  let error = $state<string | null>(null);

  // URLは同期的に派生(これはOK)
  let apiUrl = $derived(`/api/users/${userId}`);

  // 非同期処理は$effectで実行
  $effect(() => {
    loading = true;
    error = null;

    const controller = new AbortController();

    fetch(apiUrl, { signal: controller.signal })
      .then(response => {
        if (!response.ok) throw new Error('Failed to fetch');
        return response.json();
      })
      .then(data => {
        userData = data;
        loading = false;
      })
      .catch(e => {
        if (e.name !== 'AbortError') {
          error = e.message;
          loading = false;
        }
      });

    // クリーンアップ: コンポーネント破棄時やuserIdが変わった時にリクエストをキャンセル
    return () => controller.abort();
  });

  // 取得したデータから同期的に派生値を計算
  let userDisplayName = $derived(
    userData ? `${userData.name} (${userData.email})` : ''
  );
</script>

<select bind:value={userId}>
  <option value={1}>ユーザー 1</option>
  <option value={2}>ユーザー 2</option>
  <option value={3}>ユーザー 3</option>
</select>

{#if loading}
  <p>読み込み中...</p>
{:else if error}
  <p class="error">エラー: {error}</p>
{:else if userData}
  <p>ユーザー: {userDisplayName}</p>
{/if}
svelte

パターン2: デバウンス付き検索

入力値の変更を検知して、デバウンス後にAPIを呼び出すパターンです。

<script lang="ts">
  interface SearchResult {
    id: string;
    title: string;
    score: number;
  }

  let searchInput = $state('');
  let debouncedQuery = $state('');
  let results = $state<SearchResult[]>([]);
  let loading = $state(false);

  // デバウンス処理(300ms待機)
  $effect(() => {
    const timer = setTimeout(() => {
      debouncedQuery = searchInput;
    }, 300);

    return () => clearTimeout(timer);
  });

  // デバウンス後のクエリでAPI呼び出し
  $effect(() => {
    if (!debouncedQuery.trim()) {
      results = [];
      return;
    }

    loading = true;
    const controller = new AbortController();

    fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}`, {
      signal: controller.signal
    })
      .then(res => res.json())
      .then(data => {
        results = data;
        loading = false;
      })
      .catch(e => {
        if (e.name !== 'AbortError') {
          loading = false;
        }
      });

    return () => controller.abort();
  });

  // 結果から派生値を計算(これは同期的)
  let resultCount = $derived(results.length);
  let hasResults = $derived(results.length > 0);
  let topResults = $derived(results.filter(r => r.score > 0.8));
</script>

<input
  type="search"
  bind:value={searchInput}
  placeholder="検索..."
/>

{#if loading}
  <p>検索中...</p>
{:else if hasResults}
  <p>{resultCount}件の結果(上位: {topResults.length}件)</p>
  <ul>
    {#each results as result}
      <li>{result.title} (スコア: {result.score})</li>
    {/each}
  </ul>
{:else if searchInput}
  <p>結果なし</p>
{/if}
svelte

パターン3: SvelteKitのload関数を活用(推奨)

最も推奨されるパターンです。サーバーサイドでデータを取得し、クライアントでは同期的に派生値を計算します。

// src/routes/users/[id]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';

export const load: PageServerLoad = async ({ params, fetch }) => {
  const response = await fetch(`/api/users/${params.id}`);

  if (!response.ok) {
    throw error(404, 'ユーザーが見つかりません');
  }

  const user = await response.json();
  const posts = await fetch(`/api/users/${params.id}/posts`).then(r => r.json());

  return { user, posts };
};
typescript
<!-- src/routes/users/[id]/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data }: { data: PageData } = $props();

  // サーバーから取得したデータを同期的に派生
  // 非同期処理は不要!
  let fullName = $derived(
    `${data.user.firstName} ${data.user.lastName}`
  );

  let recentPosts = $derived(
    data.posts.filter(post => {
      const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
      return new Date(post.createdAt).getTime() > oneWeekAgo;
    })
  );

  let postCount = $derived(data.posts.length);
  let hasRecentActivity = $derived(recentPosts.length > 0);
</script>

<h1>{fullName}</h1>
<p>投稿数: {postCount}</p>

{#if hasRecentActivity}
  <h2>最近の投稿 ({recentPosts.length}件)</h2>
  <ul>
    {#each recentPosts as post}
      <li>{post.title}</li>
    {/each}
  </ul>
{:else}
  <p>最近の投稿はありません</p>
{/if}
svelte

パターン比較表

パターン用途メリットデメリット
$effect + $stateクライアントサイド検索リアルタイム更新、柔軟初期表示が空、ローディング管理必要
SvelteKit loadページ単位のデータSEO対応、SSR、シンプルURLパラメータ必要
デバウンス付き入力連動検索リクエスト削減遅延が発生

非同期処理のベストプラクティス

// ✅ 推奨: 非同期データ取得と同期的な派生を分離
let rawData = $state<Data | null>(null);

$effect(() => {
  // 非同期処理
  fetchData().then(data => { rawData = data; });
});

// 同期的に派生
let processedData = $derived.by(() => {
  if (!rawData) return [];
  return rawData.items.filter(item => item.active);
});

// ❌ 避ける: 派生値の中で非同期処理
let data = $derived(await fetchData()); // コンパイルエラー
typescript

実践例:シンプルなフィルタリング

実際のアプリケーションでよく使われる、商品リストのフィルタリング機能を実装してみましょう。 複数の条件を組み合わせた動的なフィルタリングと、リアルタイムの統計情報表示を実現します。

フィルタ条件

該当商品: 6件 平均価格: ¥62,500 平均評価: ★4.2 在庫あり: 5件

ノートPC Pro

パソコン

¥150,000

★ 4.5

在庫あり

ワイヤレスマウス

周辺機器

¥3,000

★ 4

在庫あり

機械式キーボード

周辺機器

¥12,000

★ 4.8

在庫なし

ウェブカメラ HD

周辺機器

¥8,000

★ 3.5

在庫あり

デスクトップPC

パソコン

¥200,000

★ 4.7

在庫あり

USBハブ

周辺機器

¥2,000

★ 3.8

在庫あり

<script lang="ts">
  interface Product {
    id: number;
    name: string;
    category: string;
    price: number;
    inStock: boolean;
    rating: number;
  }

  let products = $state<Product[]>([
    { id: 1, name: 'ノートPC Pro', category: 'パソコン', price: 150000, inStock: true, rating: 4.5 },
    { id: 2, name: 'ワイヤレスマウス', category: '周辺機器', price: 3000, inStock: true, rating: 4.0 },
    { id: 3, name: '機械式キーボード', category: '周辺機器', price: 12000, inStock: false, rating: 4.8 },
    { id: 4, name: 'ウェブカメラ HD', category: '周辺機器', price: 8000, inStock: true, rating: 3.5 },
    { id: 5, name: 'デスクトップPC', category: 'パソコン', price: 200000, inStock: true, rating: 4.7 },
    { id: 6, name: 'USBハブ', category: '周辺機器', price: 2000, inStock: true, rating: 3.8 }
  ]);

  // フィルタ条件
  let searchQuery = $state('');
  let selectedCategory = $state('all');
  let minPrice = $state(0);
  let maxPrice = $state(300000);
  let onlyInStock = $state(false);
  let minRating = $state(0);

  // カテゴリ一覧を動的に生成
  let categories = $derived.by(() => {
    const cats = new Set(products.map(p => p.category));
    return ['all', ...Array.from(cats)];
  });

  // フィルタリングされた商品
  let filteredProducts = $derived.by(() => {
    return products.filter(product => {
      // 検索クエリ
      if (searchQuery && !product.name.toLowerCase().includes(searchQuery.toLowerCase())) {
        return false;
      }

      // カテゴリ
      if (selectedCategory !== 'all' && product.category !== selectedCategory) {
        return false;
      }

      // 価格範囲
      if (product.price < minPrice || product.price > maxPrice) {
        return false;
      }

      // 在庫
      if (onlyInStock && !product.inStock) {
        return false;
      }

      // 評価
      if (product.rating < minRating) {
        return false;
      }

      return true;
    });
  });

  // 統計情報
  let stats = $derived.by(() => {
    const total = filteredProducts.length;
    const avgPrice = total > 0
      ? filteredProducts.reduce((sum, p) => sum + p.price, 0) / total
      : 0;
    const avgRating = total > 0
      ? filteredProducts.reduce((sum, p) => sum + p.rating, 0) / total
      : 0;
    const inStockCount = filteredProducts.filter(p => p.inStock).length;

    return {
      total,
      avgPrice: Math.round(avgPrice),
      avgRating: avgRating.toFixed(1),
      inStockCount
    };
  });
</script>

<div class="search-filter-demo">
  <div class="filters">
    <h3>フィルタ条件</h3>

    <div class="filter-group">
      <label for="search">検索:</label>
      <input
        id="search"
        type="text"
        bind:value={searchQuery}
        placeholder="商品名で検索..."
      />
    </div>

    <div class="filter-group">
      <label for="category">カテゴリ:</label>
      <select id="category" bind:value={selectedCategory}>
        {#each categories as category}
          <option value={category}>
            {category === 'all' ? '全て' : category}
          </option>
        {/each}
      </select>
    </div>

    <div class="filter-group">
      <label for="min-price">価格範囲:</label>
      <div class="range-inputs">
        <input
          id="min-price"
          type="number"
          bind:value={minPrice}
          min="0"
          max={maxPrice}
        />
        <span></span>
        <input
          id="max-price"
          type="number"
          bind:value={maxPrice}
          min={minPrice}
        />
      </div>
    </div>

    <div class="filter-group">
      <label>
        <input type="checkbox" bind:checked={onlyInStock} />
        在庫ありのみ
      </label>
    </div>

    <div class="filter-group">
      <label for="min-rating">最低評価: {minRating}</label>
      <input
        id="min-rating"
        type="range"
        bind:value={minRating}
        min="0"
        max="5"
        step="0.5"
      />
    </div>
  </div>

  <div class="results">
    <div class="stats">
      <span>該当商品: {stats.total}</span>
      <span>平均価格: ¥{stats.avgPrice.toLocaleString()}</span>
      <span>平均評価: ★{stats.avgRating}</span>
      <span>在庫あり: {stats.inStockCount}</span>
    </div>

    <div class="product-list">
      {#if filteredProducts.length === 0}
        <p class="no-results">該当する商品がありません</p>
      {:else}
        {#each filteredProducts as product}
          <div class="product-card">
            <h4>{product.name}</h4>
            <p class="category">{product.category}</p>
            <p class="price">¥{product.price.toLocaleString()}</p>
            <p class="rating">{product.rating}</p>
            <p class="stock" class:out-of-stock={!product.inStock}>
              {product.inStock ? '在庫あり' : '在庫なし'}
            </p>
          </div>
        {/each}
      {/if}
    </div>
  </div>
</div>

<style>
  .search-filter-demo {
    display: grid;
    grid-template-columns: 250px 1fr;
    gap: 2rem;
    padding: 1rem;
  }

  .filters {
    color: white;
    background: #446;
    padding: 1rem;
    border-radius: 8px;
  }

  .filter-group {
    margin-bottom: 1rem;
    width: 100%;
  }

  .filter-group label {
    display: block;
    margin-bottom: 0.25rem;
    font-weight: bold;
  }

  .filter-group input[type="text"],
  .filter-group input[type="number"],
  .filter-group select {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    box-sizing: border-box;
  }

  .range-inputs {
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }

  .range-inputs input {
    width: 80px;
  }

  .stats {
    display: flex;
    color: black;
    gap: 1rem;
    padding: 1rem;
    background: #e8f4ff;
    border-radius: 4px;
    margin-bottom: 1rem;
    font-size: 0.9rem;
  }

  .product-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 1rem;
  }

  .product-card {
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 8px;
    background: #cce;
  }

  .product-card h4 {
    margin: 0 0 0.5rem 0;
    color: white;
  }

  .product-card p {
    margin: 0.25rem 0;
    font-size: 0.9rem;
  }

  .category {
    color: #666;
  }

  .price {
    font-weight: bold;
    color: #ff3e00;
  }

  .stock {
    color: green;
  }

  .stock.out-of-stock {
    color: #999;
  }
  .rating {
    color: white;
  }

  .no-results {
    grid-column: 1 / -1;
    text-align: center;
    padding: 2rem;
    color: #666;
  }
</style>
svelte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
Click fold/expand code

パフォーマンス最適化

$derivedを効率的に使用するためのパフォーマンス最適化テクニックを紹介します。 大規模なデータや複雑な計算を扱う際に特に重要です。

メモ化

$derivedの最大の利点の一つは、自動的なメモ化です。 同じ依存関係の値では再計算されず、キャッシュされた結果が使用されます。

$derivedは自動的にメモ化されます。同じ依存関係の値では再計算されません。

<script lang="ts">
  let numbers = $state([1, 2, 3, 4, 5]);
  let multiplier = $state(2);

  // この計算は依存関係が変わらない限り実行されない
  let expensiveCalculation = $derived.by(() => {
    console.log('計算実行'); // 依存関係が変わった時のみ出力
    return numbers.reduce((sum, n) => {
      // 重い計算をシミュレート
      for (let i = 0; i < 1000000; i++) {
        Math.sqrt(i);
      }
      return sum + n * multiplier;
    }, 0);
  });
</script>
svelte

細分化

大きな派生値を小さな部分に分割することで、パフォーマンスを向上できます。 各派生値が独立してメモ化されるため、必要な部分だけが再計算されます。

<script lang="ts">
  // ❌ 悪い例:すべてを1つの派生値で計算
  let everything = $derived.by(() => {
    const filtered = items.filter(/* ... */);
    const sorted = filtered.sort(/* ... */);
    const grouped = groupBy(sorted, /* ... */);
    const stats = calculateStats(grouped);
    return { filtered, sorted, grouped, stats };
  });

  // ✅ 良い例:段階的に派生値を作成(単純な式は$derivedでOK)
  let filtered = $derived(items.filter(i => i.active));
  let sorted = $derived([...filtered].sort((a, b) => a.name.localeCompare(b.name)));
  let grouped = $derived.by(() => groupBy(sorted, 'category'));
  let stats = $derived.by(() => calculateStats(grouped));
</script>
svelte

まとめ

$derivedルーンは、リアクティブな値から新しい値を派生させる強力な機能です。 主な特徴と利点は以下の通りです。

  • 自動追跡 - 依存関係を自動的に検出
  • メモ化 - 不要な再計算を避ける
  • 型安全 - TypeScript の型推論が機能
  • 宣言的 - 計算ロジックを明確に表現
他のフレームワークとの比較
  • Vue: computedとほぼ同じ
  • React: useMemoと似ているが、依存配列が不要
  • Angular: Computed signals と類似
  • MobX: computedと同じ概念

関連ドキュメント

さらに深く理解する

よくある質問(FAQ)

React useMemo との違いは?

項目React useMemoSvelte 5 $derived
宣言方法useMemo(() => value, [deps])$derived(value)
依存配列必須(手動で指定)不要(自動追跡)
再計算タイミングdeps 変更時依存値変更時(自動)
複雑なロジック同じ構文$derived.by()
メモ化deps 指定ミスで無効常に正確
参照安定性deps 次第値が同じなら同じ

Vue computed との違いは?

項目Vue 3 computedSvelte 5 $derived
宣言方法computed(() => value)$derived(value)
ゲッター/セッターありなし(オーバーライドで代替)
デバッグonTrack/onTrigger$inspect
TypeScript.valueアクセス必要直接アクセス
書き込み可能別途定義必要5.25+でオーバーライド可

$derived vs $derived.by の使い分け

条件使うべき API
単純な式(1 行)$derived$derived(count * 2)
配列メソッドチェーン$derived$derived(items.filter(...))
複数ステートメント$derived.byif 文、変数宣言を含む
早期リターン$derived.by条件による return
ループ処理$derived.byfor 文、reduce

コード比較:React useMemo vs Svelte $derived

// === React useMemo ===
const [items, setItems] = useState<Item[]>([]);
const [filter, setFilter] = useState('');

// 依存配列を手動で管理(漏れるとバグの原因)
const filteredItems = useMemo(() => {
	return items.filter((item) => item.name.includes(filter));
}, [items, filter]); // ← 依存配列必須

// === Svelte 5 $derived ===
let items = $state<Item[]>([]);
let filter = $state('');

// 依存関係は自動追跡(依存配列不要)
let filteredItems = $derived(items.filter((item) => item.name.includes(filter)));
typescript

配列のフィルタリングパターン

// 検索 + フィルタ + ソートの組み合わせ
let items = $state<Product[]>([...]);
let searchQuery = $state('');
let category = $state('all');
let sortBy = $state<'name' | 'price'>('name');

let results = $derived.by(() => {
  let filtered = items;

  // 検索フィルタ
  if (searchQuery) {
    filtered = filtered.filter(item =>
      item.name.toLowerCase().includes(searchQuery.toLowerCase())
    );
  }

  // カテゴリフィルタ
  if (category !== 'all') {
    filtered = filtered.filter(item => item.category === category);
  }

  // ソート
  return [...filtered].sort((a, b) => {
    if (sortBy === 'name') return a.name.localeCompare(b.name);
    return a.price - b.price;
  });
});
typescript

パフォーマンス:いつ$derived を分割すべきか?

シナリオアプローチ理由
軽量な計算1 つの$derivedオーバーヘッド少
重い計算 + 軽い計算分割軽い方の再計算回避
異なる依存関係分割独立した再計算
UI 表示用 + API 送信用分割用途別に最適化
// ❌ 結合された派生値(どちらかの依存が変わると全て再計算)
let combined = $derived.by(() => ({
	filtered: heavyFilter(items),
	stats: calculateStats(otherData),
}));

// ✅ 分割された派生値(独立して再計算)
let filtered = $derived.by(() => heavyFilter(items));
let stats = $derived.by(() => calculateStats(otherData));
typescript

次のステップ

$derivedで派生値の作成方法を学んだら、次は副作用の管理方法を学びましょう。 $effect - 副作用 では、リアクティブな値の変更に応じて副作用を実行する方法を詳しく解説します。

Last update at: 2026/01/08 14:52:14