こんにちは。よっしーです(^^)
今日は、SvelteKitでのパフォーマンスについて解説しています。
背景
SvelteKitでのパフォーマンスについて調査する機会がありましたので、その時の内容を備忘として記事に残しました。
ナビゲーション
プリロード
リンクオプションを使用して、必要なコードとデータを積極的にプリロードすることで、クライアントサイドのナビゲーションを高速化できます。これは新しいSvelteKitアププリケーションを作成する際、デフォルトで`<body>`要素に設定されています。
- 基本的なプリロード設定
<!-- app.html -->
<html>
<head>
%sveltekit.head%
</head>
<!-- デフォルトのプリロード設定 -->
<body data-sveltekit-preload-data="hover">
%sveltekit.body%
</body>
</html>
- プリロードオプションの実装例
<!-- カスタムプリロード動作の設定 -->
<a
href="/about"
data-sveltekit-preload-data="hover" <!-- ホバー時にプリロード -->
data-sveltekit-preload-code="eager" <!-- 即時コードプリロード -->
>
About Us
</a>
プリロード戦略のオプション:
- データプリロード(data-sveltekit-preload-data)
// オプション一覧
"hover" // マウスホバー時にプリロード
"tap" // タップ/クリック時にプリロード
"off" // プリロードを無効化
- コードプリロード(data-sveltekit-preload-code)
"eager" // 即時プリロード
"viewport" // ビューポート内に入った時にプリロード
"hover" // ホバー時にプリロード
"tap" // タップ/クリック時にプリロード
"off" // プリロードを無効化
実装のベストプラクティス:
- 選択的プリロード
<!-- 重要なリンクの優先プリロード -->
<a
href="/important-page"
data-sveltekit-preload-data="eager"
data-sveltekit-preload-code="eager"
>
重要なページ
</a>
<!-- 二次的なリンクの遅延プリロード -->
<a
href="/secondary-page"
data-sveltekit-preload-data="hover"
data-sveltekit-preload-code="viewport"
>
その他のページ
</a>
- プログラマティックなプリロード制御
// プリロードの手動制御
import { preloadData, preloadCode } from '$app/navigation';
// データのプリロード
async function handleHover(path) {
await preloadData(path);
}
// コードのプリロード
async function handleScroll(path) {
await preloadCode(path);
}
- 条件付きプリロード
<script>
import { browser } from '$app/environment';
let shouldPreload = browser && navigator.connection?.saveData !== true;
</script>
<a
href="/data-heavy-page"
data-sveltekit-preload-data={shouldPreload ? 'hover' : 'tap'}
>
大容量データページ
</a>
最適化のチェックリスト:
- パフォーマンス考慮事項
- ネットワーク帯域の使用
- メモリ使用量
- 優先順位付け
- ユーザー体験の最適化
- 適切なタイミングでのプリロード
- フォールバック動作
- エラーハンドリング
- リソースの効率的な使用
- 重要なリソースの特定
- 帯域幅の考慮
- キャッシュ戦略
- モニタリングと分析
// プリロードパフォーマンスの監視
let preloadStart;
let preloadEnd;
navigation.subscribe(({ from, to }) => {
if (from && to) {
preloadEnd = performance.now();
console.log(`Navigation preload took: ${preloadEnd - preloadStart}ms`);
}
});
// プリロード開始時の記録
function handlePreloadStart() {
preloadStart = performance.now();
}
これらの最適化を適切に実装することで、ユーザーの体感速度を向上させ、よりスムーズなナビゲーション体験を提供することができます。
非必須データ
すぐには必要ない、読み込みに時間のかかるデータについては、`load`関数から返すオブジェクトにデータそのものではなく、プロミスを含めることができます。サーバーの`load`関数の場合、これによりナビゲーション(または初期ページ読み込み)の後にデータをストリーミングすることができます。
- 基本的な実装例
// routes/+page.server.js
export async function load() {
return {
// 即時必要なデータ
criticalData: await fetchCriticalData(),
// 遅延ロードする非必須データ
nonEssentialData: fetchNonEssentialData() // Promiseを直接返す
};
}
- クライアントでの使用例
<!-- +page.svelte -->
<script>
export let data;
// 非必須データが準備できたら表示
$: ({ criticalData, nonEssentialData } = data);
</script>
<!-- 重要なコンテンツを即時表示 -->
<div>
<h1>{criticalData.title}</h1>
<!-- 非必須データの遅延ロード処理 -->
{#await nonEssentialData}
<p>Loading additional content...</p>
{:then additionalData}
<div class="additional-content">
{additionalData.content}
</div>
{:catch error}
<p class="error">Failed to load additional content</p>
{/await}
</div>
実践的な実装パターン:
- 優先順位付けされたデータロード
// routes/+page.server.js
export async function load() {
return {
// 優先度の高いデータ(即時ロード)
header: await fetchHeaderData(),
navigation: await fetchNavigationData(),
// 優先度の中程度のデータ(遅延ロード)
content: fetchMainContent(),
// 優先度の低いデータ(さらに遅延)
recommendations: fetchRecommendations(),
analytics: fetchAnalytics()
};
}
- 条件付き遅延ロード
// routes/+page.server.js
export async function load({ url }) {
const isPreview = url.searchParams.get('preview') === 'true';
return {
// プレビューモードの場合は即時ロード
content: isPreview
? await fetchFullContent()
: fetchFullContent(), // 通常は遅延ロード
metadata: await fetchMetadata()
};
}
- ストリーミングデータの処理
<!-- コンポーネントでのストリーミングデータ処理 -->
<script>
export let data;
let loadedSections = new Set();
function trackLoadedSection(section) {
loadedSections.add(section);
}
</script>
<!-- プログレッシブ表示 -->
<div class="content-wrapper">
<!-- 即時表示セクション -->
<header>{data.header}</header>
<!-- メインコンテンツ(遅延ロード) -->
{#await data.content}
<ContentSkeleton />
{:then content}
<main>{content}</main>
{trackLoadedSection('main')}
{/await}
<!-- 補足情報(さらに遅延) -->
{#if loadedSections.has('main')}
{#await data.recommendations}
<RecommendationsSkeleton />
{:then recommendations}
<aside>{recommendations}</aside>
{/await}
{/if}
</div>
最適化のベストプラクティス:
- ロード優先順位の設定
- クリティカルパスの特定
- ユーザー体験への影響評価
- リソース使用の最適化
- エラーハンドリング
// エラー処理を含むロード関数
export async function load() {
return {
critical: await fetchCriticalData().catch(error => ({
error: 'Failed to load critical data',
details: error.message
})),
nonEssential: fetchNonEssentialData()
.catch(error => ({
error: 'Additional content unavailable',
retry: true
}))
};
}
- パフォーマンスモニタリング
// データロードのパフォーマンス測定
function measureDataLoad(promise, name) {
const start = performance.now();
return promise.finally(() => {
const duration = performance.now() - start;
console.log(`Loading ${name} took ${duration}ms`);
});
}
export async function load() {
return {
critical: await measureDataLoad(
fetchCriticalData(),
'critical data'
),
nonEssential: measureDataLoad(
fetchNonEssentialData(),
'non-essential data'
)
};
}
これらの最適化テクニックを適切に実装することで、重要なコンテンツを即座に表示しながら、補足的なデータを効率的に読み込むことができ、全体的なユーザー体験を向上させることができます。
ウォーターフォールの防止
パフォーマンスに最も悪影響を与えるものの1つは、ウォーターフォールと呼ばれる連続的に行われる一連のリクエストです。これはサーバーでもブラウザでも発生する可能性があります。
* アセットのウォーターフォールは、HTMLがJSを要求し、JSがCSSを要求し、CSSが背景画像やWebフォントを要求するような場合にブラウザで発生することがあります。SvelteKitは`modulepreload`タグやヘッダーを追加することでこの種の問題の大部分を解決しますが、追加のリソースをプリロードする必要があるかどうかを確認するため、devtoolsのネットワークタブを確認する必要があります。手動で処理する必要があるWebフォントを使用する場合は、特に注意を払ってください。
* ユニバーサル`load`関数が現在のユーザーを取得するためのAPIコールを行い、そのレスポンスの詳細を使用して保存されたアイテムのリストを取得し、さらにそのレスポンスを使用して各アイテムの詳細を取得する場合、ブラウザは複数の連続的なリクエストを行うことになります。これはパフォーマンスに致命的で、特にバックエンドから物理的に遠い場所にいるユーザーにとって深刻です。可能な限りサーバー`load`関数を使用することで、この問題を回避してください。
* サーバー`load`関数もウォーターフォールの影響を受けないわけではありません(ただし、高遅延を伴うラウンドトリップが少ないため、影響は大幅に小さくなります)。例えば、現在のユーザーを取得するためにデータベースにクエリを行い、そのデータを使用して保存されたアイテムのリストのための2回目のクエリを行う場合、通常はデータベースの結合を使用して単一のクエリを発行する方が効率的です。
- アセットウォーターフォールの最適化
<!-- プリロードの実装例 -->
<head>
<!-- フォントのプリロード -->
<link
rel="preload"
href="/fonts/my-font.woff2"
as="font"
type="font/woff2"
crossorigin
>
<!-- 重要なCSSのプリロード -->
<link
rel="preload"
href="/styles/critical.css"
as="style"
>
<!-- 重要なスクリプトのプリロード -->
<link
rel="modulepreload"
href="/scripts/critical.js"
>
</head>
- ユニバーサル
load
関数の最適化
// 悪い例(ウォーターフォール)
// routes/+page.js
export async function load({ fetch }) {
// 連続的なリクエスト
const userResponse = await fetch('/api/user');
const user = await userResponse.json();
const itemsResponse = await fetch(`/api/items/${user.id}`);
const items = await itemsResponse.json();
const detailsPromises = items.map(item =>
fetch(`/api/item/${item.id}`).then(r => r.json())
);
const details = await Promise.all(detailsPromises);
return { user, items, details };
}
// 良い例(サーバー側で最適化)
// routes/+page.server.js
export async function load({ locals }) {
// 単一のデータベースクエリで全データを取得
const data = await db.query(`
SELECT
users.*,
items.*,
item_details.*
FROM users
LEFT JOIN items ON items.user_id = users.id
LEFT JOIN item_details ON item_details.item_id = items.id
WHERE users.id = $1
`, [locals.userId]);
return { data };
}
- サーバーサイドクエリの最適化
// 悪い例(複数のクエリ)
async function getDataWithWaterfall() {
const user = await db.users.findUnique({
where: { id: userId }
});
const items = await db.items.findMany({
where: { userId: user.id }
});
return { user, items };
}
// 良い例(結合クエリ)
async function getDataOptimized() {
return await db.users.findUnique({
where: { id: userId },
include: {
items: true // 関連データを一括取得
}
});
}
実装のベストプラクティス:
- リソースのプリロード戦略
- クリティカルパスの特定
- 適切なプリロードの実装
- リソースの優先順位付け
- データ取得の最適化
- バッチ処理の活用
- 並列リクエストの実装
- キャッシュの活用
- パフォーマンスモニタリング
- リクエストチェーンの分析
- レイテンシーの測定
- ボトルネックの特定
これらの最適化を適切に実装することで、アプリケーションの応答性を大幅に向上させ、特に高レイテンシー環境下でのユーザー体験を改善することができます。
おわりに
今日は、 SvelteKitでのパフォーマンスについて解説しました。
何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント