How to improve initial page load time by deferring rendering Livewire components
Livewire is an amazing piece of technology. It is extensively used in two products I work on: Oh Dear and Mailcoach Cloud.
In this post, I'd like to show you a simple technique where Livewire can help to improve your initial page load time significantly.
Fetching stuff from S3 can decrease initial page load time
Oh Dear is the all-in-one-monitoring tool I've built that features all sorts of checks: uptime, SSL certificate, scheduled jobs, broken links, and more.
One of the new checks we will introduce soon is the Google Lighthouse check. Using the Lighthouse tool by Google, this check can generate a report listing SEO & Performance issues and solutions.
Here's what it looks like.
This top part displays the most important results. These numbers are simply stored in our database.
The Lighthouse tool also generates a full report in HTML. That report contains a detailed analysis of your site. We decided not to rebuild this entire report in our UI but to show our users the entire HTML output of Google Lighthouse. This way, when Google updates the report with new categories, our users can immediately see the new info.
The full HTML report can be seen when you scroll a bit down.
The screenshot only shows a part of a report; it is quite lengthy. On disk, a single report takes about 3 - 4 MB. At Oh Dear, we monitor tens of thousands of sites, and we will run the Lighthouse check frequently. The total disk space needed to store all reports can easily be over 100 GB. That's why we decided to store these reports on Amazon S3.
On S3, we have virtually unlimited space, nice! But by using S3, we introduced a new problem. Fetching the Lighthouse report takes a few seconds. This is the actual code on our LighthouseReport
model that fetches the HTML report from S3.
public function htmlReport(): string
{
return Cache::remember(
"lighthouse-report-{$this->id}",
now()->addDay(),
function () {
return Storage::disk('lighthouse-reports)->get($this->html_report_file_name) ?? '';
}
);
}
You can see that we use Laravel's excellent filesystem abstraction to communicate with S3. We also copy the report to the cache, so we don't have to fetch it anymore for subsequent views.
In our view we can use it as such:
{{-- rest of view omitted for brevity --}}
<h2>Full report</h2>
{{-- In our actual codebase, displaying the HTML report --}}
{{-- is a bit more complicated, but I've omitted that --}}
{{-- too for brevity --}}
<iframe html="{{ $lightHouseReport->htmlReport() }} />
By fetching the HTML report like this, the first time we display the page, the initial page load time will be long, as we need to copy the report from S3, which takes a few seconds. The next time a report is viewed, the page load will be fast, as we got the HTML report in our local cache.
Improving initial page load using Livewire
Let's look at how we can make the page load faster when we view the report for the first time. This is where Livewire comes in.
Using Livewire, we're not going to display the HTML report immediately but instead a loading message.
Let's replace the iframe
in our view with a Livewire component. That $report
is not the HTML report but the model in our database.
<h2>Full report</h2>
<livewire:lighthouse-html-report :report="$report"/>
This is what the view backing that Livewire component looks like.
<div>
Loading the HTML report
</div>
And the Livewire component itself:
namespace App\Http\App\Livewire\Checks;
use App\Domain\Check\Checkers\Lighthouse\Models\LighthouseReport;
use Livewire\Component;
class LighthouseHtmlReportComponent extends Component
{
public LighthouseReport $report;
public function mount(LighthouseReport $reportModel)
{
$this->report = $reportModel;
}
public function render()
{
return view('app.sites.checks.lighthouse.partials.htmlReport');
}
}
Our initial page load is now very fast, because we're not fetching the HTML report from S3 anymore. But of course, we still need the load that HTML report at some point in time.
Let's add a function on the Livewire component to fetch the HTML report and add a piece of state to store it.
namespace App\Http\App\Livewire\Checks;
use App\Domain\Check\Checkers\Lighthouse\Models\LighthouseReport;
use Livewire\Component;
class LighthouseHtmlReportComponent extends Component
{
public LighthouseReport $report;
public ?string $htmlReport = null;
public function mount(LighthouseReport $reportModel)
{
$this->report = $reportModel;
}
public function fetchHtmlReport()
{
$this->htmlReport = $this->report->htmlReport();
}
public function render()
{
return view('app.sites.checks.lighthouse.partials.htmlReport');
}
}
That fetchReport
isn't being executed now. Let's also slightly modify the view that backs the LighthouseHtmlReportComponent
so that fetchReport
will be executed.
<div wire:init="fetchHtmlReport">
@if($htmlReport)
<iframe html="{{ $htmlReport }}" />
@else
Loading the HTML report
@endif
</div>
With this in place, fetchReport
will be executed after Livewire has initially rendered the component on the page. So on the initial page render, it will display "Loading the HTML report". This initial page render will be very fast, as we don't have to fetch the HTML report from S3. Then, after the page has been rendered, Livewire will execute fetchReport
. Meanwhile, the user can already interact with the page. When the HTML report has been fetched from S3, Livewire will replace that "Loading..." Message with the actual report.
Here is it in action.
wire:init
is excellent for the deferred loading of expensive stuff, and it only takes a few lines of code to improve the user experience significantly. Don't you just love Livewire?
Other use cases
In the above example, we've used wire:init
to avoid the performance penalty when loading something from S3, but there are many other use cases.
We use this technique in Mailcoach to speed up our UI in multiple places. On the overview of an email list, we show a graph of the historical view of the list. It takes a second to fetch all the data for the graph. We shave that second of the initial page load by using wire:init
. Here's what that looks like in the browser.
You can use wire:init
for more minor things too. In Mailcoach, you can segment your list into smaller parts by various criteria. Because everyone uses different criteria, creating an optimized query to determine a segment's total population is nearly impossible, and that's why we deferred computing the total. This video shows that the values in the population column are added after the page load.
So again, loading the segment page would be slower without this technique.
In closing
Deferred loading of content using wire:init
is a powerful technique that allows you to decrease the initial page load with only a couple of lines of code.
Stay moderate with this technique, though. If you have a performance problem that can be solved in a better way, you should go for that better way most of the time. Optimize queries if possible instead of reaching for deferred loading.
If you want to see this technique in action, start your free 10-day trial at Oh Dear or Mailcoach.
I've always struggled to understand the concept of wire:init with Livewire. With this article everything is now quite clear. This will now allow me to increase the performance of my pages. Thanks
Thanks for the article! I am curious about the implementation of the segments example. Is each 'count' column a Livewire component? Or is the whole table one component?
Excellent article!
I've just implemented this on a component because on load it makes an API request which can take a second.
Unfortunately, the component also has sortable columns which are dependent on the current
Request
, and that seems to be lost when the function inwire:init
is called 🙁