Improve page responsiveness with lazy loading in InertiaJS

Part of the attractiveness of A javascript single page app vs. the more traditional server-rendered ones is a perception of speed for the users - page loads and navigation can feel instantaneous while presenting loading states to the user to show them that something's happening, rather than just waiting for the entire page to load.

We're currently using InertiaJS, which allows us to marry all the best parts of a single page app with the server-side routing and data management benefits of a more traditional javascript app.

Now normally you'd pass your data to Inertia by passing data in a similar way you'd pass it to blade views:

MyController.php
Copy

_10
return Inertia::render('MyComponent.svelte', [
_10
'name' => auth()->user()->name,
_10
'posts' => $posts,
_10
]);

And in our frontend component:

MyComponent.svelte
Copy

_13
<script>
_13
// Grab the data as props.
_13
export let user = undefined;
_13
export let posts = [];
_13
</script>
_13
_13
{#each posts as post}
_13
<div>
_13
<h2>{post.title}</h2>
_13
</div>
_13
{:else}
_13
<p>No Posts Yet</p>
_13
{/each}

Simple right? But what if $posts is a complicated query takes 3-4 seconds to pull from the database? Laravel won't even send anything to the browser until it's completed that query. Your users will see a blank screen, or even worse, it'll just look like they've stayed on the same page and nothing has happened.

Now normally, we'd fix this by returning the empty page skeleton, and then making an AJAX request - usually with something like Axios, to retrieve our data.

This approach has a few drawbacks:

  1. You usually have to register another, separate route to make the request to which means you have to write a separate controller method or handler.
  2. The new controller or handler doesn't automatically have the context from the current page load, so you have to push that context (any query arguments or route parameters), in the ajax request.
  3. All of that is extra code to maintain and test.

Lazy loading solves this by allowing you run a partial reload on data that wasn't included at all during the initial page load. The Inertia documentation has a great guide on how to set up data for lazy loading:

MyController.php
Copy

_17
return Inertia::render('Users/Index', [
_17
_17
// ALWAYS included on first visit
_17
// OPTIONALLY included on partial reloads
_17
// ALWAYS evaluated
_17
'users' => User::get(),
_17
_17
// ALWAYS included on first visit
_17
// OPTIONALLY included on partial reloads
_17
// ONLY evaluated when needed
_17
'users' => fn () => User::get(),
_17
_17
// NEVER included on first visit
_17
// OPTIONALLY included on partial reloads
_17
// ONLY evaluated when needed
_17
'users' => Inertia::lazy(fn () => User::get()),
_17
]);

You can see the big difference with the third approach is that it's "NEVER included on the first visit". This makes your initial page load much faster, and allows you to set up a spinner, skeleton, or loading component of your own to show that data is being loaded in.

To do this, first, wrap any large datasets or queries in the Inertia::lazy() function:

MyController.php
Copy

_10
return Inertia::render('MyComponent.svelte', [
_10
'name' => auth()->user()->name,
_10
'posts' => Inertia::lazy(function () {
_10
return Post::query()
_10
->with('author')
_10
->where('published','=', 1)
_10
->limit(50)
_10
->get();
_10
}
_10
]);

Now, because this is lazy-loaded, in our javascript component, the prop will have whatever initial value we set. Here, i'm setting it as undefined by default.

MyComponent.svelte
Copy

_26
<script>
_26
export let posts = undefined;
_26
_26
// mounted() in VueJS / useEffect() in React
_26
onMount(() => {
_26
_26
// This will show the variable as empty.
_26
console.log(posts);
_26
_26
// We fire a partial reload to load the data in:
_26
Inertia.reload({
_26
only: ['posts']
_26
});
_26
})
_26
</script>
_26
_26
<!-- In our template, we determine the state -->
_26
<!-- of the posts variable to show a loader -->
_26
{#if posts === undefined}
_26
<!-- Show our loader component -->
_26
<p>Loading</p>
_26
{:else}
_26
{#each posts as post}
_26
<!-- Render our blog post components -->
_26
{/each}
_26
{/if}

We're doing a few things here:

  • Defining our prop as undefined by default, but you could also make this an empty array if you like.
  • When the component is mounted, we're making a partial reload call using Inertia.reload.
  • Checking for the state of the posts variable, and if it's undefined, we're showing a loader.

And that's it! You can use this approach to load in data when it's needed, not just on the mount hook.

For example, you could pair it with the Intersection API to detect when an element enters the brower viewport and fire the partial reload function.