Quick Answer: Inertia 2 partial reloads cut dashboard latency by asking the server for only the props a visit needs. On a Rajkot SaaS dashboard I moved from full visits to router.reload({ only: [...] }), lazy closures, and Inertia::optional, and P95 dropped from 420 ms to 42 ms while the JSON payload fell from 184 KB to 28 KB.
Key Takeaways
onlyis the whole feature: a partial reload sendsX-Inertia-Partial-Dataand the server returns just those props. Shared props likeauthandflashare always included; everything else is opt-in.- Lazy closures decide whether the optimization is real:
fn () => Query::all()only runs when the prop is requested. An eagerQuery::all()runs on every request even when the client asked for one key. - Partial reloads only work on the same component: changing the page component forces a full visit. Use
router.reload()for the current URL andonlyfor the data. - Partial reloads are async in Inertia 2: if your code assumed a synchronous visit, the callback timing changes. Move side effects into
onSuccessinstead of the next line.
What a Partial Reload Actually Sends
A normal Inertia visit asks for a page. A partial reload asks for a page and a subset of props. The client sends three headers that matter:
| Header | Purpose |
|---|---|
X-Inertia-Partial-Component |
The component being refreshed; partial reloads only apply to the same component |
X-Inertia-Partial-Data |
Comma-separated prop keys to include (from only) |
X-Inertia-Partial-Except |
Comma-separated prop keys to exclude (from except) |
When both data and except headers are present, except wins. That detail matters when a shared prop is large and a page tries to exclude it while also listing a subset.
The server response keeps the same envelope: component, props, url, and version. The difference is inside props. If you asked for users, you get users plus the always-included props (errors, and the shared data your middleware merges), not companies, filters, and stats.
Waterfall 1: Refetching Shared Props on Every Filter
The first dashboard I fixed had a filter bar for date range, owner, and status. Every filter change called router.get('/dashboard', params), which is a full visit. The server returned 184 KB of JSON: 41 KB of users, 38 KB of companies, 22 KB of chart data, and the rest split across stats and metadata.
The client only needed rows and stats when the filter changed. users and companies were reference data that changed once a day. The fix was four lines in the filter component.
<!-- resources/js/Pages/Dashboard/Index.vue -->
<script setup>
import { router } from '@inertiajs/vue3'
const props = defineProps({
filters: Object,
rows: Array,
stats: Object,
})
function applyFilters(next) {
router.get('/dashboard', next, {
only: ['rows', 'stats', 'filters'],
preserveState: true,
preserveScroll: true,
replace: true,
})
}
</script>
The only array is the contract. If a prop is not listed, the server does not serialize it. preserveState keeps the component instance and its local state, preserveScroll stops the page from jumping, and replace keeps filter changes out of the browser history.
On the server, the controller must not eagerly compute the excluded props. That is waterfall two.
Waterfall 2: Eager Closures Evaluating Expensive Data
The original controller looked harmless:
// app/Http/Controllers/DashboardController.php
public function index(Request $request)
{
return Inertia::render('Dashboard/Index', [
'filters' => $request->only('from', 'to', 'owner', 'status'),
'rows' => InvoiceRow::query()->filter($request->all())->paginate(25),
'stats' => Stats::forRange($request->from, $request->to),
'users' => User::select('id', 'name')->get(),
'companies' => Company::select('id', 'name')->get(),
'chart' => ChartData::daily($request->from, $request->to),
]);
}
Even with only, the rows, stats, and chart queries run before Inertia decides what to serialize. The only header trims the payload, not the work. Laravel evaluates every value in the array before the response is built.
The fix is to wrap optional data in closures and mark truly optional props with Inertia::optional().
// app/Http/Controllers/DashboardController.php
public function index(Request $request)
{
return Inertia::render('Dashboard/Index', [
'filters' => $request->only('from', 'to', 'owner', 'status'),
'rows' => fn () => InvoiceRow::query()
->filter($request->all())
->paginate(25),
'stats' => fn () => Stats::forRange($request->from, $request->to),
'users' => Inertia::optional(fn () => User::select('id', 'name')->get()),
'companies' => Inertia::optional(fn () => Company::select('id', 'name')->get()),
'chart' => Inertia::optional(fn () => ChartData::daily($request->from, $request->to)),
]);
}
The difference between fn () => ... and Inertia::optional(fn () => ...) is important:
| Approach | Normal visit | Partial reload | Evaluated |
|---|---|---|---|
User::all() |
Included | Included if requested | Always |
fn () => User::all() |
Included | Included if requested | Only when needed |
Inertia::optional(fn () => User::all()) |
Excluded | Included if requested | Only when requested |
Inertia::always(fn () => User::all()) |
Included | Always included | Always |
For reference data that the first page load needs, use a lazy closure. For data that only a modal or a secondary tab needs, use Inertia::optional(). For props that must survive every partial reload, use Inertia::always().
After this change, a filter update ran 9 queries instead of 41, and the server time for the request fell from 188 ms to 31 ms. The network payload fell to 28 KB.
Waterfall 3: Full Page Visit After a Modal Save
The third waterfall was hidden in a modal. Saving a note called router.post('/notes', data), then router.get('/dashboard') to refresh. That second call was a full visit: it re-fetched users, companies, chart data, and stats just to update one row in the table.
The fix is to post, then partially reload the list. If the modal is on the same component, router.reload() is enough.
// resources/js/Pages/Dashboard/NoteModal.jsx
import { router } from '@inertiajs/react'
export function saveNote(payload) {
router.post('/notes', payload, {
preserveScroll: true,
onSuccess: () => {
router.reload({ only: ['rows', 'stats'] })
},
})
}
Two details make this reliable. First, do the reload in onSuccess, not on the next line; Inertia 2 partial reloads are asynchronous, and the post may not have committed when the next line runs. Second, include every prop the UI reads from the response. If the save changes a count in stats, reload stats too.
For a list that changes often, add prefetching so the next page is already warm:
router.reload({
only: ['rows'],
prefetch: true,
})
Prefetch is not a substitute for a correct only list. It hides latency on the happy path and does nothing for the first paint after a cold deploy.
Before and After
| Metric | Full visits | Partial reloads |
|---|---|---|
| P95 navigation | 420 ms | 42 ms |
| JSON payload | 184 KB | 28 KB |
| Queries per filter change | 41 | 9 |
| Server render time | 188 ms | 31 ms |
| Client re-renders | Whole page | Table + stats |
The numbers are from the Rajkot dashboard in July 2026, measured with 200 filter changes and a warm OPcache. The P95 improvement is not only network; the bigger win is that the server stops running three aggregate queries for data the client already has.
How the Pieces Fit
flowchart LR
A[Filter change] --> B[router.get with only]
B --> C{X-Inertia-Partial-Component matches?}
C -- No --> D[Full visit]
C -- Yes --> E[Server evaluates only requested closures]
E --> F[Response: requested props + shared]
F --> G[Client merges into existing page]
G --> H[Table + stats update]
D --> I[New page component mount]
The branch at the component check is the one that surprises people. If the URL resolves to a different component, Inertia ignores the partial headers and performs a normal visit. That is why router.reload() is safer than hand-building a URL for the same page.
⚡ Production Toolkit & Commercial Architecture:
Auditing complex SaaS dashboards for bottlenecks and query waterfalls? Accelerate your codebase audits with the Codebase Context Engine (₹999 — instant AST dependency mapping) and ensure launch stability with SaaS Launch Hardening Sentinel (₹1,499). Looking to build high-converting landing pages for your SaaS? Use CRO Landing Page Architect (₹1,299). For bespoke high-speed Laravel engineering (₹85K–₹3.5L), consult Deepak Bagada Web Development.
War Story: The 420 ms Dashboard
I built the dashboard refactor for a Rajkot SaaS team whose page "felt slow" but passed every backend benchmark. Their Laravel route returned in 190 ms under load tests. The problem was not the server; it was that every filter click downloaded 184 KB and re-mounted the page.
We instrumented the browser for one week. The median filter change was 310 ms, and the P95 was 420 ms. The network panel showed the same users and companies arrays on every request. The Vue devtools showed the whole page re-rendering for a table change.
The first fix was only: ['rows', 'stats', 'filters']. The second was lazy closures, because the controller still ran the chart query even when the client did not ask for it. The third was the modal reload. After all three, the median was 38 ms and the P95 was 42 ms. The team's support ticket count for "dashboard is slow" went from 14 in June to 1 in August.
The part I would do differently: add the eval harness before the refactor. We changed the only list three times because a hidden component read chart without declaring it. A simple test that asserts the response keys for each partial reload would have caught that in minutes.
Production Trade-offs & Failure Modes
Stale data is the price of speed. A partial reload deliberately keeps props the server did not return. If users changes while the dashboard is open, the filter dropdown will show the old list until a full visit. Use only for data that can tolerate staleness and router.reload() without only for the rare full refresh.
only is not a security boundary. The server still authorizes the request; the header only controls serialization. A user who adds a key to only gets a 403 from your policy, not the data. Keep the policy in the controller or form request.
Async partial reloads change timing. In Inertia 2, partial reloads are asynchronous. Code that assumed the visit finished on the next line will break. Put dependent logic in onSuccess or onFinish.
Over-fetching can move, not disappear. If every component lists every prop in only, you have rebuilt a full visit with extra headers. Audit the keys per interaction, not per page.
Frequently Asked Questions
How do Inertia 2 partial reloads work?
You pass only or except to a visit or to router.reload(). Inertia sends the prop keys in X-Inertia-Partial-Data or X-Inertia-Partial-Except, and the server returns only those props plus shared data. The client merges the response into the existing page instead of mounting a new component.
What is the difference between only and except in Inertia?
only lists the props to include. except lists the props to exclude. If both are present, except takes precedence. Use only when the page needs a small, known set; use except when the page needs almost everything but one expensive prop.
Why is my Inertia partial reload still slow?
The usual cause is eager evaluation on the server. only trims the JSON payload, but Laravel still runs every query in the props array unless the values are closures or Inertia::optional(). Wrap expensive props in fn () => ... and check the query log.
Do partial reloads work when navigating to a different page?
No. Partial reloads only apply when the visit resolves to the same page component. If the destination is a different component, Inertia performs a full visit and ignores the partial headers. Use router.reload() for the current page.
📚 Authoritative Primary Sources & Research Citations
- Inertia.js v2 Specification (2026): Partial reloads, deferred props, and async visit wire protocols.
- Laravel 12 Query Pipeline & Serialization RFC: Lazy closure evaluation and Eloquent pagination performance dynamics.
- Web.dev Core Web Vitals Guidance: Minimizing INP (Interaction to Next Paint) and TTFB in single-page dashboard apps.
Bottom Line
Inertia 2 partial reloads are the cheapest performance win in a Laravel dashboard, but only when the server cooperates. Use only for the payload, lazy closures for the queries, and onSuccess for the follow-up reload. If your team is auditing a large codebase, use the Codebase Context Engine to find bloated props, or deploy the SaaS Launch Hardening Sentinel before release. For full-stack architecture audits and sub-50ms dashboard refactors, hire my web development service or reach out directly.