# Inertia Rails Performance

> Optimize Inertia Rails application performance. Use when implementing code splitting, prefetching, deferred props, infinite scrolling, polling, or other performance optimizations.

- **Type:** Skill
- **Install:** `agentstack add skill-cole-robertson-inertia-rails-skills-inertia-rails-performance`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [cole-robertson](https://agentstack.voostack.com/s/cole-robertson)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [cole-robertson](https://github.com/cole-robertson)
- **Source:** https://github.com/cole-robertson/inertia-rails-skills/tree/main/skills/inertia-rails-performance

## Install

```sh
agentstack add skill-cole-robertson-inertia-rails-skills-inertia-rails-performance
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Inertia Rails Performance Optimization

Comprehensive guide to optimizing Inertia Rails applications for speed and efficiency.

## Props Optimization

### Return Minimal Data

**Impact:** CRITICAL - Reduces payload size, improves security

```ruby
# 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
}
```

### Lazy Evaluation with Lambdas

**Impact:** HIGH - Prevents unnecessary queries

```ruby
# 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

Load non-critical data after initial page render:

```ruby
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
```

### Frontend Handling

```jsx
import { Deferred } from '@inertiajs/react'

export default function Dashboard({ user, analytics, recommendations, trending }) {
  return (
    
      {/* Immediate render */}
      Welcome, {user.name}

      {/* Shows loading state then content */}
      }>
        
      

      {/* Multiple deferred props */}
      }>
        
        
      
    
  )
}
```

## Partial Reloads

Refresh only specific props without full page reload:

```javascript
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 }
})
```

### Server-Side Optimization

```ruby
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 with Partial Reload

```jsx

  Refresh Users

  Search John

```

## Code Splitting

Split your bundle to load pages on demand:

### Vite (Recommended)

```javascript
// 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 (Small Apps)

```javascript
// 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`],
  // ...
})
```

### Hybrid Approach

```javascript
// 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`]()
  },
})
```

## Prefetching

Load pages before user navigates:

### Link Prefetching

```jsx
{/* Prefetch on hover (default: 75ms delay) */}
Users

{/* Prefetch immediately on mount */}
Dashboard

{/* Prefetch on mousedown */}
Reports

{/* Multiple strategies */}
Settings
```

### Cache Configuration

```jsx
{/* Cache for 1 minute */}
Users

{/* Cache for 30 seconds, stale for 1 minute (stale-while-revalidate) */}
Users
```

### Programmatic Prefetching

```javascript
import { router } from '@inertiajs/react'

// Prefetch a page
router.prefetch('/users')

// With options
router.prefetch('/users', {
  method: 'get',
  data: { page: 2 }
}, {
  cacheFor: '1m'
})
```

### Cache Tags for Invalidation

```jsx
Users
Active Users

{/* Form that invalidates user cache */}

  {/* ... */}

```

```javascript
// Manual invalidation
router.flushByCacheTags('users')

// Flush all prefetch cache
router.flushAll()
```

## Instant Visits (v3)

Render the target page immediately with shared props while the server request is in flight. When the response arrives, the page is updated with the full props.

### Link with Instant Visit

```jsx
{/* Specify the target component to render immediately */}

  Dashboard

{/* With intermediate page props */}

  View User

```

### Programmatic Instant Visit

```javascript
router.visit('/dashboard', {
  component: 'Dashboard',
  pageProps: { title: 'Loading dashboard...' }
})
```

### Server-Side Configuration

Enable `expose_shared_prop_keys` (default: true) so the client knows which shared props are available for instant visits:

```ruby
InertiaRails.configure do |config|
  config.expose_shared_prop_keys = true
end
```

Instant visits work best when pages share data through `inertia_share` — the shared props are available immediately, and page-specific props load when the server responds.

## Infinite Scrolling

### Server-Side with InertiaRails.scroll()

The `InertiaRails.scroll()` method integrates with pagination gems (Pagy, Kaminari) for seamless infinite scrolling:

```ruby
# 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
```

### Frontend with InfiniteScroll Component (v3)

```jsx
import { InfiniteScroll } from '@inertiajs/react'

export default function PostsIndex({ posts }) {
  return (
    
      {({ items }) => items.map(post => (
        
          {post.title}
          {post.excerpt}
        
      ))}
    
  )
}
```

### InfiniteScroll Options

```jsx
{/* Bidirectional scrolling */}

{/* Reverse scrolling (chat-like) */}

{/* Manual mode (click to load) */}
Load More}
/>

{/* URL synchronization */}

