关于 inertia-rails-performance
面向 Inertia Rails 开发者的性能优化指南,涵盖数据加载、页面导航、缓存、数据库查询和性能监控。
主要功能
- 01精简并延迟加载 Props
- 02使用局部重新加载
- 03拆分并预取页面代码
- 04优化数据库查询
- 05缓存响应与计算结果
- 06监控请求和导航耗时
适用场景
- 01缩减页面初始数据量
- 02在渲染后加载次要数据
- 03仅刷新指定页面属性
- 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;
}
最佳实践总结
- Props:仅返回必要的数据,并使用惰性求值
- Deferred Props:将非关键数据改为延迟加载
- Partial Reloads:仅刷新发生变化的数据
- Code Splitting:为大型应用延迟加载页面
- Prefetching:预加载可能访问的下一页面
- Infinite Scroll:使用 merge props 实现无缝分页
- Polling:谨慎使用,并做好节流
- Database:预加载关联数据,并仅选择所需列
- Caching:缓存开销较大的计算结果
- Monitoring:跟踪并优化缓慢的请求
- WhenVisible:延迟加载首屏以下的内容
- Scroll Regions:在包含多个滚动区域的复杂布局中使用
- Instant Visits:用于实现对已知页面的近乎即时导航
- View Transitions:在页面切换之间添加流畅的动画