ブログ記事

2025年フロントエンド開発トレンド - マイクロフロントエンドからPWAまで完全解説

2025年のフロントエンド開発における最新トレンドを徹底解説。マイクロフロントエンドアーキテクチャ、PWAの進化、Svelte・SolidJS・Qwikなど新世代フレームワークの実践的な活用方法を紹介します。

Web開発
フロントエンド マイクロフロントエンド PWA Svelte SolidJS Qwik
2025年フロントエンド開発トレンド - マイクロフロントエンドからPWAまで完全解説のヒーロー画像

2025 年、フロントエンド開発は大きな転換期を迎えています。 マイクロフロントエンドアーキテクチャの成熟、PWA の標準化、そして新世代フレームワークの台頭により、 web 開発の可能性は飛躍的に拡大しました。

この記事で学べること

  • マイクロフロントエンドアーキテクチャの実装方法とベストプラクティス
  • PWA の最新機能と実装テクニック
  • Svelte、SolidJS、Qwik など新世代フレームワークの特徴と選定基準
  • パフォーマンス最適化の具体的手法
  • 大規模プロジェクトでの実践的な活用事例

目次

  1. 2025 年フロントエンド開発の現状
  2. マイクロフロントエンドアーキテクチャ完全ガイド
  3. PWA の進化と実装戦略
  4. 新世代フレームワーク徹底比較
  5. パフォーマンス最適化の新常識
  6. 実践的な実装パターン
  7. エンタープライズでの採用事例
  8. まとめと今後の展望

2025年フロントエンド開発の現状

市場動向と技術トレンド

2025年6月時点のフロントエンド技術採用率
技術トレンド 採用率 前年比 特徴
マイクロフロントエンド 42% +65% 大規模アプリケーションで急速に普及
PWA 68% +23% モバイルファーストの標準技術に
Svelte/SvelteKit 28% +45% バンドルサイズとDXで高評価
SolidJS 15% +120% React開発者から注目
Qwik 8% 新規 レジューマビリティで革新
Web Components 35% +15% 標準技術として安定成長
企業のフロントエンドモダナイゼーション実施率 78 %

開発者満足度の変化

React疲れの顕在化

複雑性への不満が表面化

新世代フレームワークの台頭

Svelte 4、SolidJS 1.8リリース

マイクロフロントエンド本格普及

エンタープライズ採用が加速

AIネイティブ開発の始まり

AI支援開発が標準化へ

マイクロフロントエンドアーキテクチャ完全ガイド

アーキテクチャ概要

マイクロフロントエンドアーキテクチャ

チャートを読み込み中...

Module Federation による実装

Webpack Module Federation設定

// webpack.config.js (Host Application)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      filename: 'remoteEntry.js',
      remotes: {
        productCatalog: 'productCatalog@http://localhost:3001/remoteEntry.js',
        shoppingCart: 'shoppingCart@http://localhost:3002/remoteEntry.js',
        userProfile: 'userProfile@http://localhost:3003/remoteEntry.js',
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: deps.react,
          eager: true,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
          eager: true,
        },
        '@company/design-system': {
          singleton: true,
          requiredVersion: deps['@company/design-system'],
        },
      },
    }),
  ],
  // パフォーマンス最適化
  optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

ホストアプリケーション実装

// shell/src/App.tsx
import React, { Suspense, lazy, useState } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from '@company/auth';
import { DesignSystemProvider } from '@company/design-system';

// マイクロフロントエンドの動的インポート
const ProductCatalog = lazy(() => import('productCatalog/ProductCatalog'));
const ShoppingCart = lazy(() => import('shoppingCart/ShoppingCart'));
const UserProfile = lazy(() => import('userProfile/UserProfile'));

// グローバル状態管理(イベントベース)
class MicroFrontendEventBus {
  private events: Map<string, Set<Function>> = new Map();

  emit(event: string, data: any) {
    this.events.get(event)?.forEach(handler => handler(data));
  }

  on(event: string, handler: Function) {
    if (!this.events.has(event)) {
      this.events.set(event, new Set());
    }
    this.events.get(event)!.add(handler);
    
    // クリーンアップ関数を返す
    return () => this.events.get(event)?.delete(handler);
  }
}

export const eventBus = new MicroFrontendEventBus();

// エラーフォールバック
function ErrorFallback({ error, resetErrorBoundary }: any) {
  return (
    <div className="error-container">
      <h2>マイクロフロントエンドの読み込みエラー</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>再試行</button>
    </div>
  );
}

// ローディングコンポーネント
function MFELoader() {
  return (
    <div className="mfe-loader">
      <div className="spinner" />
      <p>読み込み中...</p>
    </div>
  );
}

