The Dev Log › Programming
Inertia.js v2: Deferred Props, Prefetching and Polling Explained
By Jezer Niel Blanca, Full Stack Developer ·
·
6 min read
A practical guide to the Inertia v2 features I use most: deferred props for slow data, prefetching for instant navigation, and polling for live updates.
Inertia.js has been my default way to build Laravel front ends for a while now, and version 2 made it even better. The headline features are deferred props, prefetching, and polling, and together they solve problems I used to handle with custom API endpoints and a pile of Axios calls. In this post I'll explain what each feature does, when I reach for it, and the small details that make it feel polished instead of janky.
A Quick Refresher on How Inertia Loads Data
In a classic Inertia page, your controller returns a component name and a set of props. Every prop is computed on the server before the response is sent, and the page renders once all the data is ready.
return Inertia::render('Dashboard', [
'user' => $request->user(),
'projects' => $request->user()->projects()->latest()->get(),
]);
That's simple and great for most pages. The problem appears when one prop is slow. If a statistics query takes a while, the whole page waits for it, and the user stares at the old page until everything is ready. Inertia v2 gives you tools to fix that without leaving the Inertia way of working.
Deferred Props: Render Fast, Load the Heavy Stuff After
Deferred props let the page render immediately with the fast data, then fetch slower props in a follow-up request automatically. On the server side, you wrap the expensive prop with Inertia::defer():
use Inertia\Inertia;
public function show(Request $request): Response
{
return Inertia::render('Dashboard', [
'user' => $request->user(),
'projects' => $request->user()->projects()->latest()->limit(10)->get(),
'stats' => Inertia::defer(fn () => $this->stats->forUser($request->user())),
'activity' => Inertia::defer(fn () => $this->activity->recent($request->user()), 'feed'),
]);
}
The closure is not executed on the first request. Once the page mounts, Inertia makes a second request for the deferred props. The second argument, 'feed' in this example, is a group name. Props in the same group are fetched together, and different groups load in parallel.
On the client, the Deferred component handles the loading state for you:
<script setup>
import { Deferred } from '@inertiajs/vue3'
defineProps({
user: Object,
projects: Array,
stats: Object,
})
</script>
<template>
<div class="grid gap-6 md:grid-cols-3">
<Deferred data="stats">
<template #fallback>
<div class="h-32 animate-pulse rounded-xl bg-gray-200 dark:bg-gray-800" />
</template>
<StatsCards :stats="stats" />
</Deferred>
</div>
</template>
A few habits I follow with deferred props:
- Always show a skeleton. A pulsing placeholder with roughly the same size as the real content prevents layout shift and tells the user something is coming.
- Defer only what is slow. Deferring everything just adds a second round trip for no benefit.
- Remember the prop is undefined at first. Anything outside the
Deferred component that reads the prop needs to handle that.
Deferred props are not about making slow queries fast. They are about making sure one slow query never holds the whole page hostage.
Prefetching: Make Navigation Feel Instant
Prefetching loads a page's data before the user clicks. When the click finally happens, Inertia already has the response cached, so the page swaps in immediately. Adding it is a single attribute on Link:
<script setup>
import { Link } from '@inertiajs/vue3'
</script>
<template>
<nav class="flex gap-4">
<Link href="/projects" prefetch>Projects</Link>
<Link href="/reports" prefetch cache-for="1m">Reports</Link>
<Link href="/settings" :prefetch="['mount', 'hover']">Settings</Link>
</nav>
</template>
By default, prefetch triggers when the user hovers over the link for a short moment. You can also prefetch on mount, which is useful for the page you are almost certain the user will visit next, or on click, which starts loading on mousedown. The cache-for attribute controls how long the prefetched response stays fresh.
When Not to Prefetch
Prefetching is not free. Every prefetch is a real request that runs your controller code. I avoid it on:
- Links to pages with expensive queries that most users won't open.
- Anything that changes state. Prefetching should only ever be used on normal GET navigation.
- Long lists of links, like a table with a hundred rows. Hovering across the table would fire a burst of requests.
Polling: Keep Data Fresh Without WebSockets
Some pages need to stay up to date: an import progress screen, an order status page, a dashboard that shows new leads. WebSockets are great, but they add infrastructure. For many cases, polling every few seconds is enough, and Inertia v2 ships a usePoll helper for it.
<script setup>
import { usePoll } from '@inertiajs/vue3'
const props = defineProps({
importJob: Object,
})
const { start, stop } = usePoll(3000, {
only: ['importJob'],
onFinish() {
if (props.importJob.status === 'completed') {
stop()
}
},
})
</script>
The important part is only. It turns each poll into a partial reload, so the server only computes and returns the importJob prop instead of rebuilding the entire page. A few other details worth knowing:
- Polling starts automatically when the component mounts and stops when it unmounts.
- Pass
autoStart: false if you want to start it yourself with start().
- By default, polling slows down while the browser tab is in the background. Set
keepAlive: true if it must keep full speed.
On the server, I pair polling with lazily evaluated props so that partial reloads stay cheap:
return Inertia::render('Imports/Show', [
'importJob' => fn () => ImportJobResource::make($import->fresh()),
'settings' => fn () => $this->settings->all(),
]);
Because both props are closures, a partial reload that asks only for importJob never runs the settings query.
Putting It Together on a Real Page
The pattern I use most often combines all three features on one dashboard:
- The page renders instantly with the user, navigation, and a short list of recent items.
- Heavy statistics and charts arrive through deferred props with skeleton placeholders.
- Sidebar links prefetch on hover, so moving between sections feels instant.
- A small "live" widget uses
usePoll with only to refresh one prop every few seconds.
The result feels like a hand-tuned single page app, but the code is still plain Laravel controllers and Vue components. There is no separate API layer, no client-side cache library, and no duplicated validation logic.
Wrapping up
Inertia v2 closes the gap between "server-driven app" and "snappy SPA". Use Inertia::defer to keep slow props from blocking the first render, add prefetch to the links users are most likely to click, and reach for usePoll with partial reloads when data needs to stay fresh. Each feature is only a few lines of code, and together they make an app feel noticeably faster. If you want a fast, modern Laravel and Vue product built the right way, reach out and let's build it together with my team.
Tags: Inertia, Vue, Laravel, Performance