Skip to content
inertia-rails-performance logo

Inertia Rails Performance Optimization

inertia-rails-performance

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

NeverSight/skills_feed0installs215stars

SKILL.md

Full skill instructions

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

<script setup>
import { Deferred } from '@inertiajs/​vue3'

defineProps(['user', 'analytics', 'recommendations'])
</​script>

<template>
  <div>
    <!-- Immediate render -->
    <h1>Welcome, {{ user.name }}</​h1>

    <!-- Shows loading state then content -->
    <Deferred data="analytics">
      <template #fallback>
        <AnalyticsSkeleton />
      </​template>
      <AnalyticsChart :data="analytics" />
    </​Deferred>

    <!-- Multiple deferred props -->
    <Deferred :data="['recommendations', 'trending']">
      <template #fallback>
        <RecommendationsSkeleton />
      </​template>
      <RecommendationsList :items="recommendations" />
      <TrendingList :items="trending" />
    </​Deferred>
  </​div>
</​template>

Partial Reloads

Refresh only specific props without full page reload:

import { router } from '@inertiajs/​vue3'

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

<Link href="/​users" :only="['users']">
  Refresh Users
</​Link>

<Link href="/​users?search=john" :only="['users']" preserve-state>
  Search John
</​Link>

Code Splitting

Split your bundle to load pages on demand:

Vite (Recommended)

// Lazy loading - loads pages on demand
const pages = import.meta.glob('../​pages/​**/​*.vue')

createInertiaApp({
  resolve: (name) => {
    return pages[`../​pages/​${name}.vue`]()  // Note: returns Promise
  },
  // ...
})

Eager Loading (Small Apps)

// All pages in initial bundle - faster for small apps
const pages = import.meta.glob('../​pages/​**/​*.vue', { eager: true })

createInertiaApp({
  resolve: (name) => pages[`../​pages/​${name}.vue`],
  // ...
})

Hybrid Approach

// Eager load critical pages, lazy load others
const criticalPages = import.meta.glob([
  '../​pages/​Home.vue',
  '../​pages/​Dashboard.vue',
], { eager: true })

const otherPages = import.meta.glob([
  '../​pages/​**/​*.vue',
  '!../​pages/​Home.vue',
  '!../​pages/​Dashboard.vue',
])

createInertiaApp({
  resolve: (name) => {
    const page = criticalPages[`../​pages/​${name}.vue`]
    if (page) return page

    return otherPages[`../​pages/​${name}.vue`]()
  },
})

Prefetching

Load pages before user navigates:

Link Prefetching

<!-- 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 Configuration

<!-- Cache for 1 minute -->
<Link href="/​users" prefetch cache-for="1m">Users</​Link>

<!-- Cache for 30 seconds, stale for 1 minute (stale-while-revalidate) -->
<Link href="/​users" prefetch :cache-for="['30s', '1m']">Users</​Link>

Programmatic Prefetching

import { router } from '@inertiajs/​vue3'

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

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

Cache Tags for Invalidation

<Link href="/​users" prefetch cache-tags="users">Users</​Link>
<Link href="/​users/​active" prefetch cache-tags="users">Active Users</​Link>

<!-- Form that invalidates user cache -->
<Form action="/​users" method="post" invalidate-cache-tags="users">
  <!-- ... -->
</​Form>
// Manual invalidation
router.flushByCacheTags('users')

// Flush all prefetch cache
router.flushAll()

Infinite Scrolling

Load more content without pagination:

Server-Side

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

Client-Side (Vue)

<script setup>
import { router } from '@inertiajs/​vue3'
import { ref, onMounted, onUnmounted } from 'vue'

const props = defineProps(['posts', 'pagination'])
const loading = ref(false)
const sentinel = ref(null)

function loadMore() {
  if (loading.value || !props.pagination.has_more) return

  loading.value = true
  router.reload({
    data: { page: props.pagination.current_page + 1 },
    only: ['posts', 'pagination'],
    preserveScroll: true,
    preserveState: true,
    onFinish: () => (loading.value = false),
  })
}

// Intersection Observer for automatic loading
let observer
onMounted(() => {
  observer = new IntersectionObserver(
    (entries) => {
      if (entries[0].isIntersecting) loadMore()
    },
    { rootMargin: '100px' }
  )
  if (sentinel.value) observer.observe(sentinel.value)
})

onUnmounted(() => observer?.disconnect())
</​script>

<template>
  <div>
    <div v-for="post in posts" :key="post.id" class="post">
      <h2>{{ post.title }}</​h2>
      <p>{{ post.excerpt }}</​p>
    </​div>

    <div ref="sentinel" class="h-4" />

    <div v-if="loading" class="text-center py-4">
      Loading more...
    </​div>

    <div v-if="!pagination.has_more" class="text-center py-4 text-gray-500">
      No more posts
    </​div>
  </​div>
</​template>

Merge Options

# Append to array (default)
InertiaRails.merge { items }

# Prepend to array
InertiaRails.merge(prepend: true) { items }

# Target specific key
InertiaRails.merge(append: 'data') { { data: items, meta: meta } }

# Update matching items instead of duplicating
InertiaRails.merge(match_on: 'id') { items }

