inertia-rails-performance

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Inertia 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
undefined

Bad - 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 }
undefined
render inertia: { users: User.select(:id, :name, :email, :avatar_url).as_json }
undefined

Lazy Evaluation with Lambdas

使用Lambda进行延迟求值

Impact: HIGH - Prevents unnecessary queries
ruby
undefined
影响:高 - 避免不必要的查询
ruby
undefined

Bad - 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
undefined
inertia_share do { recent_posts: -> { Post.recent.limit(5).as_json }, total_users: -> { User.count } } end
undefined

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

Frontend 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 }
  }
end
ruby
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 }
  }
end

Link 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?
    }
  }
end
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?
    }
  }
end

Client-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
undefined
ruby
// 追加到数组(默认)
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 }
undefined
javascript
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
undefined

Disable 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] } })
  }
end

Once 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 }
  }
end
Combined 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] } }
    )
  }
end

Asset Versioning

缓存策略

片段缓存

Ensure users get fresh assets after deployment:
ruby
undefined
ruby
def index
  render inertia: {
    stats: Rails.cache.fetch('dashboard_stats', expires_in: 5.minutes) do
      compute_expensive_stats
    end
  }
end

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

Database 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] } })
  }
end
ruby
undefined

Selective 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] } }
    )
  }
end
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 "慢请求:#{request.path} 耗时 #{duration.round(2)}s" end end
undefined

Caching Strategies

客户端指标

Fragment Caching

ruby
def index
  render inertia: {
    stats: Rails.cache.fetch('dashboard_stats', expires_in: 5.minutes) do
      compute_expensive_stats
    end
  }
end
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(`导航到 ${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
undefined
vue
<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
undefined
vue
<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-region
属性的元素的滚动位置。

With 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
})
  1. 属性:仅返回必要数据,使用延迟求值
  2. 延迟属性:将非关键数据移至延迟加载
  3. 局部重载:仅刷新变更的数据
  4. 代码分割:大型应用中懒加载页面
  5. 预加载:提前加载用户可能访问的下一个页面
  6. 无限滚动:使用合并属性实现无缝分页
  7. 轮询:谨慎使用,并设置合理的节流
  8. 数据库:预加载关联数据,仅选择所需列
  9. 缓存:缓存昂贵的计算结果
  10. 监控:跟踪并优化慢请求
  11. WhenVisible:懒加载首屏以下的内容
  12. 滚动区域:在包含多个滚动区域的复杂布局中使用

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
scroll-region
attribute.

Reset Scroll Programmatically

javascript
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