A modern dashboard rarely needs every dataset on its first render. Users may never open the audit log, permission panel, or analytics widget. Loading all of that data upfront adds work before the user can interact with the page.
Inertia 3.x gives you a more precise option. The WhenVisible component loads data when its section enters the viewport. It uses the browser’s IntersectionObserver API and triggers an Inertia partial reload for the requested props.
The result is simple: render the page shell first, then load each expensive section when the user is ready to see it.
WhenVisible: A viewport-based partial reload
WhenVisible watches an element in the browser. When that element becomes visible, Inertia requests the props listed in its data attribute.
<script setup>
import { WhenVisible } from '@inertiajs/vue3'
</script>
<template>
<WhenVisible data="permissions">
<template #fallback>
<div class="skeleton">Loading permissions...</div>
</template>
<PermissionList :permissions="permissions" />
</WhenVisible>
</template>
The browser handles visibility detection. Inertia handles the request. Laravel resolves the requested prop and returns the partial page data.
This keeps the interaction inside the normal Inertia lifecycle. You do not need to create a separate JSON endpoint for every below-the-fold widget. You also avoid manually wiring an IntersectionObserver to router.reload.
WhenVisible works best with deferred props. Deferred props are omitted from the initial page response. The server resolves them later when Inertia requests them.

data: Load one prop or several
The data prop accepts either a string or an array.
Use a string when one component depends on one prop:
<WhenVisible data="activity">
<ActivityList :items="activity" />
</WhenVisible>
Use an array when several props belong to the same visible section:
<WhenVisible :data="['analytics', 'trend']">
<AnalyticsPanel
:analytics="analytics"
:trend="trend"
/>
</WhenVisible>
Grouping related props keeps their request together. It also gives you one loading boundary for the complete section.
Keep the groups focused. A permission panel should not request the audit log unless both sections need to appear together.
buffer: Start before the user arrives
A request that starts only after a component enters the viewport can still produce a visible delay. The buffer prop moves the trigger point upward.
<WhenVisible
data="activity"
:buffer="400"
>
<template #fallback>
<ActivitySkeleton />
</template>
<ActivityList :items="activity" />
</WhenVisible>
With buffer="400", Inertia starts loading the prop 400 pixels before the element becomes visible. The data can arrive while the user is still scrolling toward the section.
A larger buffer can make the interface feel smoother. It can also fetch data that the user never reaches. Start with a modest value, then adjust it based on the section’s height, query cost, and typical scrolling speed.
always: Refresh on every re-entry
By default, WhenVisible triggers once. It stops observing after the first successful load.
Add always when the section should refresh each time it re-enters the viewport:
<WhenVisible data="activity" always>
<ActivityList :items="activity" />
</WhenVisible>
This suits data that can change while the user moves through a page. Examples include:
- Recent activity feeds
- Live order summaries
- Notification panels
- Infinite scroll sentinels
- Short-lived analytics
Inertia avoids starting a second WhenVisible request while the current one is still in flight. If the element remains visible, it waits for the active request to finish before starting the next one.
That protects the server from a burst of overlapping requests when users scroll quickly.
fetching: Keep existing data visible
The fallback slot handles the first load. It should usually contain a skeleton or placeholder.
For later requests, hiding the existing content creates unnecessary movement. The fetching slot prop lets you show a subtle refresh state while keeping the current data visible.
<WhenVisible data="activity" always>
<template #default="{ fetching }">
<section :class="{ 'opacity-60': fetching }">
<ActivityList :items="activity" />
<span v-if="fetching" class="text-sm">
Refreshing...
</span>
</section>
</template>
<template #fallback>
<ActivitySkeleton />
</template>
</WhenVisible>
This distinction matters during re-entry. The user has already seen the activity list, so a skeleton is no longer the right response. A small indicator communicates that the list is being refreshed without discarding useful context.
WhenVisible, Deferred, and usePoll: Choose the right trigger
These features solve related problems, but they do not mean the same thing.
| Feature | Trigger | Best for |
|---|---|---|
| Deferred props | After the initial page render | Expensive data that should not block first paint |
WhenVisible |
When an element enters the viewport | Below-the-fold sections and scroll-driven loading |
usePoll |
At a time interval | Data that must refresh independently of scrolling |
A deferred prop answers this question:
Should this data wait until after the page has rendered?
WhenVisible answers a different question:
Should this data wait until the user reaches its section?
usePoll answers another:
Should this data refresh every few seconds?
For example, a dashboard might defer its analytics data and load it with WhenVisible. Once the user reaches the panel, it can refresh on every re-entry.
A live operations screen may use usePoll instead:
<script setup>
import { usePoll } from '@inertiajs/vue3'
usePoll(5000, {
only: ['activeOrders'],
})
</script>
This refreshes activeOrders every five seconds while the page is mounted. It does not depend on the user scrolling to the relevant section.

