Faster Laravel: Surprising Optimization Tips

Sponsor

October 3rd, 2022

Faster Laravel: Surprising Optimization Tips

When we started working on our test management tool Testmo a few years ago and selected Laravel for our backend implementation, there were a couple of surprising performance issues we've noticed.

For Testmo our goal is to answer most server requests in 100ms or less. You might be wondering why this is important to us when most apps spend significantly more time rendering the frontend in the browser and executing JavaScript (which is also true for Testmo, especially with our complex UI for test case management with multiple panes and dynamic folders).

It's pretty simple: slow server-side performance will trickle down to all parts of the stack, so optimizing this part will have very positive effects on the entire app. Testmo customers are also heavily using our API as part of our test automation features. So optimizing the server-side performance also helps us to significantly reduce server load to better scale the app with fewer servers & resources.

You can find many generic Laravel optimization articles out there describing the basics of configuring caching, how to build faster database queries etc. Instead, in this article I want to share three specific optimizations for Laravel Blade views that surprised us when we started using Laravel and that resulted in drastic performance improvements.

Blade loops can be surprisingly slow

A significant part of server-side performance is based on how fast you can transform database query results to rendered content. In Laravel's case you would usually use Blade views to render your content and send results to browsers. When we initially started working with Blade and its various loop directives, we were surprised that loops are quite slow. The reason for this is that they often provide additional features that might be useful in some cases, but are unnecessary slow when you don't need them.

Let's look at Laravel Blade's @foreach loop directive as an example. You would use @foreach very often in views to iterate over data and render content, so its performance is quite critical. When we started using Laravel and measured the view performance, this directive was surprisingly slow. We expected that it maps more or less directly to PHP's foreach loops, but when you look at Laravel's implementation, it adds a lot of overhead to add the loop variable.

This feature might be useful from time to time, but it adds significant overhead in the majority of cases you don't need it. The solution for us was quite simple: we don't use @foreachat all, but added our own @loop directive, which directly maps to PHP's native foreach:

1Blade::directive('loop', function ($expression) {
2 return "<?php foreach ($expression): ?>";
3});
4 
5Blade::directive('endloop', function ($expression) {
6 return "<?php endforeach; ?>";
7});

Don't include & render many views

Views are a great way to reuse common UI elements in different parts of the app without repeating your frontend code. Let's say you are rendering the avatar of users in the UI as part of various data tables, sidebar sections and in other places. Similar to how we render user avatars to indicate contributors and owners of exploratory testing sessions in Testmo:

Ideally we could use regular Blade views to reuse and include such common UI elements, as rendering the avatar takes >10 lines of code (e.g. to fallback to user initials for users who haven't uploaded an avatar yet). But it turns out that using many Blade views in a request is a very bad idea, as including views can be very slow. So slow in fact that rendering larger tables with many UI elements can add 100s of milliseconds of overhead, which would be unacceptable for most apps.

For Testmo we try to keep the number of rendered views per request to less than 15 on most pages. This unfortunately rules out using views for common elements that could be rendered dozens of times. Instead, we've come up with an alternative approach with a good balance of dev productivity & app performance, a concept we call partials internally.

Partials are pre-rendered views that we compile & store during development and directly inject into views via a simple blade directive. This way we can easily reuse common UI elements and only write them once without the overhead of including full views. Instead, our internal @partial blade directive loads the pre-rendered code and outputs it in-place. So our views can load partials similar to including a full view, but when you look at the compiled & cached view output, it directly includes the pre-rendered code without any overhead.

1@partial('avatars.user', [
2 'user' => $user,
3 'size' => 'table',
4 'tooltip' => $user->name
5])

Including views is still slow & error-prone

Using partials instead of views was a big performance win, but including views was still unreasonable slow. But there was another issue we didn't like about Blade's default @include behavior: included views automatically inherit all data available in the parent view. So it becomes difficult to control which parameters are passed to views, which can lead to bugs and unexpected behavior quickly if you are not careful.

We noticed quite quickly that we didn't like this default behavior, so we looked into ways to improve this. When we reviewed Laravel's default @include implementation, we noticed that changing this behavior also had the potential for significant speed improvements.

In Testmo we don't use @include at all anymore. Instead, we've built our own lightweight alternative we call @require. Our own directive doesn't make all parameters available in sub views anymore, which makes it easier for us to avoid bugs, improves testability and helps us quickly understand which parameters are used & available. It also avoids using PHP's get_defined_vars function, which causes a lot of the performance issues with Laravel's default @include implementation.

1@php
2// Not automatically available in sub view
3$completed = true;
4@endphp
5 
6@require('automation.results.header', [
7 'results' => $results,
8 'states' => $states,
9])

The three above mentioned optimizations combined resulted in huge performance differences for non-trivial pages (sometimes 100s of milliseconds). Laravel's default behavior and Blade implementation provides a lot of useful features. If you don't need all these features and prefer faster views, it can make sense to use simpler & faster implementations though. Hopefully this article gives you some ideas on how to optimize your own Laravel apps.

This guest posting was written by Dennis Gurock, one of the founders of Testmo. Testmo is built using Laravel and helps teams manage all their software tests in one modern platform. If you are not familiar with QA tools, Testmo recently published various tool guides to get started:

Filed in:

Eric L. Barnes

Eric is the creator of Laravel News and has been covering Laravel since 2012.