inertia-rails-performance について
Inertia Rails アプリケーションを最適化する開発者向けのガイドです。データ読み込み、ページ遷移、キャッシュ、データベースクエリ、性能監視を扱います。
主な機能
- 01Propsの最小化と遅延読み込み
- 02部分的な再読み込み
- 03ページコードの分割と先読み
- 04データベースクエリの最適化
- 05レスポンスと計算結果のキャッシュ
- 06リクエストと遷移時間の監視
活用シーン
- 01初期ページデータ量の削減
- 02描画後の副次データ読み込み
- 03指定したページPropsのみ更新
- 04遅いリクエストや遷移の特定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ cole-robertson/ inertia-rails-skills --skill inertia-rails-performanceインストール後、Claude Code で/inertia-rails-performanceを入力して読み込まれたか確認します。
詳細
Inertia Rails のパフォーマンス最適化
Inertia Rails アプリケーションの速度と効率を最適化するための包括的なガイドです。
Props の最適化
必要最小限のデータを返す
効果: CRITICAL - ペイロードサイズを削減し、セキュリティを向上させます
# Bad - sends entire model
render inertia: { users: User.all }
# Good - only required fields
render inertia: {
users: User.all.as_json(only: [:id, :name, :email, :avatar_url])
}
# Better - use select to avoid loading unnecessary columns
render inertia: {
users: User.select(:id, :name, :email, :avatar_url).as_json
}
ラムダによる遅延評価
効果: HIGH - 不要なクエリを防ぎます
# Bad - evaluates even if not used
inertia_share do
{
recent_posts: Post.recent.limit(5).as_json,
total_users: User.count
}
end
# Good - only evaluates when accessed
inertia_share do
{
recent_posts: -> { Post.recent.limit(5).as_json },
total_users: -> { User.count }
}
end
Deferred Props
重要度の低いデータは、ページの初回レンダリング後に読み込みます。
def dashboard
render inertia: {
# Critical data - loads immediately
user: current_user.as_json(only: [:id, :name]),
# Non-critical - loads after page renders
analytics: InertiaRails.defer { Analytics.for_user(current_user) },
# Group related deferred props (fetched in parallel)
recommendations: InertiaRails.defer(group: 'suggestions') {
Recommendations.for(current_user)
},
trending: InertiaRails.defer(group: 'suggestions') {
Post.trending.limit(10).as_json
},
# Separate group - fetched in parallel with 'suggestions'
notifications: InertiaRails.defer(group: 'alerts') {
current_user.notifications.unread.as_json
}
}
end
フロントエンドでの処理
import { Deferred } from '@inertiajs/react'
export default function Dashboard({ user, analytics, recommendations, trending }) {
return (
<div>
{/* Immediate render */}
<h1>Welcome, {user.name}</h1>
{/* Shows loading state then content */}
<Deferred data="analytics" fallback={<AnalyticsSkeleton />}>
<AnalyticsChart data={analytics} />
</Deferred>
{/* Multiple deferred props */}
<Deferred data={['recommendations', 'trending']} fallback={<RecommendationsSkeleton />}>
<RecommendationsList items={recommendations} />
<TrendingList items={trending} />
</Deferred>
</div>
)
}
部分的な再読み込み
ページ全体を再読み込みせず、特定の props のみを更新します。
import { router } from '@inertiajs/react'
// Reload only 'users' prop
router.reload({ only: ['users'] })
// Exclude specific props
router.reload({ except: ['analytics'] })
// With data parameters
router.reload({
only: ['users'],
data: { search: 'john', page: 2 }
})
サーバーサイドの最適化
def index
render inertia: {
# Standard prop - always included
users: User.search(params[:search]).page(params[:page]).as_json,
# Optional prop - only when explicitly requested
statistics: InertiaRails.optional { compute_statistics },
# Always prop - included even in partial reloads
csrf_token: InertiaRails.always { form_authenticity_token }
}
end
部分的な再読み込みを行うリンク
<Link href="/users" only={['users']}>
Refresh Users
</Link>
<Link href="/users?search=john" only={['users']} preserveState>
Search John
</Link>
コード分割
バンドルを分割して、ページを必要に応じて読み込みます。
Vite(推奨)
// Lazy loading - loads pages on demand
const pages = import.meta.glob('../pages/**/*.tsx')
createInertiaApp({
resolve: (name) => {
return pages[`../pages/${name}.tsx`]() // Note: returns Promise
},
// ...
})
Eager Loading(小規模なアプリ向け)
// All pages in initial bundle - faster for small apps
const pages = import.meta.glob('../pages/**/*.tsx', { eager: true })
createInertiaApp({
resolve: (name) => pages[`../pages/${name}.tsx`],
// ...
})
ハイブリッド方式
// Eager load critical pages, lazy load others
const criticalPages = import.meta.glob([
'../pages/Home.tsx',
'../pages/Dashboard.tsx',
], { eager: true })
const otherPages = import.meta.glob([
'../pages/**/*.tsx',
'!../pages/Home.tsx',
'!../pages/Dashboard.tsx',
])
createInertiaApp({
resolve: (name) => {
const page = criticalPages[`../pages/${name}.tsx`]
if (page) return page
return otherPages[`../pages/${name}.tsx`]()
},
})
プリフェッチ
ユーザーがページに移動する前に読み込みます。
リンクのプリフェッチ
{/* Prefetch on hover (default: 75ms delay) */}
<Link href="/users" prefetch>Users</Link>
{/* Prefetch immediately on mount */}
<Link href="/dashboard" prefetch="mount">Dashboard</Link>
{/* Prefetch on mousedown */}
<Link href="/reports" prefetch="click">Reports</Link>
{/* Multiple strategies */}
<Link href="/settings" prefetch={['mount', 'hover']}>Settings</Link>
キャッシュの設定
{/* Cache for 1 minute */}
<Link href="/users" prefetch cacheFor="1m">Users</Link>
{/* Cache for 30 seconds, stale for 1 minute (stale-while-revalidate) */}
<Link href="/users" prefetch cacheFor={['30s', '1m']}>Users</Link>
プログラムによるプリフェッチ
import { router } from '@inertiajs/react'
// Prefetch a page
router.prefetch('/users')
// With options
router.prefetch('/users', {
method: 'get',
data: { page: 2 }
}, {
cacheFor: '1m'
})
無効化用のキャッシュタグ
<Link href="/users" prefetch cacheTags="users">Users</Link>
<Link href="/users/active" prefetch cacheTags="users">Active Users</Link>
{/* Form that invalidates user cache */}
<Form action="/users" method="post" invalidateCacheTags="users">
{/* ... */}
</Form>
// Manual invalidation
router.flushByCacheTags('users')
// Flush all prefetch cache
router.flushAll()
Instant Visits(v3)
サーバーへのリクエスト中に、共有 props を使って遷移先のページをすぐに表示します。レスポンスが到着すると、ページが完全な props で更新されます。
Instant Visit を使うリンク
{/* Specify the target component to render immediately */}
<Link href="/dashboard" component="Dashboard">
Dashboard
</Link>
{/* With intermediate page props */}
<Link
href="/users/123"
component="Users/Show"
pageProps={{ user: { name: 'Loading...' } }}
>
View User
</Link>
プログラムによる Instant Visit
router.visit('/dashboard', {
component: 'Dashboard',
pageProps: { title: 'Loading dashboard...' }
})
サーバーサイドの設定
expose_shared_prop_keys(デフォルト:true)を有効にすると、Instant Visit で利用できる共有 props をクライアントが認識できます。
InertiaRails.configure do |config|
config.expose_shared_prop_keys = true
end
ページ間で inertia_share を通じてデータを共有すると、Instant Visit を最も効果的に利用できます。共有 props はすぐに利用でき、ページ固有の props はサーバーのレスポンス時に読み込まれます。
無限スクロール
InertiaRails.scroll() を使ったサーバーサイドの処理
InertiaRails.scroll() メソッドはページネーション gem(Pagy、Kaminari)と連携し、シームレスな無限スクロールを実現します。
# Using Pagy (recommended)
def index
pagy, posts = pagy(Post.order(created_at: :desc), limit: 20)
render inertia: {
posts: InertiaRails.scroll(pagy) {
posts.as_json(only: [:id, :title, :excerpt, :created_at])
}
}
end
# Using Kaminari
def index
posts = Post.order(created_at: :desc).page(params[:page]).per(20)
render inertia: {
posts: InertiaRails.scroll(posts) {
posts.as_json(only: [:id, :title, :excerpt])
}
}
end
InfiniteScroll コンポーネントを使ったフロントエンドの処理(v3)
import { InfiniteScroll } from '@inertiajs/react'
export default function PostsIndex({ posts }) {
return (
<InfiniteScroll data={posts} component="posts">
{({ items }) => items.map(post => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</div>
))}
</InfiniteScroll>
)
}
InfiniteScroll のオプション
{/* Bidirectional scrolling */}
<InfiniteScroll data={messages} component="messages" direction="both" />
{/* Reverse scrolling (chat-like) */}
<InfiniteScroll data={messages} component="messages" direction="reverse" />
{/* Manual mode (click to load) */}
<InfiniteScroll data={posts} component="posts" manual
trigger={<button>Load More</button>}
/>
{/* URL synchronization */}
<InfiniteScroll data={posts} component="posts" preserveUrl />
Merge Props を使った手動の無限スクロール
InfiniteScroll コンポーネントを使わずにカスタム実装する場合:
def index
posts = Post.order(created_at: :desc).page(params[:page]).per(20)
render inertia: {
posts: InertiaRails.merge { posts.as_json(only: [:id, :title, :excerpt]) },
pagination: {
current_page: posts.current_page,
total_pages: posts.total_pages,
has_more: !posts.last_page?
}
}
end
import { router } from '@inertiajs/react'
import { useState } from 'react'
export default function PostsIndex({ posts, pagination }) {
const [loading, setLoading] = useState(false)
function loadMore() {
if (loading || !pagination.has_more) return
setLoading(true)
router.reload({
data: { page: pagination.current_page + 1 },
only: ['posts', 'pagination'],
preserveScroll: true,
preserveState: true,
onFinish: () => setLoading(false),
})
}
return (
<div>
{posts.map(post => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</div>
))}
{pagination.has_more && (
<button onClick={loadMore} disabled={loading}>
{loading ? 'Loading...' : 'Load More'}
</button>
)}
</div>
)
}
Merge のオプション
# Append to array (default)
InertiaRails.merge { items }
# Deep merge objects with item matching
InertiaRails.deep_merge { updated_items }
InertiaRails.deep_merge(match_on: 'id') { items }
ポーリング
WebSocket を使わずにリアルタイムで更新します。
import { usePoll } from '@inertiajs/react'
export default function Notifications({ notifications }) {
// Poll every 5 seconds
usePoll(5000)
// With options
usePoll(5000, {
only: ['notifications', 'messages'],
onStart: () => console.log('Polling...'),
onFinish: () => console.log('Poll complete'),
})
// Manual control
const { start, stop } = usePoll(5000, {}, { autoStart: false })
return (
<div>
{notifications.map(n => <div key={n.id}>{n.message}</div>)}
</div>
)
}
バックグラウンドでのスロットリング
// Default: 90% throttle in background tabs
usePoll(5000)
// Keep polling at full speed in background
usePoll(5000, {}, { keepAlive: true })
進行状況インジケーター
デフォルトの NProgress
createInertiaApp({
progress: {
delay: 250, // Show after 250ms (skip quick loads)
color: '#29d', // Progress bar color
includeCSS: true, // Include default styles
showProgress: true // Show percentage
},
})
特定のリクエストでは無効にする
router.visit('/quick-action', {
showProgress: false
})
非同期リクエスト
// Background request without progress indicator
router.post('/analytics/track', { event: 'view' }, {
async: true,
showProgress: false
})
// Async with progress
router.post('/upload', formData, {
async: true,
showProgress: true
})
Once Props
一度だけ解決され、ページ遷移をまたいで記憶されるデータです。
inertia_share do
{
# Evaluated once per session, not on every navigation
app_config: InertiaRails.once { AppConfig.to_json },
feature_flags: InertiaRails.once { FeatureFlags.current }
}
end
Optional/Deferred と組み合わせる場合:
render inertia: {
# Optional + once: resolved only when requested, then remembered
user_preferences: InertiaRails.optional(once: true) {
current_user.preferences.as_json
}
}
Once Props の設定
render inertia: {
# Expire after a time period
app_config: InertiaRails.once(expires_in: 1.day) { AppConfig.to_json },
# Force refresh based on a condition
feature_flags: InertiaRails.once(fresh: current_user.updated_at > 1.hour.ago) {
FeatureFlags.for_user(current_user)
},
# Share cache key across pages (any page can refresh this data)
notifications_count: InertiaRails.once(key: 'notifications') {
current_user.unread_notifications_count
}
}
アセットのバージョン管理
デプロイ後にユーザーが最新のアセットを取得できるようにします。
# config/initializers/inertia_rails.rb
InertiaRails.configure do |config|
# Using ViteRuby digest
config.version = -> { ViteRuby.digest }
# Or custom version
config.version = -> { ENV['ASSET_VERSION'] || Rails.application.config.assets_version }
end
バージョンが変わると、Inertia は XHR の代わりにページ全体を再読み込みします。
データベースクエリの最適化
Eager Loading
def index
# Bad - N+1 queries
users = User.all
render inertia: {
users: users.map { |u| u.as_json(include: :posts) }
}
# Good - eager load
users = User.includes(:posts)
render inertia: {
users: users.as_json(include: { posts: { only: [:id, :title] } })
}
end
選択的な読み込み
def index
# Only select needed columns
users = User
.select(:id, :name, :email, :created_at)
.includes(:profile)
.order(created_at: :desc)
.limit(50)
render inertia: {
users: users.as_json(
only: [:id, :name, :email],
include: { profile: { only: [:avatar_url] } }
)
}
end
キャッシュ戦略
フラグメントキャッシュ
def index
render inertia: {
stats: Rails.cache.fetch('dashboard_stats', expires_in: 5.minutes) do
compute_expensive_stats
end
}
end
ETag によるレスポンスキャッシュ
def show
user = User.find(params[:id])
if stale?(user)
render inertia: { user: user.as_json(only: [:id, :name]) }
end
end
パフォーマンスのモニタリング
遅いリクエストを追跡する
# app/controllers/application_controller.rb
around_action :track_request_time
private
def track_request_time
start = Time.current
yield
duration = Time.current - start
if duration > 1.second
Rails.logger.warn "Slow request: #{request.path} took #{duration.round(2)}s"
end
end
クライアントサイドのメトリクス
router.on('start', (event) => {
event.detail.visit.startTime = performance.now()
})
router.on('finish', (event) => {
const duration = performance.now() - event.detail.visit.startTime
if (duration > 1000) {
console.warn(`Slow navigation to ${event.detail.visit.url}: ${duration}ms`)
}
})
WhenVisible - ビューポートに入ったときに遅延読み込み
Intersection Observer を使って、要素が表示されたときにのみデータを読み込みます。
基本的な使い方
import { WhenVisible } from '@inertiajs/react'
export default function Dashboard({ users, teams }) {
return (
<div>
{/* Main content loads immediately */}
<UserList users={users} />
{/* Teams load when scrolled into view */}
<WhenVisible data="teams" fallback={<TeamsSkeleton />}>
<TeamList teams={teams} />
</WhenVisible>
</div>
)
}
複数の Props
<WhenVisible data={['teams', 'projects']} fallback={<LoadingSpinner />}>
<Dashboard teams={teams} projects={projects} />
</WhenVisible>
設定オプション
{/* Start loading 500px before element is visible */}
<WhenVisible data="comments" buffer={500}>
<Comments comments={comments} />
</WhenVisible>
{/* Custom wrapper element */}
<WhenVisible data="stats" as="section">
<Stats stats={stats} />
</WhenVisible>
{/* Reload every time element becomes visible */}
<WhenVisible data="posts" always>
<PostList posts={posts} />
</WhenVisible>
フォーム送信と併用する
フォーム送信後に WhenVisible の props が再読み込みされないようにします。
form.post('/comments', {
except: ['teams'], // Don't reload teams managed by WhenVisible
})
スクロールの管理
スクロール位置の保持
// Always preserve scroll position
router.visit('/users', { preserveScroll: true })
// Preserve only on validation errors
router.visit('/users', { preserveScroll: 'errors' })
// Conditional preservation
router.visit('/users', {
preserveScroll: (page) => page.props.shouldPreserve
})
スクロール制御付きリンク
<Link href="/users" preserveScroll>Users</Link>
スクロール領域
ドキュメントの body 以外のスクロール可能なコンテナー向け:
export default function AppLayout({ children }) {
return (
<div className="h-screen flex">
{/* Sidebar with independent scroll */}
<nav className="w-64 overflow-y-auto" scroll-region>
<SidebarContent />
</nav>
{/* Main content with independent scroll */}
<main className="flex-1 overflow-y-auto" scroll-region>
{children}
</main>
</div>
)
}
Inertia は scroll-region 属性を持つ要素のスクロール位置を追跡し、復元します。
プログラムによるスクロール位置のリセット
router.visit('/users', {
preserveScroll: false, // Reset to top (default)
})
View Transitions(v3)
View Transitions API を使って、ページ間を滑らかなアニメーションで遷移させます。
import { router } from '@inertiajs/react'
// Enable view transitions globally
router.on('before', (event) => {
event.detail.visit.viewTransition = true
})
{/* Per-link view transitions */}
<Link href="/users" viewTransition>Users</Link>
/* Define transition animations */
::view-transition-old(root) {
animation: slide-out 0.3s ease-in-out;
}
::view-transition-new(root) {
animation: slide-in 0.3s ease-in-out;
}
ベストプラクティスのまとめ
- Props:必要なデータのみを返し、遅延評価を使用します
- Deferred Props:重要度の低いデータは遅延読み込みに回します
- Partial Reloads:変更されたデータのみを更新します
- Code Splitting:大規模なアプリケーションでは、ページを遅延読み込みします
- Prefetching:次に表示される可能性の高いページを事前に読み込みます
- Infinite Scroll:シームレスなページネーションには、merge props を使用します
- Polling:適切なスロットリングを設定し、必要最小限に使用します
- Database:関連データを事前読み込みし、必要な列のみを選択します
- Caching:コストの高い計算結果をキャッシュします
- Monitoring:遅いリクエストを追跡し、最適化します
- WhenVisible:スクロールしないと見えないコンテンツを遅延読み込みします
- Scroll Regions:複数のスクロール領域がある複雑なレイアウトで使用します
- Instant Visits:既知のページへのナビゲーションを即座に感じられるようにするために使用します
- View Transitions:ページ遷移の間に滑らかなアニメーションを追加します