A Laravel example: Lazy activity data
Consider a dashboard with recent activity. The initial page needs the user profile and summary cards. The activity feed can wait until the user scrolls down.
The controller declares activity as a deferred prop:
<?php
namespace App\Http\Controllers;
use App\Models\Activity;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Inertia\Inertia;
use Inertia\Response;
class ActivityController extends Controller
{
public function index(Request $request): Response
{
$user = $request->user();
return Inertia::render('Dashboard', [
'summary' => fn () => [
'openTasks' => $user->tasks()->open()->count(),
'unreadNotifications' => $user->unreadNotifications()->count(),
],
'activity' => Inertia::defer(
fn () => Cache::remember(
"dashboard:activity:{$user->id}",
now()->addSeconds(30),
fn () => Activity::query()
->where('user_id', $user->id)
->latest()
->limit(20)
->get([
'id',
'description',
'created_at',
])
)
),
]);
}
}
The activity query sits inside the deferred callback. Laravel does not run it for the initial dashboard payload. When WhenVisible requests activity, Inertia performs a partial reload and resolves the callback.
The cache protects repeated loads. This is especially useful when always is enabled or when users move back and forth across a section. Choose the cache duration according to how fresh the activity feed needs to be.
Laravel’s cache API supports several backends, including Redis and Memcached. A shared cache is usually the right choice for multi-server applications.
The route can remain a normal Laravel route:
use App\Http\Controllers\ActivityController;
use Illuminate\Support\Facades\Route;
Route::get('/dashboard', [ActivityController::class, 'index'])
->middleware('auth')
->name('dashboard');
Laravel’s controller structure keeps the request logic focused. See the controller documentation for resource routes, middleware, and dependency injection.
Vue: Connect the activity feed
The Vue page can now load the deferred prop as the user approaches the feed:
<script setup>
import { WhenVisible } from '@inertiajs/vue3'
const props = defineProps({
activity: {
type: Array,
default: () => [],
},
})
</script>
<template>
<section class="mt-12">
<h2>Recent activity</h2>
<WhenVisible
data="activity"
:buffer="400"
always
>
<template #default="{ fetching }">
<div :class="{ 'opacity-60': fetching }">
<ActivityList :items="props.activity" />
<p v-if="fetching" class="text-sm">
Refreshing activity...
</p>
</div>
</template>
<template #fallback>
<ActivitySkeleton />
</template>
</WhenVisible>
</section>
</template>
The first visit shows ActivitySkeleton. Later re-entries preserve the activity list and expose fetching during the partial reload.
The same pattern works for a slow analytics query, a permission panel, or an audit log. The controller remains a normal Laravel controller. The frontend decides when the data becomes relevant.
Combine WhenVisible with Deferred rescue states
Visibility-based loading improves timing. It does not remove failure modes. A slow analytics service, unavailable reporting database, or temporary network problem can still affect a deferred section.
For resilient sections, combine WhenVisible with a rescued deferred prop:
'analytics' => Inertia::defer(
fn () => $this->analytics->forUser($user),
rescue: true
),
Then provide a retry interface through the Deferred component:
<script setup>
import { Deferred, WhenVisible, router } from '@inertiajs/vue3'
</script>
<template>
<WhenVisible
data="analytics"
:buffer="500"
>
<template #fallback>
<AnalyticsSkeleton />
</template>
<Deferred data="analytics">
<template #rescue="{ reloading }">
<div class="rounded border p-4">
<p>Analytics are temporarily unavailable.</p>
<button
:disabled="reloading"
@click="router.reload({ only: ['analytics'] })"
>
{{ reloading ? 'Retrying...' : 'Try again' }}
</button>
</div>
</template>
<AnalyticsPanel :data="analytics" />
</Deferred>
</WhenVisible>
</template>
rescue: true tells Inertia to report the exception while omitting the failed prop from the response. The rescue slot then gives the user a useful state instead of a broken section.
This pattern is valuable when the section is optional but important. Users can continue using the dashboard while the analytics widget retries independently.
A practical loading strategy
Use WhenVisible when visibility determines relevance. Use buffer when the section needs time to prepare. Add always only when re-entry should produce fresh data.
Keep the server side equally deliberate:
- Put expensive work inside
Inertia::defer(). - Return only the fields the component needs.
- Cache repeated results when freshness allows it.
- Use
rescue: truefor optional services and slow integrations. - Keep authorization checks inside the deferred callback.
- Group related props instead of loading the entire dashboard.
These choices matter whether you use Laravel as a PHP web framework for an internal dashboard or as one of your PHP developer tools for teams that build REST APIs with PHP.
The broader principle is straightforward: let the page load what establishes context, then load detail when context makes it useful. Inertia 3.x gives Laravel and Vue a clean way to make that decision without leaving the application’s normal request flow.
For more patterns, explore the Inertia WhenVisible documentation, deferred props, and polling. You can also browse Laravel’s starter kits for a practical foundation when building the next dashboard or application.