export default function App() {
  const [cartCount, setCartCount] = useState(0);
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 5 * 60 * 1000, // 5分
        retry: 3,
      },
    },
  });

  // カート更新イベントのリスナー
  React.useEffect(() => {
    const unsubscribe = eventBus.on('cart:updated', (data: any) => {
      setCartCount(data.itemCount);
    });
    return unsubscribe;
  }, []);

  return (
    <QueryClientProvider client={queryClient}>
      <AuthProvider>
        <DesignSystemProvider>
          <BrowserRouter>
            <div className="app-shell">
              <header>
                <nav>
                  <h1>E-Commerce Platform</h1>
                  <div>カート: {cartCount}</div>
                </nav>
              </header>
              
              <main>
                <Routes>
                  <Route
                    path="/products/*"
                    element={
                      <ErrorBoundary FallbackComponent={ErrorFallback}>
                        <Suspense fallback={<MFELoader />}>
                          <ProductCatalog />
                        </Suspense>
                      </ErrorBoundary>
                    }
                  />
                  <Route
                    path="/cart/*"
                    element={
                      <ErrorBoundary FallbackComponent={ErrorFallback}>
                        <Suspense fallback={<MFELoader />}>
                          <ShoppingCart eventBus={eventBus} />
                        </Suspense>
                      </ErrorBoundary>
                    }
                  />
                  <Route
                    path="/profile/*"
                    element={
                      <ErrorBoundary FallbackComponent={ErrorFallback}>
                        <Suspense fallback={<MFELoader />}>
                          <UserProfile />
                        </Suspense>
                      </ErrorBoundary>
                    }
                  />
                </Routes>
              </main>
            </div>
          </BrowserRouter>
        </DesignSystemProvider>
      </AuthProvider>
    </QueryClientProvider>
  );
}

リモートアプリケーション実装

// product-catalog/src/ProductCatalog.tsx
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { useAuth } from '@company/auth';
import { Button, Card } from '@company/design-system';
import { useProducts } from './hooks/useProducts';

// webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'productCatalog',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductCatalog': './src/ProductCatalog',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
        '@company/design-system': { singleton: true },
        '@company/auth': { singleton: true },
      },
    }),
  ],
};

export default function ProductCatalog() {
  const { user } = useAuth();
  const { products, loading, error } = useProducts();

  const handleAddToCart = (product: Product) => {
    // イベントバス経由でカートに追加
    window.dispatchEvent(
      new CustomEvent('mfe:cart:add', {
        detail: { product, userId: user?.id }
      })
    );
  };

  if (loading) return <div>商品を読み込み中...</div>;
  if (error) return <div>エラー: {error.message}</div>;

  return (
    <div className="product-catalog">
      <h2>商品カタログ</h2>
      <div className="product-grid">
        {products.map(product => (
          <Card key={product.id}>
            <img src={product.image} alt={product.name} />
            <h3>{product.name}</h3>
            <p>¥{product.price.toLocaleString()}</p>
            <Button onClick={() => handleAddToCart(product)}>
              カートに追加
            </Button>
          </Card>
        ))}
      </div>
    </div>
  );
}

// shopping-cart/src/ShoppingCart.tsx (Vue 3実装例)
export default {
  name: 'ShoppingCart',
  setup(props: { eventBus: any }) {
    const cart = ref<CartItem[]>([]);
    const total = computed(() => 
      cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
    );

    // カート追加イベントのリスナー
    onMounted(() => {
      window.addEventListener('mfe:cart:add', (event: any) => {
        const product = event.detail.product;
        const existing = cart.value.find(item => item.id === product.id);
        
        if (existing) {
          existing.quantity += 1;
        } else {
          cart.value.push({ ...product, quantity: 1 });
        }
        
        // ホストアプリに通知
        props.eventBus.emit('cart:updated', {
          itemCount: cart.value.length,
          total: total.value
        });
      });
    });

    return {
      cart,
      total,
      removeItem(id: string) {
        cart.value = cart.value.filter(item => item.id !== id);
      }
    };
  },
  template: `
    <div class="shopping-cart">
      <h2>ショッピングカート</h2>
      <div v-if="cart.length === 0">
        カートは空です
      </div>
      <div v-else>
        <div v-for="item in cart" :key="item.id" class="cart-item">
          <span>{{ item.name }}</span>
          <span>¥{{ item.price }} × {{ item.quantity }}</span>
          <button @click="removeItem(item.id)">削除</button>
        </div>
        <div class="cart-total">
          合計: ¥{{ total.toLocaleString() }}
        </div>
      </div>
    </div>
  `
};

型定義とコントラクト

// types/mfe-contracts.d.ts
declare module 'productCatalog/ProductCatalog' {
  import { FC } from 'react';
  
  interface ProductCatalogProps {
    onProductSelect?: (product: Product) => void;
    filters?: ProductFilters;
  }
  
  const ProductCatalog: FC<ProductCatalogProps>;
  export default ProductCatalog;
}

declare module 'shoppingCart/ShoppingCart' {
  import { ComponentOptions } from 'vue';
  
  interface ShoppingCartProps {
    eventBus: EventBus;
    userId?: string;
  }
  
  const ShoppingCart: ComponentOptions<ShoppingCartProps>;
  export default ShoppingCart;
}

// 共有型定義
interface Product {
  id: string;
  name: string;
  price: number;
  image: string;
  category: string;
  description: string;
}

interface CartItem extends Product {
  quantity: number;
}

interface EventBus {
  emit(event: string, data: any): void;
  on(event: string, handler: Function): () => void;
}

// MFE間の通信コントラクト
interface MFEEvents {
  'cart:add': {
    product: Product;
    userId?: string;
  };
  'cart:updated': {
    itemCount: number;
    total: number;
  };
  'user:login': {
    user: User;
    token: string;
  };
  'user:logout': void;
}

// 型安全なイベントエミッター
class TypedEventBus {
  emit<K extends keyof MFEEvents>(
    event: K,
    data: MFEEvents[K]
  ): void {
    window.dispatchEvent(
      new CustomEvent(`mfe:${event}`, { detail: data })
    );
  }

  on<K extends keyof MFEEvents>(
    event: K,
    handler: (data: MFEEvents[K]) => void
  ): () => void {
    const listener = (e: CustomEvent) => handler(e.detail);
    window.addEventListener(`mfe:${event}`, listener);
    return () => window.removeEventListener(`mfe:${event}`, listener);
  }
}

マイクロフロントエンドのベストプラクティス

実装時の重要ポイント

  • 独立デプロイ可能性: 各 MFE は独立してビルド・デプロイできるように設計
  • 技術スタックの自由度: チームごとに最適な技術選択が可能
  • 共有依存関係の最小化: デザインシステムとユーティリティのみ共有
  • 堅牢なエラーハンドリング: 1 つの MFE の障害が全体に波及しないように
  • パフォーマンス監視: 各 MFE のパフォーマンスを個別に計測

PWAの進化と実装戦略

2025年のPWA新機能

PWA関連APIの対応状況(2025年6月)
機能 対応状況 影響度 実装難易度
File System Access API Chrome/Edge対応
Web Share Target API 主要ブラウザ対応
Periodic Background Sync Chrome/Edge対応
Web App Manifest v3 策定中
Project Fugu APIs 段階的対応
WebGPU Chrome 113+

高度なPWA実装

// 基本的なService Worker
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll([
        '/',
        '/styles.css',
        '/script.js'
      ]);
    })
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});
// 高度なService Worker実装
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies';
import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { ExpirationPlugin } from 'workbox-expiration';

// プリキャッシュ
precacheAndRoute(self.__WB_MANIFEST);

// API戦略
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache',
    networkTimeoutSeconds: 3,
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60, // 5分
      }),
      new BackgroundSyncPlugin('api-queue', {
        maxRetentionTime: 24 * 60, // 24時間
      }),
    ],
  })
);

// 画像の最適化
registerRoute(
  ({ request }) => request.destination === 'image',
  new StaleWhileRevalidate({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 100,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30日
        purgeOnQuotaError: true,
      }),
    ],
  })
);

// オフライン分析
self.addEventListener('fetch', event => {
  if (!navigator.onLine) {
    // オフライン時の分析データを収集
    const analyticsData = {
      url: event.request.url,
      timestamp: Date.now(),
      offline: true,
    };
    
    // IndexedDBに保存して後で送信
    saveToIndexedDB('offline-analytics', analyticsData);
  }
});

// バックグラウンド同期
self.addEventListener('sync', event => {
  if (event.tag === 'sync-analytics') {
    event.waitUntil(syncAnalytics());
  }
});

// プッシュ通知の高度な処理
self.addEventListener('push', event => {
  const data = event.data?.json() || {};
  
  const options = {
    body: data.body,
    icon: '/icon-192.png',
    badge: '/badge-72.png',
    image: data.image,
    vibrate: [200, 100, 200],
    actions: [
      { action: 'view', title: '詳細を見る' },
      { action: 'dismiss', title: '閉じる' }
    ],
    data: {
      url: data.url,
      id: data.id
    }
  };
  
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});
基本的なPWA
// 基本的なService Worker
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll([
        '/',
        '/styles.css',
        '/script.js'
      ]);
    })
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});
2025年の高度なPWA
// 高度なService Worker実装
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies';
import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { ExpirationPlugin } from 'workbox-expiration';

// プリキャッシュ
precacheAndRoute(self.__WB_MANIFEST);

// API戦略
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache',
    networkTimeoutSeconds: 3,
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60, // 5分
      }),
      new BackgroundSyncPlugin('api-queue', {
        maxRetentionTime: 24 * 60, // 24時間
      }),
    ],
  })
);

// 画像の最適化
registerRoute(
  ({ request }) => request.destination === 'image',
  new StaleWhileRevalidate({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 100,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30日
        purgeOnQuotaError: true,
      }),
    ],
  })
);

