mcpField
I
inertia-rails-performance

inertia-rails-performance

github.com/cole-robertson开发3892 次安装30 周前更新
代码仓库

关于 inertia-rails-performance

面向 Inertia Rails 开发者的性能优化指南,涵盖数据加载、页面导航、缓存、数据库查询和性能监控。

主要功能

  1. 01精简并延迟加载 Props
  2. 02使用局部重新加载
  3. 03拆分并预取页面代码
  4. 04优化数据库查询
  5. 05缓存响应与计算结果
  6. 06监控请求和导航耗时

适用场景

  1. 01缩减页面初始数据量
  2. 02在渲染后加载次要数据
  3. 03仅刷新指定页面属性
  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 优化

返回最少数据

影响: 严重 — 减小负载大小,提升安全性

# 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
}
使用 Lambda 延迟求值

影响: 高 — 避免不必要的查询

# 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

延迟 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
  },
  // ...
})
急切加载(小型应用)
// 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()

即时访问(v3)

在服务器请求进行期间,使用共享 props 立即渲染目标页面。收到响应后,页面会更新为完整的 props。

使用即时访问的链接
{/* 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>
以编程方式即时访问
router.visit('/dashboard', {
  component: 'Dashboard',
  pageProps: { title: 'Loading dashboard...' }
})
服务端配置

启用 expose_shared_prop_keys(默认值:true),以便客户端知道即时访问可用的共享 props:

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

当页面通过 inertia_share 共享数据时,即时访问效果最佳——共享 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 }

轮询

无需 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 (
    <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

与可选/延迟方式结合:

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。

数据库查询优化

预加载
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
使用 ETags 缓存响应
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>
滚动区域

适用于可滚动容器(而非文档主体):

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

视图过渡(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:在页面切换之间添加流畅的动画

评论

支持 Markdown