mcpField
I
inertia-rails-performance

inertia-rails-performance

github.com/cole-robertson開発3892 回インストール30 週間前に更新
リポジトリ

inertia-rails-performance について

Inertia Rails アプリケーションを最適化する開発者向けのガイドです。データ読み込み、ページ遷移、キャッシュ、データベースクエリ、性能監視を扱います。

主な機能

  1. 01Propsの最小化と遅延読み込み
  2. 02部分的な再読み込み
  3. 03ページコードの分割と先読み
  4. 04データベースクエリの最適化
  5. 05レスポンスと計算結果のキャッシュ
  6. 06リクエストと遷移時間の監視

活用シーン

  1. 01初期ページデータ量の削減
  2. 02描画後の副次データ読み込み
  3. 03指定したページPropsのみ更新
  4. 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;
}

ベストプラクティスのまとめ

  1. Props:必要なデータのみを返し、遅延評価を使用します
  2. Deferred Props:重要度の低いデータは遅延読み込みに回します
  3. Partial Reloads:変更されたデータのみを更新します
  4. Code Splitting:大規模なアプリケーションでは、ページを遅延読み込みします
  5. Prefetching:次に表示される可能性の高いページを事前に読み込みます
  6. Infinite Scroll:シームレスなページネーションには、merge props を使用します
  7. Polling:適切なスロットリングを設定し、必要最小限に使用します
  8. Database:関連データを事前読み込みし、必要な列のみを選択します
  9. Caching:コストの高い計算結果をキャッシュします
  10. Monitoring:遅いリクエストを追跡し、最適化します
  11. WhenVisible:スクロールしないと見えないコンテンツを遅延読み込みします
  12. Scroll Regions:複数のスクロール領域がある複雑なレイアウトで使用します
  13. Instant Visits:既知のページへのナビゲーションを即座に感じられるようにするために使用します
  14. View Transitions:ページ遷移の間に滑らかなアニメーションを追加します

コメント