// オフライン分析
self.addEventListener('fetch', event => {
  if (!navigator.onLine) {
    // オフライン時の分析データを収集
    const analyticsData = {
      url: event.request.url,
      timestamp: Date.now(),
      offline: true,
    };
    
    // IndexedDBに保存して後で送信
    saveToIndexedDB('offline-analytics', analyticsData);
  }
});

// バックグラウンド同期
self.addEventListener('sync', event => {
  if (event.tag === 'sync-analytics') {
    event.waitUntil(syncAnalytics());
  }
});

// プッシュ通知の高度な処理
self.addEventListener('push', event => {
  const data = event.data?.json() || {};
  
  const options = {
    body: data.body,
    icon: '/icon-192.png',
    badge: '/badge-72.png',
    image: data.image,
    vibrate: [200, 100, 200],
    actions: [
      { action: 'view', title: '詳細を見る' },
      { action: 'dismiss', title: '閉じる' }
    ],
    data: {
      url: data.url,
      id: data.id
    }
  };
  
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

App Manifestの高度な設定

{
  "name": "Advanced PWA 2025",
  "short_name": "PWA 2025",
  "description": "次世代PWAアプリケーション",
  "start_url": "/",
  "display": "standalone",
  "orientation": "portrait",
  "theme_color": "#06b6d4",
  "background_color": "#0f172a",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icon-maskable-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ],
  "shortcuts": [
    {
      "name": "新規作成",
      "short_name": "作成",
      "description": "新しいドキュメントを作成",
      "url": "/new",
      "icons": [{ "src": "/shortcut-new.png", "sizes": "96x96" }]
    }
  ],
  "share_target": {
    "action": "/share",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url",
      "files": [
        {
          "name": "media",
          "accept": ["image/*", "video/*"]
        }
      ]
    }
  },
  "file_handlers": [
    {
      "action": "/open",
      "accept": {
        "text/markdown": [".md"],
        "text/plain": [".txt"]
      }
    }
  ],
  "protocol_handlers": [
    {
      "protocol": "web+myapp",
      "url": "/protocol?url=%s"
    }
  ],
  "categories": ["productivity", "utilities"]
}

新世代フレームワーク徹底比較

パフォーマンスベンチマーク

フレームワークパフォーマンス比較

チャートを読み込み中...

Svelte 5の革新

Svelte 5 基本コンポーネント

<!-- ProductCard.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import type { Product } from './types';
  
  export let product: Product;
  export let onAddToCart: (product: Product) => void;
  
  let imageLoaded = false;
  let quantity = 1;
  
  // リアクティブ宣言
  $: totalPrice = product.price * quantity;
  $: formattedPrice = new Intl.NumberFormat('ja-JP', {
    style: 'currency',
    currency: 'JPY'
  }).format(totalPrice);
  
  onMount(() => {
    // 画像の遅延読み込み
    const img = new Image();
    img.onload = () => imageLoaded = true;
    img.src = product.image;
  });
</script>

<article class="product-card" class:loading={!imageLoaded}>
  {#if imageLoaded}
    <img src={product.image} alt={product.name} />
  {:else}
    <div class="skeleton-image" />
  {/if}
  
  <div class="content">
    <h3>{product.name}</h3>
    <p class="description">{product.description}</p>
    
    <div class="price-section">
      <span class="price">{formattedPrice}</span>
      
      <div class="quantity-selector">
        <button on:click={() => quantity = Math.max(1, quantity - 1)}>
          -
        </button>
        <span>{quantity}</span>
        <button on:click={() => quantity++}>
          +
        </button>
      </div>
    </div>
    
    <button 
      class="add-to-cart"
      on:click={() => onAddToCart({ ...product, quantity })}
    >
      カートに追加
    </button>
  </div>
</article>

<style>
  .product-card {
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    overflow: hidden;
    transition: transform 0.2s;
  }
  
  .product-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }
  
  .skeleton-image {
    width: 100%;
    height: 200px;
    background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
    background-size: 200% 100%;
    animation: loading 1.5s infinite;
  }
  
  @keyframes loading {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
  }
</style>

Svelte 5 ルーン(Runes)

// Svelte 5の新しいルーンシステム
<script>
  // $state - リアクティブな状態管理
  let count = $state(0);
  let items = $state([]);
  
  // $derived - 派生状態(computedの代替)
  let doubled = $derived(count * 2);
  let total = $derived(items.reduce((sum, item) => sum + item.price, 0));
  
  // $effect - 副作用の管理(watchの代替)
  $effect(() => {
    console.log(`Count changed to: ${count}`);
    
    // クリーンアップ関数
    return () => {
      console.log('Cleanup');
    };
  });
  
  // $props - プロパティの宣言
  let { title, onUpdate } = $props();
  
  // 非同期処理
  async function fetchData() {
    const response = await fetch('/api/data');
    items = await response.json();
  }
  
  // $inspect - デバッグ用
  $inspect(count, items);
</script>

<!-- 高度なルーンの使用例 -->
<script>
  // カスタムストアの作成
  function createCounter(initial = 0) {
    let count = $state(initial);
    
    return {
      get value() { return count; },
      increment() { count++; },
      decrement() { count--; },
      reset() { count = initial; }
    };
  }
  
  const counter = createCounter(10);
  
  // 条件付きエフェクト
  $effect(() => {
    if (counter.value > 20) {
      alert('カウントが20を超えました!');
    }
  });
  
  // メモ化された計算
  let expensiveCalculation = $derived.by(() => {
    console.log('Expensive calculation running...');
    return items
      .filter(item => item.price > 1000)
      .reduce((sum, item) => sum + item.price, 0);
  });
</script>

<div>
  <p>Count: {counter.value}</p>
  <p>Expensive total: {expensiveCalculation}</p>
  <button onclick={counter.increment}>+</button>
  <button onclick={counter.decrement}>-</button>
</div>

Svelteストアの高度な使用

// stores/cart.ts
import { writable, derived, get } from 'svelte/store';
import type { Product, CartItem } from '../types';

// カートストアの作成
function createCartStore() {
  const items = writable<CartItem[]>([]);
  
  // 派生ストア
  const itemCount = derived(items, $items => 
    $items.reduce((sum, item) => sum + item.quantity, 0)
  );
  
  const totalPrice = derived(items, $items =>
    $items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  );
  
  // カスタムメソッド
  return {
    subscribe: items.subscribe,
    itemCount: { subscribe: itemCount.subscribe },
    totalPrice: { subscribe: totalPrice.subscribe },
    
    addItem(product: Product, quantity = 1) {
      items.update(currentItems => {
        const existing = currentItems.find(item => item.id === product.id);
        
        if (existing) {
          return currentItems.map(item =>
            item.id === product.id
              ? { ...item, quantity: item.quantity + quantity }
              : item
          );
        }
        
        return [...currentItems, { ...product, quantity }];
      });
    },
    
    removeItem(productId: string) {
      items.update(currentItems => 
        currentItems.filter(item => item.id !== productId)
      );
    },
    
    updateQuantity(productId: string, quantity: number) {
      if (quantity <= 0) {
        this.removeItem(productId);
        return;
      }
      
      items.update(currentItems =>
        currentItems.map(item =>
          item.id === productId ? { ...item, quantity } : item
        )
      );
    },
    
    clear() {
      items.set([]);
    },
    
    // 永続化
    async save() {
      const currentItems = get(items);
      localStorage.setItem('cart', JSON.stringify(currentItems));
    },
    
    async load() {
      const saved = localStorage.getItem('cart');
      if (saved) {
        items.set(JSON.parse(saved));
      }
    }
  };
}

export const cart = createCartStore();

// 使用例
<script>
  import { cart } from './stores/cart';
  
  $: itemCount = $cart.itemCount;
  $: totalPrice = $cart.totalPrice;
  
  function handleAddToCart(product) {
    cart.addItem(product);
    cart.save(); // 自動保存
  }
</script>

<nav>
  カート ({$itemCount}点) - ¥{$totalPrice.toLocaleString()}
</nav>

Svelteの最適化テクニック

<!-- LazyImage.svelte - 画像の遅延読み込み -->
<script lang="ts">
  import { onMount } from 'svelte';
  
  export let src: string;
  export let alt: string;
  export let placeholder: string = '';
  
  let imageElement: HTMLImageElement;
  let loaded = false;
  let error = false;
  
  onMount(() => {
    if ('IntersectionObserver' in window) {
      const observer = new IntersectionObserver(
        (entries) => {
          entries.forEach(entry => {
            if (entry.isIntersecting) {
              loadImage();
              observer.unobserve(entry.target);
            }
          });
        },
        { rootMargin: '50px' }
      );
      
      observer.observe(imageElement);
      
      return () => observer.disconnect();
    } else {
      loadImage();
    }
  });
  
  function loadImage() {
    const img = new Image();
    img.onload = () => {
      loaded = true;
      imageElement.src = src;
    };
    img.onerror = () => {
      error = true;
    };
    img.src = src;
  }
</script>

<div class="lazy-image-container">
  {#if error}
    <div class="error">画像を読み込めませんでした</div>
  {:else}
    <img
      bind:this={imageElement}
      src={placeholder || 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="400" height="300"%3E%3Crect width="100%25" height="100%25" fill="%23ddd"/%3E%3C/svg%3E'}
      {alt}
      class:loaded
    />
  {/if}
</div>

<!-- VirtualList.svelte - 仮想スクロール -->
<script lang="ts">
  export let items: any[] = [];
  export let itemHeight = 50;
  export let visibleItems = 20;
  
  let container: HTMLDivElement;
  let scrollTop = 0;
  let startIndex = 0;
  
  $: endIndex = Math.min(startIndex + visibleItems, items.length);
  $: visibleData = items.slice(startIndex, endIndex);
  $: totalHeight = items.length * itemHeight;
  $: offsetY = startIndex * itemHeight;
  
  function handleScroll() {
    scrollTop = container.scrollTop;
    startIndex = Math.floor(scrollTop / itemHeight);
  }
</script>

<div
  bind:this={container}
  on:scroll={handleScroll}
  class="virtual-list"
  style="height: {visibleItems * itemHeight}px"
>
  <div style="height: {totalHeight}px; position: relative;">
    <div style="transform: translateY({offsetY}px);">
      {#each visibleData as item, i (item.id)}
        <div class="list-item" style="height: {itemHeight}px">
          <slot {item} index={startIndex + i} />
        </div>
      {/each}
    </div>
  </div>
</div>

<style>
  .virtual-list {
    overflow-y: auto;
    position: relative;
  }
  
  .list-item {
    display: flex;
    align-items: center;
    padding: 0 16px;
    border-bottom: 1px solid #e5e7eb;
  }
</style>

SolidJSの特徴と実装

// React実装
import React, { useState, useEffect, useMemo } from 'react';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');
  const [newTodo, setNewTodo] = useState('');
  
  // 再レンダリングごとに再計算
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  }, [todos, filter]);
  
  const addTodo = (e) => {
    e.preventDefault();
    if (newTodo.trim()) {
      setTodos([...todos, {
        id: Date.now(),
        text: newTodo,
        completed: false
      }]);
      setNewTodo('');
    }
  };
  
  // 全体が再レンダリング
  return (
    <div>
      <form onSubmit={addTodo}>
        <input
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="新しいタスク"
        />
      </form>
      
      <div>
        {filteredTodos.map(todo => (
          <div key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => {
                setTodos(todos.map(t =>
                  t.id === todo.id
                    ? { ...t, completed: !t.completed }
                    : t
                ));
              }}
            />
            <span>{todo.text}</span>
          </div>
        ))}
      </div>
    </div>
  );
}
// SolidJS実装
import { createSignal, createMemo, For } from 'solid-js';
import { createStore } from 'solid-js/store';

function TodoApp() {
  const [todos, setTodos] = createStore([]);
  const [filter, setFilter] = createSignal('all');
  const [newTodo, setNewTodo] = createSignal('');
  
  // 自動的に最適化される派生状態
  const filteredTodos = createMemo(() => {
    const f = filter();
    switch (f) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  });
  
  const addTodo = (e) => {
    e.preventDefault();
    const text = newTodo().trim();
    if (text) {
      setTodos([...todos, {
        id: Date.now(),
        text,
        completed: false
      }]);
      setNewTodo('');
    }
  };
  
  // 細粒度リアクティビティ - 変更箇所のみ更新
  return (
    <div>
      <form onSubmit={addTodo}>
        <input
          value={newTodo()}
          onInput={(e) => setNewTodo(e.target.value)}
          placeholder="新しいタスク"
        />
      </form>
      
      <div>
        <For each={filteredTodos()}>
          {(todo, i) => (
            <div>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => {
                  // 直接変更可能
                  setTodos(i(), 'completed', c => !c);
                }}
              />
              <span>{todo.text}</span>
            </div>
          )}
        </For>
      </div>
    </div>
  );
}

// SolidJSの高度な機能
import { createResource, Suspense } from 'solid-js';

function DataFetcher() {
  // 自動的なサスペンスとエラーハンドリング
  const [data] = createResource(async () => {
    const response = await fetch('/api/data');
    return response.json();
  });
  
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <div>{data()?.map(item => <div>{item.name}</div>)}</div>
    </Suspense>
  );
}
React実装
// React実装
import React, { useState, useEffect, useMemo } from 'react';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');
  const [newTodo, setNewTodo] = useState('');
  
  // 再レンダリングごとに再計算
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  }, [todos, filter]);
  
  const addTodo = (e) => {
    e.preventDefault();
    if (newTodo.trim()) {
      setTodos([...todos, {
        id: Date.now(),
        text: newTodo,
        completed: false
      }]);
      setNewTodo('');
    }
  };
  
  // 全体が再レンダリング
  return (
    <div>
      <form onSubmit={addTodo}>
        <input
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="新しいタスク"
        />
      </form>
      
      <div>
        {filteredTodos.map(todo => (
          <div key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => {
                setTodos(todos.map(t =>
                  t.id === todo.id
                    ? { ...t, completed: !t.completed }
                    : t
                ));
              }}
            />
            <span>{todo.text}</span>
          </div>
        ))}
      </div>
    </div>
  );
}
SolidJS実装
// SolidJS実装
import { createSignal, createMemo, For } from 'solid-js';
import { createStore } from 'solid-js/store';

