Qwik Framework徹底解説 2025 - 究極のパフォーマンスを実現する次世代フレームワーク
Qwikの革新的なResumability技術と細粒度の遅延読み込みにより、どんなに大きなアプリケーションでも瞬時に起動。React/Next.jsを超えるパフォーマンスを実現する実装方法を徹底解説します。
2025年のフロントエンド開発における最新トレンドを徹底解説。マイクロフロントエンドアーキテクチャ、PWAの進化、Svelte・SolidJS・Qwikなど新世代フレームワークの実践的な活用方法を紹介します。
2025 年、フロントエンド開発は大きな転換期を迎えています。 マイクロフロントエンドアーキテクチャの成熟、PWA の標準化、そして新世代フレームワークの台頭により、 web 開発の可能性は飛躍的に拡大しました。
技術トレンド | 採用率 | 前年比 | 特徴 |
---|---|---|---|
マイクロフロントエンド | 42% | +65% | 大規模アプリケーションで急速に普及 |
PWA | 68% | +23% | モバイルファーストの標準技術に |
Svelte/SvelteKit | 28% | +45% | バンドルサイズとDXで高評価 |
SolidJS | 15% | +120% | React開発者から注目 |
Qwik | 8% | 新規 | レジューマビリティで革新 |
Web Components | 35% | +15% | 標準技術として安定成長 |
複雑性への不満が表面化
Svelte 4、SolidJS 1.8リリース
エンタープライズ採用が加速
AI支援開発が標準化へ
チャートを読み込み中...
// 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);
}
}
機能 | 対応状況 | 影響度 | 実装難易度 |
---|---|---|---|
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+ | 高 | 高 |
// 基本的な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)
);
});
// 基本的な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)
);
});
{
"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"]
}
チャートを読み込み中...
<!-- 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の新しいルーンシステム
<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>
// 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>
<!-- 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>
// 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実装
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>
);
}
// 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,
},
},
},
});
指標 | 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%向上しました。新世代フレームワークの選択も重要ですが、 アーキテクチャの設計がより重要になっています。
これらの技術を適切に組み合わせることで、2025 年の web 開発において 競争力のあるアプリケーションを構築できます。
2025 年のフロントエンドトレンドを理解した後は、具体的な技術の実装方法を学びましょう。