```

### Manual Infinite Scrolling with Merge Props

For custom implementations without the InfiniteScroll component:

```ruby
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
```

```jsx
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 (
    
      {posts.map(post => (
        
          {post.title}
          {post.excerpt}
        
      ))}
      {pagination.has_more && (
        
          {loading ? 'Loading...' : 'Load More'}
        
      )}
    
  )
}
```

### Merge Options

```ruby
# 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 }
```

## Polling

Real-time updates without WebSockets:

```jsx
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 (
    
      {notifications.map(n => {n.message})}
    
  )
}
```

### Throttling in Background

```javascript
// Default: 90% throttle in background tabs
usePoll(5000)

// Keep polling at full speed in background
usePoll(5000, {}, { keepAlive: true })
```

## Progress Indicators

### Default NProgress

```javascript
createInertiaApp({
  progress: {
    delay: 250,        // Show after 250ms (skip quick loads)
    color: '#29d',     // Progress bar color
    includeCSS: true,  // Include default styles
    showProgress: true // Show percentage
  },
})
```

### Disable for Specific Requests

```javascript
router.visit('/quick-action', {
  showProgress: false
})
```

### Async Requests

```javascript
// 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

Data resolved once and remembered across navigations:

```ruby
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
```

Combined with optional/deferred:

```ruby
render inertia: {
  # Optional + once: resolved only when requested, then remembered
  user_preferences: InertiaRails.optional(once: true) {
    current_user.preferences.as_json
  }
}
```

### Once Props Configuration

```ruby
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
  }
}
```

## Asset Versioning

Ensure users get fresh assets after deployment:

```ruby
# 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
```

When version changes, Inertia triggers a full page reload instead of XHR.

## Database Query Optimization

### Eager Loading

```ruby
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
```

### Selective Loading

```ruby
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
```

## Caching Strategies

### Fragment Caching

```ruby
def index
  render inertia: {
    stats: Rails.cache.fetch('dashboard_stats', expires_in: 5.minutes) do
      compute_expensive_stats
    end
  }
end
```

### Response Caching with ETags

```ruby
def show
  user = User.find(params[:id])

  if stale?(user)
    render inertia: { user: user.as_json(only: [:id, :name]) }
  end
end
```

## Performance Monitoring

### Track Slow Requests

```ruby
# 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
```

### Client-Side Metrics

```javascript
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 - Lazy Load on Viewport Entry

Load data only when elements become visible using Intersection Observer:

### Basic Usage

```jsx
import { WhenVisible } from '@inertiajs/react'

export default function Dashboard({ users, teams }) {
  return (
    
      {/* Main content loads immediately */}
      

      {/* Teams load when scrolled into view */}
      }>
        
      
    
  )
}
```

### Multiple Props

```jsx
}>
  

```

### Configuration Options

```jsx
{/* Start loading 500px before element is visible */}

  

{/* Custom wrapper element */}

  

{/* Reload every time element becomes visible */}

  

```

### With Form Submissions

Prevent reloading WhenVisible props after form submission:

```javascript
form.post('/comments', {
  except: ['teams'],  // Don't reload teams managed by WhenVisible
})
```

## Scroll Management

### Scroll Preservation

```javascript
// 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 with Scroll Control

```jsx
Users
```

### Scroll Regions

For scrollable containers (not document body):

```jsx
export default function AppLayout({ children }) {
  return (
    
      {/* Sidebar with independent scroll */}
      
        
      

      {/* Main content with independent scroll */}
      
        {children}
      
    
  )
}
```

Inertia tracks and restores scroll position for elements with `scroll-region` attribute.

### Reset Scroll Programmatically

```javascript
router.visit('/users', {
  preserveScroll: false,  // Reset to top (default)
})
```

## View Transitions (v3)

Use the View Transitions API for smooth animated transitions between pages:

```javascript
import { router } from '@inertiajs/react'

// Enable view transitions globally
router.on('before', (event) => {
  event.detail.visit.viewTransition = true
})
```

```jsx
{/* Per-link view transitions */}
Users
```

```css
/* 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;
}
```

## Best Practices Summary

1. **Props**: Return only necessary data, use lazy evaluation
2. **Deferred Props**: Move non-critical data to deferred loading
3. **Partial Reloads**: Refresh only changed data
4. **Code Splitting**: Lazy load pages for large applications
5. **Prefetching**: Preload likely next pages
6. **Infinite Scroll**: Use merge props for seamless pagination
7. **Polling**: Use sparingly with proper throttling
8. **Database**: Eager load associations, select only needed columns
9. **Caching**: Cache expensive com

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [cole-robertson](https://github.com/cole-robertson)
- **Source:** [cole-robertson/inertia-rails-skills](https://github.com/cole-robertson/inertia-rails-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-cole-robertson-inertia-rails-skills-inertia-rails-performance
- Seller: https://agentstack.voostack.com/s/cole-robertson
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
