inertia-rails-performance
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseInertia Rails Performance Optimization
Inertia Rails应用性能优化指南
Comprehensive guide to optimizing Inertia Rails applications for speed and efficiency.
本指南全面介绍如何优化Inertia Rails应用的速度与效率。
Props Optimization
属性优化
Return Minimal Data
返回最小化数据
Impact: CRITICAL - Reduces payload size, improves security
ruby
undefined影响:关键 - 减小负载大小,提升安全性
ruby
undefinedBad - sends entire model
不良示例 - 返回整个模型
render inertia: { users: User.all }
render inertia: { users: User.all }
Good - only required fields
良好示例 - 仅返回所需字段
render inertia: {
users: User.all.as_json(only: [:id, :name, :email, :avatar_url])
}
render inertia: {
users: User.all.as_json(only: [:id, :name, :email, :avatar_url])
}
Better - use select to avoid loading unnecessary columns
更优示例 - 使用select避免加载不必要的列
render inertia: {
users: User.select(:id, :name, :email, :avatar_url).as_json
}
undefinedrender inertia: {
users: User.select(:id, :name, :email, :avatar_url).as_json
}
undefinedLazy Evaluation with Lambdas
使用Lambda进行延迟求值
Impact: HIGH - Prevents unnecessary queries
ruby
undefined影响:高 - 避免不必要的查询
ruby
undefinedBad - evaluates even if not used
不良示例 - 即使未使用也会求值
inertia_share do
{
recent_posts: Post.recent.limit(5).as_json,
total_users: User.count
}
end
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
undefinedinertia_share do
{
recent_posts: -> { Post.recent.limit(5).as_json },
total_users: -> { User.count }
}
end
undefinedDeferred 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在初始页面渲染后加载非关键数据:
ruby
def dashboard
render inertia: {
# 关键数据 - 立即加载
user: current_user.as_json(only: [:id, :name]),
# 非关键数据 - 页面渲染后加载
analytics: InertiaRails.defer { Analytics.for_user(current_user) },
# 分组相关延迟属性(并行获取)
recommendations: InertiaRails.defer(group: 'suggestions') {
Recommendations.for(current_user)
},
trending: InertiaRails.defer(group: 'suggestions') {
Post.trending.limit(10).as_json
},
# 独立分组 - 与'suggestions'并行获取
notifications: InertiaRails.defer(group: 'alerts') {
current_user.notifications.unread.as_json
}
}
endFrontend Handling
前端处理
vue
<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>vue
<script setup>
import { Deferred } from '@inertiajs/vue3'
defineProps(['user', 'analytics', 'recommendations'])
</script>
<template>
<div>
<!-- 立即渲染 -->
<h1>欢迎回来,{{ user.name }}</h1>
<!-- 先显示加载状态,再显示内容 -->
<Deferred data="analytics">
<template #fallback>
<AnalyticsSkeleton />
</template>
<AnalyticsChart :data="analytics" />
</Deferred>
<!-- 多个延迟属性 -->
<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:
javascript
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 }
})无需整页刷新,仅刷新特定属性:
javascript
import { router } from '@inertiajs/vue3'
// 仅重载'users'属性
router.reload({ only: ['users'] })
// 排除特定属性
router.reload({ except: ['analytics'] })
// 携带数据参数
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 }
}
endruby
def index
render inertia: {
// 标准属性 - 始终包含
users: User.search(params[:search]).page(params[:page]).as_json,
// 可选属性 - 仅在显式请求时包含
statistics: InertiaRails.optional { compute_statistics },
// 必选属性 - 即使局部重载也会包含
csrf_token: InertiaRails.always { form_authenticity_token }
}
endLink with Partial Reload
带局部重载的链接
vue
<Link href="/users" :only="['users']">
Refresh Users
</Link>
<Link href="/users?search=john" :only="['users']" preserve-state>
Search John
</Link>vue
<Link href="/users" :only="['users']">
刷新用户列表
</Link>
<Link href="/users?search=john" :only="['users']" preserve-state>
搜索John
</Link>Code Splitting
代码分割
Split your bundle to load pages on demand:
拆分代码包,按需加载页面:
Vite (Recommended)
Vite(推荐)
javascript
// Lazy loading - loads pages on demand
const pages = import.meta.glob('../pages/**/*.vue')
createInertiaApp({
resolve: (name) => {
return pages[`../pages/${name}.vue`]() // Note: returns Promise
},
// ...
})javascript
// 懒加载 - 按需加载页面
const pages = import.meta.glob('../pages/**/*.vue')
createInertiaApp({
resolve: (name) => {
return pages[`../pages/${name}.vue`]() // 注意:返回Promise
},
// ...
})Eager Loading (Small Apps)
预加载(小型应用)
javascript
// 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`],
// ...
})javascript
// 所有页面都包含在初始包中 - 小型应用速度更快
const pages = import.meta.glob('../pages/**/*.vue', { eager: true })
createInertiaApp({
resolve: (name) => pages[`../pages/${name}.vue`],
// ...
})Hybrid Approach
混合方式
javascript
// 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`]()
},
})javascript
// 预加载关键页面,懒加载其他页面
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
链接预加载
vue
<!-- 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>vue
<!-- 鼠标悬停时预加载(默认延迟75ms) -->
<Link href="/users" prefetch>用户列表</Link>
<!-- 组件挂载后立即预加载 -->
<Link href="/dashboard" prefetch="mount">控制台</Link>
<!-- 鼠标按下时预加载 -->
<Link href="/reports" prefetch="click">报表</Link>
<!-- 多种策略组合 -->
<Link href="/settings" :prefetch="['mount', 'hover']">设置</Link>Cache Configuration
缓存配置
vue
<!-- 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>vue
<!-- 缓存1分钟 -->
<Link href="/users" prefetch cache-for="1m">用户列表</Link>
<!-- 缓存30秒,过期后1分钟内仍可使用过期数据(stale-while-revalidate) -->
<Link href="/users" prefetch :cache-for="['30s', '1m']">用户列表</Link>Programmatic Prefetching
程序化预加载
javascript
import { router } from '@inertiajs/vue3'
// Prefetch a page
router.prefetch('/users')
// With options
router.prefetch('/users', {
method: 'get',
data: { page: 2 }
}, {
cacheFor: '1m'
})javascript
import { router } from '@inertiajs/vue3'
// 预加载页面
router.prefetch('/users')
// 带选项的预加载
router.prefetch('/users', {
method: 'get',
data: { page: 2 }
}, {
cacheFor: '1m'
})Cache Tags for Invalidation
用于失效的缓存标签
vue
<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>javascript
// Manual invalidation
router.flushByCacheTags('users')
// Flush all prefetch cache
router.flushAll()vue
<Link href="/users" prefetch cache-tags="users">用户列表</Link>
<Link href="/users/active" prefetch cache-tags="users">活跃用户</Link>
<!-- 提交后会失效用户缓存的表单 -->
<Form action="/users" method="post" invalidate-cache-tags="users">
<!-- ... -->
</Form>javascript
// 手动失效缓存
router.flushByCacheTags('users')
// 清空所有预加载缓存
router.flushAll()Infinite Scrolling
无限滚动
Load more content without pagination:
无需分页,加载更多内容:
Server-Side
服务端
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?
}
}
endruby
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?
}
}
endClient-Side (Vue)
客户端(Vue)
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>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实现自动加载
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">
加载更多中...
</div>
<div v-if="!pagination.has_more" class="text-center py-4 text-gray-500">
没有更多内容了
</div>
</div>
</template>Merge Options
合并选项
ruby
undefinedruby
// 追加到数组(默认)
InertiaRails.merge { items }
// 前置到数组
InertiaRails.merge(prepend: true) { items }
// 目标特定键
InertiaRails.merge(append: 'data') { { data: items, meta: meta } }
// 更新匹配项而非重复添加
InertiaRails.merge(match_on: 'id') { items }Append to array (default)
轮询
InertiaRails.merge { items }
无需WebSocket实现实时更新:
vue
<script setup>
import { usePoll } from '@inertiajs/vue3'
// 每5秒轮询一次
usePoll(5000)
// 带选项的轮询
usePoll(5000, {
only: ['notifications', 'messages'],
onStart: () => console.log('开始轮询...'),
onFinish: () => console.log('轮询完成'),
})
// 手动控制
const { start, stop } = usePoll(5000, {}, { autoStart: false })
// 组件可见时开始轮询
const visible = usePageVisibility()
watch(visible, (isVisible) => {
isVisible ? start() : stop()
})
</script>Prepend to array
后台节流
InertiaRails.merge(prepend: true) { items }
javascript
// 默认:后台标签页中轮询速度降低90%
usePoll(5000)
// 后台标签页中保持全速轮询
usePoll(5000, {}, { keepAlive: true })Target specific key
进度指示器
—
默认NProgress
InertiaRails.merge(append: 'data') { { data: items, meta: meta } }
javascript
createInertiaApp({
progress: {
delay: 250, // 250ms后显示(跳过快速加载)
color: '#29d', // 进度条颜色
includeCSS: true, // 包含默认样式
showProgress: true // 显示百分比
},
})Update matching items instead of duplicating
禁用特定请求的进度指示器
InertiaRails.merge(match_on: 'id') { items }
undefinedjavascript
router.visit('/quick-action', {
showProgress: false
})Polling
异步请求
Real-time updates without WebSockets:
vue
<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>javascript
// 后台请求,不显示进度指示器
router.post('/analytics/track', { event: 'view' }, {
async: true,
showProgress: false
})
// 带进度指示器的异步请求
router.post('/upload', formData, {
async: true,
showProgress: true
})Throttling in Background
一次性属性
javascript
// Default: 90% throttle in background tabs
usePoll(5000)
// Keep polling at full speed in background
usePoll(5000, {}, { keepAlive: true })数据仅解析一次,并在导航中保持:
ruby
inertia_share do
{
# 每个会话仅解析一次,而非每次导航
app_config: InertiaRails.once { AppConfig.to_json },
feature_flags: InertiaRails.once { FeatureFlags.current }
}
end与可选/延迟属性结合使用:
ruby
render inertia: {
# 可选+一次性:仅在请求时解析,之后保留
user_preferences: InertiaRails.optional(once: true) {
current_user.preferences.as_json
}
}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
},
})确保用户在部署后获取最新资源:
ruby
undefinedDisable for Specific Requests
config/initializers/inertia_rails.rb
javascript
router.visit('/quick-action', {
showProgress: false
})InertiaRails.configure do |config|
使用ViteRuby摘要
config.version = -> { ViteRuby.digest }
或自定义版本
config.version = -> { ENV['ASSET_VERSION'] || Rails.application.config.assets_version }
end
当版本变更时,Inertia会触发整页刷新而非XHR请求。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
})ruby
def index
# 不良示例 - N+1查询
users = User.all
render inertia: {
users: users.map { |u| u.as_json(include: :posts) }
}
# 良好示例 - 预加载
users = User.includes(:posts)
render inertia: {
users: users.as_json(include: { posts: { only: [:id, :title] } })
}
endOnce 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 }
}
endCombined 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
}
}ruby
def index
# 仅选择所需列
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] } }
)
}
endAsset Versioning
缓存策略
—
片段缓存
Ensure users get fresh assets after deployment:
ruby
undefinedruby
def index
render inertia: {
stats: Rails.cache.fetch('dashboard_stats', expires_in: 5.minutes) do
compute_expensive_stats
end
}
endconfig/initializers/inertia_rails.rb
带ETag的响应缓存
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.ruby
def show
user = User.find(params[:id])
if stale?(user)
render inertia: { user: user.as_json(only: [:id, :name]) }
end
endDatabase 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] } })
}
endruby
undefinedSelective Loading
app/controllers/application_controller.rb
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] } }
)
}
endaround_action :track_request_time
private
def track_request_time
start = Time.current
yield
duration = Time.current - start
if duration > 1.second
Rails.logger.warn "慢请求:#{request.path} 耗时 #{duration.round(2)}s"
end
end
undefinedCaching Strategies
客户端指标
Fragment Caching
—
ruby
def index
render inertia: {
stats: Rails.cache.fetch('dashboard_stats', expires_in: 5.minutes) do
compute_expensive_stats
end
}
endjavascript
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(`导航到 ${event.detail.visit.url} 过慢:${duration}ms`)
}
})Response Caching with ETags
WhenVisible - 进入视口时懒加载
ruby
def show
user = User.find(params[:id])
if stale?(user)
render inertia: { user: user.as_json(only: [:id, :name]) }
end
end使用Intersection Observer仅在元素可见时加载数据:
Performance Monitoring
基础用法
Track Slow Requests
—
ruby
undefinedvue
<script setup>
import { WhenVisible } from '@inertiajs/vue3'
defineProps(['users', 'teams'])
</script>
<template>
<div>
<!-- 主内容立即加载 -->
<UserList :users="users" />
<!-- 滚动到视图时加载团队数据 -->
<WhenVisible data="teams">
<template #fallback>
<TeamsSkeleton />
</template>
<TeamList :teams="teams" />
</WhenVisible>
</div>
</template>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
undefinedvue
<WhenVisible :data="['teams', 'projects']">
<template #fallback>
<LoadingSpinner />
</template>
<Dashboard :teams="teams" :projects="projects" />
</WhenVisible>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`)
}
})vue
<!-- 元素进入视口前500px开始加载 -->
<WhenVisible data="comments" :buffer="500">
<Comments :comments="comments" />
</WhenVisible>
<!-- 自定义包裹元素 -->
<WhenVisible data="stats" as="section">
<Stats :stats="stats" />
</WhenVisible>
<!-- 每次元素可见时都重新加载(用于无限滚动) -->
<WhenVisible data="posts" always>
<PostList :posts="posts" />
</WhenVisible>WhenVisible - Lazy Load on Viewport Entry
与表单提交结合
Load data only when elements become visible using Intersection Observer:
表单提交后避免重新加载WhenVisible管理的属性:
javascript
form.post('/comments', {
except: ['teams'], // 不重新加载由WhenVisible管理的teams属性
})Basic Usage
滚动管理
—
滚动位置保留
vue
<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>javascript
// 始终保留滚动位置
router.visit('/users', { preserveScroll: true })
// 仅在验证错误时保留
router.visit('/users', { preserveScroll: 'errors' })
// 条件式保留
router.visit('/users', {
preserveScroll: (page) => page.props.shouldPreserve
})Multiple Props
带滚动控制的链接
vue
<WhenVisible :data="['teams', 'projects']">
<template #fallback>
<LoadingSpinner />
</template>
<Dashboard :teams="teams" :projects="projects" />
</WhenVisible>vue
<Link href="/users" preserve-scroll>用户列表</Link>Configuration Options
滚动区域
vue
<!-- 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>适用于包含多个可滚动容器的复杂布局(非文档主体):
vue
<template>
<div class="h-screen flex">
<!-- 独立滚动的侧边栏 -->
<nav class="w-64 overflow-y-auto" scroll-region>
<SidebarContent />
</nav>
<!-- 独立滚动的主内容 -->
<main class="flex-1 overflow-y-auto" scroll-region>
<slot />
</main>
</div>
</template>Inertia会跟踪并恢复带有属性的元素的滚动位置。
scroll-regionWith Form Submissions
程序化重置滚动
Prevent reloading WhenVisible props after form submission:
javascript
form.post('/comments', {
except: ['teams'], // Don't reload teams managed by WhenVisible
})javascript
router.visit('/users', {
preserveScroll: false, // 重置到顶部(默认行为)
})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
})- 属性:仅返回必要数据,使用延迟求值
- 延迟属性:将非关键数据移至延迟加载
- 局部重载:仅刷新变更的数据
- 代码分割:大型应用中懒加载页面
- 预加载:提前加载用户可能访问的下一个页面
- 无限滚动:使用合并属性实现无缝分页
- 轮询:谨慎使用,并设置合理的节流
- 数据库:预加载关联数据,仅选择所需列
- 缓存:缓存昂贵的计算结果
- 监控:跟踪并优化慢请求
- WhenVisible:懒加载首屏以下的内容
- 滚动区域:在包含多个滚动区域的复杂布局中使用
Link with Scroll Control
—
vue
<Link href="/users" preserve-scroll>Users</Link>—
Scroll Regions
—
For scrollable containers (not document body):
vue
<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 attribute.
scroll-region—
Reset Scroll Programmatically
—
javascript
router.visit('/users', {
preserveScroll: false, // Reset to top (default)
})—
Best Practices Summary
—
- Props: Return only necessary data, use lazy evaluation
- Deferred Props: Move non-critical data to deferred loading
- Partial Reloads: Refresh only changed data
- Code Splitting: Lazy load pages for large applications
- Prefetching: Preload likely next pages
- Infinite Scroll: Use merge props for seamless pagination
- Polling: Use sparingly with proper throttling
- Database: Eager load associations, select only needed columns
- Caching: Cache expensive computations
- Monitoring: Track and optimize slow requests
- WhenVisible: Lazy load below-the-fold content
- Scroll Regions: Use for complex layouts with multiple scroll areas
—