Introduction
It’s estimated that 40% of people will leave a website if it takes longer than 3 seconds to load! So, it’s incredibly important from a business standpoint to make sure that you stay under that 3 second threshold.
Therefore, whenever I write any code for my new Laravel projects, I try to make sure to optimise the code as much as I can within my given time and cost constraints. If I ever work on any existing projects, I also try to use these techniques to update any slow running code to optimise the performance of the Laravel app. Not only does this make the Laravel application faster, but it can also improve the overall experience for the users.
Below are some of the techniques that I use (or suggest to other developers) to get some quick performance improvements for mine and my clients' Laravel websites and applications.
If you find these tips useful, you might be interested in checking out my ebook Battle Ready Laravel. It's jam-packed with tips, tricks and advice that you can use to improve your Laravel projects.
1. Only fetch the fields you need in your database queries
One easy way to speed up your Laravel site is by reducing the amount of data transferred between your app and the database. A way that you can do this is by specifying only the columns that you need in your queries using a select clause.
As an example, say you have a User model that contains 20 different fields. Now, imagine that you have 10000 users in your system and you’re trying to do some form of processing on each of them. Your code might look something like this:
1$users = User::all();2 3foreach($users as $user) {4 // Do something here5}
The above query would be responsible for retrieving 200,000 fields worth of data. But, imagine that when you’re processing each user, you only ever actually use the id
, first_name
and last_name
fields. So, this means that out of the 20 fields that you’re retrieving, 17 of them are more or less redundant for this particular piece of code. So, what we can do is explicitly define the fields that are returned in the query. In this case, your code may look something like this:
1$users = User::select([‘id’, ‘first_name’, ‘last_name’])->get();2 3foreach($users as $user) {4 // Do something here5}
By doing this, we will have reduced the amount of fields returned in the query from 200,000 to 30,000. Although this probably wouldn’t have much effect on the database’s IO load, it would reduce the network traffic between your app and the database. This is because there would (presumably) be less data to serialise, send and then deserialise than if you were to fetch all of the available fields. By reducing the network traffic and the amount of data that needs to be processed, this would help to speed up your Laravel site.
Please note, in the above example you might not ever actually do something like this and you would probably use chunks or pagination depending on the situation. The example is just to show a possible, easy-to-implement solution.
This solution might not gain you large improvements on a smaller site or application. However, it is something that can definitely assist in reducing load times on applications where performance is an important must-have. You might also see better improvements if you’re querying a table that has BLOB or TEXT fields. These fields can potentially hold megabytes of data and so can potentially increase the query time. So, if your model’s table contains either of these fields, consider explicitly defining the fields that you need in your query to reduce the load time.
2. Use eager loading wherever possible
When you are fetching any models from the database and then doing any type of processing on the model’s relations, it’s important that you use eager loading. Eager loading is super simple using Laravel and basically prevents you from encountering the N+1 problem with your data. This problem is caused by making N+1 queries to the database, where N is the number of items being fetched from the database. To explain this better and give it some context, let's check out the example below.
Imagine that you have two models (Comment
and Author
) with a one-to-one relationship between them. Now imagine that you have 100 comments and you want to loop through each one of them and output the author’s name.
Without eager loading, your code might look like this:
1$comments = Comment::all();2 3foreach ($comments as $comment ) {4 print_r($comment->author->name);5}
The code above would result in 101 database queries! The first query would be to fetch all of the comments. The other one hundred queries would come from getting the author’s name in each iteration of the loop. Obviously, this can cause performance issues and slow down your application. So, how would we improve this?
By using eager loading, we could change the code to say:
1$comments = Comment::with(‘authors’)->get();2 3foreach ($comments as $comment ) {4 print_r($comment->author->name);5}
As you can see, this code looks almost the same and is still readable. By adding the ::with('authors')
this will fetch all of the comments and then make another query to fetch the authors at once. So, this means that we will have cut down the query from 101 to 2!
This can drastically improve your application's performance and speed up your Laravel website!
For more information, check out the Laravel documentation on eager loading.
3. Get rid of any unneeded or unwanted packages
Open up your composer.json file and look through each of your dependencies. For each of your dependencies, ask yourself "do I really need this package?". Your answer is mostly going to be yes, but for some of them it might not be.
Each time you include a new Composer library into your project, you are potentially adding extra code that might be run unnecessarily. Laravel packages typically contain service providers that are run on each request that register services and run code. So, say if you add 20 Laravel packages to your application, that’s probably a minimum of 20 classes being instantiated and run on each request. Although this isn’t going to have a huge impact on performance for sites or applications with small amounts of traffic, you’ll definitely be able to notice the difference on larger applications.
The solution to this is to determine whether you actually need all of the packages. I have a list of 8 essential questions I ask myself before using a Laravel package.
Maybe you’re using a package that provides a range of features but you’re only actually using one small feature out if it. Ask yourself "could I write this code myself and remove this entire package"? Of course, due to time constraints, it’s not always feasible to write the code yourself because you’ll have to write it, test it and then maintain it. At least with using the package, you’re making use of the open-source community to do those things for you. But, if a package is simple and quick to replace with your own code, then I’d consider removing it.
4. Cache, cache, cache!
Laravel comes with plenty of caching methods out of the box. These can make it really easy to speed up your website or application while it’s live without needing to make any code changes.
Route caching
Because of the way that Laravel runs, it boots up the framework and parses the routes file on each request that is made. This requires reading the file, parsing it’s contents and then holding it in a way that your application can use and understand. So, Laravel provides a command that you can use which creates a single routes file that can be parsed much faster:
1php artisan route:cache
Please note though that if you use this command and change your routes, you’ll need to make sure to run:
1php artisan route:clear
This will remove the cached routes file so that your newer routes can be registered. It might be worthwhile to add these two commands to your deploy script if you don’t already have it. If you don’t use a deploy script, you might find my package Laravel Executor useful to help you when running your deployments.
Config caching
Similar to the route caching, each time that a request is made, Laravel is booted up and each of the config files in your project are read and parsed. So, to prevent each of the files from needed to be handled, you can run the following command which will create one cached config file:
1php artisan config:cache
Just like the route caching above though, you’ll need to remember to run the following command each time you update your .env file or config files:
1php artisan config:clear
In the past, I’ve seen a lot of developers cache their config in their local development environment and then spend ages trying to figure out why their .env file changes aren’t showing up. So, I’d probably recommend only caching your config and routes on live systems so that you don’t end up in the same situation.
Caching queries and values
Within your Laravel app’s code, you can cache items to improve the website’s performance. As an example, image you have the following query:
1$users = DB::table('users')->get();
To make use of caching with this query, you could update the code to the following:
1$users = Cache::remember('users', 120, function () {2 return DB::table('users')->get();3});
The code above uses the remember()
method. What this basically does is it opens checks if the cache contains any items with the key users
. If it does, it returns the cached value. If it doesn’t exist in the cache, the result of the DB::table('users')->get()
query will be returned and also cached. In this particular example, the item would be cached for 120 seconds.
Caching data and query results like this can be a really effective way of reducing database calls, reducing runtime and improving performance. However, it’s important to remember that you might sometimes need to remove the item from the cache if it’s no longer valid.
Using the example above, imagine that we have the users
query cached. Now imagine that a new user has been created, updated or deleted. That cached query result is no longer going to be valid and up to date. To fix this issue, we could make use of Laravel model observers to remove this item from the cache. This means that next time we try and grab the $users variable, a new database query will be ran that will give us the up-to-date result.
5. Use the latest version of PHP
With each new version of PHP that comes out, performance and speed is improved. Kinsta ran a lot of tests across multiple PHP versions and different platforms (e.g. - Laravel, WordPress, Drupal, Joomla) and found that PHP 7.4 gave the best performance increase.
This particular tip might be a bit more difficult to implement compared to the other tips above because you’ll need to audit your code to make sure that you can safely update to the latest version of PHP. As a side note, having an automated test suite might help give you the confidence doing this upgrade!
6. Make use of the queues
This tip might take a little bit longer than some of the other code-based tips above to implement. Despite this, this tip will probably be one of the most rewarding in terms of user experience.
One way that you can cut down the performance time is to make use of the Laravel queues. If there’s any code that runs in your controller or classes in a request that isn’t particularly needed for the web browser response, we can usually queue it.
To make it easier to understand, check out this example:
1class ContactController extends Controller 2{ 3 /** 4 * Store a new podcast. 5 * 6 * @param Request $request 7 * @return JsonResponse 8 */ 9 public function store(ContactFormRequest $request)10 {11 $request->storeContactFormDetails();12 Mail::to('mail@ashallendesign.co.uk')->send(new ContactFormSubmission);13 14 return response()->json(['success' => true]);15 }16}
In the above code, when the store()
method is invoked it stores the contact form details in the database, sends an email to an address to inform them of a new contact form submission, and returns a JSON response. The issue with this code is that the user will have to wait until the email has been sent before they receive their response on the web browser. Although this might only be several seconds, it can potentially cause visitors to leave.
To make use of the queue system, we could update the code to the following instead:
1class ContactController extends Controller 2{ 3 /** 4 * Store a new podcast. 5 * 6 * @param Request $request 7 * @return JsonResponse 8 */ 9 public function store(ContactFormRequest $request)10 {11 $request->storeContactFormDetails();12 13 dispatch(function () {14 Mail::to('mail@ashallendesign.co.uk')->send(new ContactFormSubmission);15 })->afterResponse();16 17 return response()->json(['success' => true]);18 }19}
The code above in the store() method will now store the contact form details in the database, queue the mail for sending and then return the response. Once the response has been sent back to user’s web browser, the email will be added to the queue so that it can be processed. By doing this, it means that we don’t need to wait for the email to be sent before we return the response.
Check out the Laravel docs for more information on how to set up the queues for your Laravel website or application.
Need Any Help?
If you need any help or advice on how to implement these tips into your Laravel application, feel free to drop me a message and contact me. I also provide a Laravel web development service if you would be interested in getting me to add these optimisations for you.