Polling

Real-time updates without WebSockets:

<script setup>
import { usePoll } from '@inertiajs/​vue3'

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

// Start polling when component becomes visible
const visible = usePageVisibility()
watch(visible, (isVisible) => {
  isVisible ? start() : stop()
})
</​script>

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

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

<script setup>
import { WhenVisible } from '@inertiajs/​vue3'

defineProps(['users', 'teams'])
</​script>

<template>
  <div>
    <!-- Main content loads immediately -->
    <UserList :users="users" />

    <!-- Teams load when scrolled into view -->
    <WhenVisible data="teams">
      <template #fallback>
        <TeamsSkeleton />
      </​template>
      <TeamList :teams="teams" />
    </​WhenVisible>
  </​div>
</​template>

Multiple Props

<WhenVisible :data="['teams', 'projects']">
  <template #fallback>
    <LoadingSpinner />
  </​template>
  <Dashboard :teams="teams" :projects="projects" />
</​WhenVisible>

Configuration Options

<!-- 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 (for infinite scroll) -->
<WhenVisible data="posts" always>
  <PostList :posts="posts" />
</​WhenVisible>

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

<Link href="/​users" preserve-scroll>Users</​Link>

Scroll Regions

For scrollable containers (not document body):

<template>
  <div class="h-screen flex">
    <!-- Sidebar with independent scroll -->
    <nav class="w-64 overflow-y-auto" scroll-region>
      <SidebarContent />
    </​nav>

    <!-- Main content with independent scroll -->
    <main class="flex-1 overflow-y-auto" scroll-region>
      <slot />
    </​main>
  </​div>
</​template>

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

Reset Scroll Programmatically

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

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 computations
  10. Monitoring: Track and optimize slow requests
  11. WhenVisible: Lazy load below-the-fold content
  12. Scroll Regions: Use for complex layouts with multiple scroll areas

More skills from NeverSight

devops-deployer logo
NeverSight/skills_feed

devops-deployer

Comprehensive DevOps and deployment workflow that orchestrates infrastructure automation, CI/CD pipelines, container orchestration, and cloud deployment. Handles everything from infrastructure as code and pipeline setup to monitoring, scaling, and disaster recovery.

215 1
View
avnu logo
NeverSight/skills_feed

avnu

Integrate avnu SDK for Starknet DeFi: token swaps with best-price routing, DCA recurring buys, native staking, gasless transactions via paymaster, and market data. Use when building Starknet trading, DEX aggregation, or DeFi automation features.

215 0
View
post-to-x logo
NeverSight/skills_feed

post-to-x

发布内容和文章到 X (Twitter)。支持带图片的普通推文和 X Articles(长文)。使用真实的 Chrome 和 CDP 来绕过自动化检测。

215 0
View
ralph-prompt-builder logo
NeverSight/skills_feed

ralph-prompt-builder

Master orchestrator for generating Ralph Wiggum-compatible prompts. Analyzes task requirements and routes to appropriate generator (single-task, multi-task, project, or research). Use when you need to create any Ralph loop prompt and want automatic selection of the right generator.

215 0
View
branch-comparison logo
NeverSight/skills_feed

branch-comparison

Use when the user asks to compare branches, analyze git diffs, review changes between branches, update specifications based on code changes, or analyze what changed. Triggers on keywords like "compare branches", "git diff", "what changed", "branch comparison", "code changes", "spec update".

215 0
View
fastapi-development logo
NeverSight/skills_feed

fastapi-development

Build high-performance FastAPI applications with async routes, validation, dependency injection, security, and automatic API documentation. Use when developing modern Python APIs with async support, automatic OpenAPI documentation, and high performance requirements.

215 0
View
zendesk-chat logo
NeverSight/skills_feed

zendesk-chat

Enables Claude to manage Zendesk Chat conversations, support tickets, and customer service operations

215 0
View
forge logo
NeverSight/skills_feed

forge

Mine transcripts for knowledge - decisions, learnings, failures, patterns. Triggers: "forge insights", "mine transcripts", "extract knowledge".

215 0
View
mixpanel logo
NeverSight/skills_feed

mixpanel

Track product analytics and user behavior with Mixpanel's event-based platform.

215 0
View
ocpp logo
NeverSight/skills_feed

ocpp

OCPP protocol reference for EV charging infrastructure development. Covers OCPP 2.0.1 and OCPP 1.6J. Use when working with OCPP messages, charging station code, CSMS/Central System backends, smart charging, transaction handling, or EV charging protocols. Activates on keywords: OCPP, charging stat...

215 0
View
bee-collect logo
NeverSight/skills_feed

bee-collect

Gather scattered ideas into organized GitHub issues. The bee collects pollen from every flower, checks if the hive already has it, and carefully deposits new work in the comb for later. Never edits code—only organizes the work ahead. Use when creating issues from TODOs or planning work.

215 0
View
vercel-react-native-skills logo
NeverSight/skills_feed

vercel-react-native-skills

Comprehensive best practices for React Native and Expo applications. Contains rules across multiple categories covering performance, animations, UI patterns, and platform-specific optimizations.

215 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View