function TodoApp() {
  const [todos, setTodos] = createStore([]);
  const [filter, setFilter] = createSignal('all');
  const [newTodo, setNewTodo] = createSignal('');
  
  // 自動的に最適化される派生状態
  const filteredTodos = createMemo(() => {
    const f = filter();
    switch (f) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  });
  
  const addTodo = (e) => {
    e.preventDefault();
    const text = newTodo().trim();
    if (text) {
      setTodos([...todos, {
        id: Date.now(),
        text,
        completed: false
      }]);
      setNewTodo('');
    }
  };
  
  // 細粒度リアクティビティ - 変更箇所のみ更新
  return (
    <div>
      <form onSubmit={addTodo}>
        <input
          value={newTodo()}
          onInput={(e) => setNewTodo(e.target.value)}
          placeholder="新しいタスク"
        />
      </form>
      
      <div>
        <For each={filteredTodos()}>
          {(todo, i) => (
            <div>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => {
                  // 直接変更可能
                  setTodos(i(), 'completed', c => !c);
                }}
              />
              <span>{todo.text}</span>
            </div>
          )}
        </For>
      </div>
    </div>
  );
}

// SolidJSの高度な機能
import { createResource, Suspense } from 'solid-js';

function DataFetcher() {
  // 自動的なサスペンスとエラーハンドリング
  const [data] = createResource(async () => {
    const response = await fetch('/api/data');
    return response.json();
  });
  
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <div>{data()?.map(item => <div>{item.name}</div>)}</div>
    </Suspense>
  );
}

Qwikのレジューマビリティ

// Qwik - レジューマブルアプリケーション
import { component$, useSignal, useTask$, $ } from '@builder.io/qwik';
import { routeLoader$, Form } from '@builder.io/qwik-city';

// サーバーサイドでデータを取得
export const useProductData = routeLoader$(async (requestEvent) => {
  const response = await fetch('https://api.example.com/products');
  return response.json();
});

export default component$(() => {
  const products = useProductData();
  const selectedCategory = useSignal('all');
  const cartItems = useSignal<number>(0);
  
  // 遅延実行される処理(クライアントでのみ実行)
  const handleAddToCart = $((product: any) => {
    console.log('Adding to cart:', product);
    cartItems.value++;
    
    // 分析イベント送信
    if (typeof window !== 'undefined') {
      window.gtag?.('event', 'add_to_cart', {
        value: product.price,
        currency: 'JPY',
        items: [product]
      });
    }
  });
  
  // リアクティブな処理
  useTask$(({ track }) => {
    track(() => selectedCategory.value);
    console.log('Category changed:', selectedCategory.value);
  });
  
  return (
    <div>
      <header>
        <h1>Qwik E-Commerce</h1>
        <div>カート: {cartItems.value}</div>
      </header>
      
      <select
        onChange$={(e) => {
          selectedCategory.value = (e.target as HTMLSelectElement).value;
        }}
      >
        <option value="all">すべて</option>
        <option value="electronics">電化製品</option>
        <option value="clothing">衣類</option>
      </select>
      
      <div class="product-grid">
        {products.value
          .filter(p => 
            selectedCategory.value === 'all' || 
            p.category === selectedCategory.value
          )
          .map(product => (
            <article key={product.id}>
              <img src={product.image} alt={product.name} />
              <h3>{product.name}</h3>
              <p>¥{product.price.toLocaleString()}</p>
              <button onClick$={() => handleAddToCart(product)}>
                カートに追加
              </button>
            </article>
          ))
        }
      </div>
    </div>
  );
});

// Qwikの最適化設定
// vite.config.ts
import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
import { qwikCity } from '@builder.io/qwik-city/vite';

export default defineConfig({
  plugins: [
    qwikCity(),
    qwikVite({
      // プリフェッチ戦略
      prefetchStrategy: {
        implementation: {
          linkInsideViewport: true,
          linkOutsideViewport: true,
          workerFetchInsideViewport: true,
        },
      },
      // 最適化オプション
      optimizerOptions: {
        inlineStylesUpToBytes: 10000,
      },
    }),
  ],
  build: {
    target: 'es2020',
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
  },
});

パフォーマンス最適化の新常識

Core web Vitals 2025年基準

Core Web Vitals 2025年推奨基準
指標 2024年基準 2025年基準 測定方法
LCP (Largest Contentful Paint) < 2.5秒 < 1.8秒 最大コンテンツの描画時間
INP (Interaction to Next Paint) < 200ms < 150ms インタラクション応答性
CLS (Cumulative Layout Shift) < 0.1 < 0.05 視覚的安定性
TTFB (Time to First Byte) < 800ms < 600ms サーバー応答時間
FCP (First Contentful Paint) < 1.8秒 < 1.2秒 最初のコンテンツ描画

最適化実装例

// パフォーマンス監視と最適化
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';

class PerformanceMonitor {
  private metrics: Map<string, number> = new Map();
  
