AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Inertia Rails Performance

skill-cole-robertson-inertia-rails-skills-inertia-rails-performance · by cole-robertson

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

No reviews yet
0 installs
3 views
0.0% view→install

Install

$ agentstack add skill-cole-robertson-inertia-rails-skills-inertia-rails-performance

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-cole-robertson-inertia-rails-skills-inertia-rails-performance)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
4mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Inertia Rails Performance? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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

# 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

# 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:

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

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:

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

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


  Refresh Users

  Search John

Code Splitting

Split your bundle to load pages on demand:

Vite (Recommended)

// 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)

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

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

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

{/* Prefetch immediately on mount */}
Dashboard

{/* Prefetch on mousedown */}
Reports

{/* Multiple strategies */}
Settings

Cache Configuration

{/* Cache for 1 minute */}
Users

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

Programmatic Prefetching

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

Users
Active Users

{/* Form that invalidates user cache */}

  {/* ... */}
// 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

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

  Dashboard

{/* With intermediate page props */}

  View User

Programmatic Instant Visit

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:

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:

# 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)

import { InfiniteScroll } from '@inertiajs/react'

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

InfiniteScroll Options

{/* 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:

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

Merge Options

# 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:

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

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

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

Progress Indicators

Default 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
  },
})

Disable for Specific Requests

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

Async Requests

// 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:

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:

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

Once Props Configuration

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:

# 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

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

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

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

Response Caching with ETags

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

# 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

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

import { WhenVisible } from '@inertiajs/react'

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

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

Multiple Props

}>
  

Configuration Options

{/* 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:

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

Scroll Management

Scroll Preservation

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

Users

Scroll Regions

For scrollable containers (not document body):

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

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

View Transitions (v3)

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

import { router } from '@inertiajs/react'

// Enable view transitions globally
router.on('before', (event) => {
  event.detail.visit.viewTransition = true
})
{/* Per-link view transitions */}
Users
/* 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.

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

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.