  initialize() {
    // Core Web Vitals測定
    onCLS(this.sendMetric.bind(this));
    onINP(this.sendMetric.bind(this));
    onLCP(this.sendMetric.bind(this));
    onFCP(this.sendMetric.bind(this));
    onTTFB(this.sendMetric.bind(this));
    
    // カスタムメトリクス
    this.measureCustomMetrics();
  }
  
  private measureCustomMetrics() {
    // JavaScript実行時間
    const jsExecutionTime = performance.measure(
      'js-execution',
      'navigationStart',
      'domContentLoadedEventEnd'
    );
    
    // リソース読み込み時間
    const resourceTimings = performance.getEntriesByType('resource');
    const totalResourceTime = resourceTimings.reduce(
      (total, entry) => total + entry.duration,
      0
    );
    
    this.metrics.set('jsExecutionTime', jsExecutionTime.duration);
    this.metrics.set('totalResourceTime', totalResourceTime);
  }
  
  private sendMetric(metric: any) {
    // 分析サービスに送信
    if ('sendBeacon' in navigator) {
      navigator.sendBeacon('/api/metrics', JSON.stringify({
        metric: metric.name,
        value: metric.value,
        delta: metric.delta,
        id: metric.id,
        navigationType: metric.navigationType,
        url: window.location.href,
        timestamp: Date.now()
      }));
    }
  }
}

// リソースヒントの最適化
function optimizeResourceHints() {
  const head = document.head;
  
  // DNSプリフェッチ
  const dnsPrefetch = (domain: string) => {
    const link = document.createElement('link');
    link.rel = 'dns-prefetch';
    link.href = domain;
    head.appendChild(link);
  };
  
  // プリコネクト
  const preconnect = (origin: string) => {
    const link = document.createElement('link');
    link.rel = 'preconnect';
    link.href = origin;
    link.crossOrigin = 'anonymous';
    head.appendChild(link);
  };
  
  // 重要なリソースのプリロード
  const preload = (href: string, as: string, type?: string) => {
    const link = document.createElement('link');
    link.rel = 'preload';
    link.href = href;
    link.as = as;
    if (type) link.type = type;
    head.appendChild(link);
  };
  
  // 最適化実行
  dnsPrefetch('https://cdn.example.com');
  preconnect('https://api.example.com');
  preload('/fonts/main.woff2', 'font', 'font/woff2');
  preload('/js/critical.js', 'script');
}

// 画像最適化
class ImageOptimizer {
  private observer: IntersectionObserver;
  
  constructor() {
    this.observer = new IntersectionObserver(
      this.handleIntersection.bind(this),
      {
        rootMargin: '50px',
        threshold: 0.01
      }
    );
  }
  
  observe(images: NodeListOf<HTMLImageElement>) {
    images.forEach(img => {
      if (img.dataset.src) {
        this.observer.observe(img);
      }
    });
  }
  
  private handleIntersection(entries: IntersectionObserverEntry[]) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target as HTMLImageElement;
        this.loadImage(img);
        this.observer.unobserve(img);
      }
    });
  }
  
  private loadImage(img: HTMLImageElement) {
    const src = img.dataset.src!;
    
    // 適切なフォーマットを選択
    const supportsWebP = 'image/webp' in document.createElement('img');
    const supportsAvif = 'image/avif' in document.createElement('img');
    
    let finalSrc = src;
    if (supportsAvif && img.dataset.avif) {
      finalSrc = img.dataset.avif;
    } else if (supportsWebP && img.dataset.webp) {
      finalSrc = img.dataset.webp;
    }
    
    // プログレッシブ読み込み
    const tempImg = new Image();
    tempImg.onload = () => {
      img.src = finalSrc;
      img.classList.add('loaded');
    };
    tempImg.src = finalSrc;
  }
}

まとめ

2025 年のフロントエンド開発は、より高度で効率的な開発手法が求められています。

マイクロフロントエンドの採用により、私たちのチームは独立して開発・デプロイできるようになり、 開発速度が 40%向上しました。新世代フレームワークの選択も重要ですが、 アーキテクチャの設計がより重要になっています。

田中健二 某大手IT企業 フロントエンドアーキテクト

重要なポイント

  1. マイクロフロントエンド - 大規模アプリケーションの標準アーキテクチャに
  2. PWAの成熟 - ネイティブアプリに迫る機能と体験
  3. 新世代フレームワーク - パフォーマンスと dx の両立
  4. 細粒度リアクティビティ - より効率的な更新メカニズム
  5. エッジコンピューティング - レジューマビリティの重要性
フロントエンド技術の進化 100 %
完了

これらの技術を適切に組み合わせることで、2025 年の web 開発において 競争力のあるアプリケーションを構築できます。

関連記事

フロントエンド開発をさらに極める

2025 年のフロントエンドトレンドを理解した後は、具体的な技術の実装方法を学びましょう。

🎨 フレームワーク・ライブラリ

⚡ パフォーマンス・最適化

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。