Tips and Techniques for Improving Symfony Performance

Symfony Performance Improvements: Tips and Techniques

Sylvia Fronczak Developer Tips, Tricks & Resources

Perhaps you came upon this post while looking at ways to improve Symfony performance. Or maybe you read our comparison of Laravel and Symfony and want to know more. You could have gotten here because you want to write a performant app from the start. Then again, you could just love reading all of Stackify’s blog posts. And who could blame you?

However you got to this post, or whatever goals you may have, I’m here to talk to you about Symfony performance tuning.

In today’s post, I’m going to cover Symfony performance tips that will help you get the most of your Symfony application. But I’m not going to start there. Instead, I first want to help you determine when you should start worrying about performance, and to consider where you should focus your performance improvements and where it would be a waste of time.

Once that’s established, we’ll dive into the tips and tricks that you can use. Some of these you can and should incorporate from day one. Others shouldn’t be used until you have a need for them.

Keys

So to kick this off, how do we know when we need Symfony performance improvements?

When should you worry about Symfony performance?

We often hear that premature optimization is the root of all evil. But what does that mean? Should we just write any code we want, as long as it works? Or should we put at least a little foresight into how we configure and run our application? And no matter which path we choose, what else should you do to make sure you’re taking care of performance concerns?

First, though we don’t want to spend too much time optimizing for performance issues we may never encounter, we do at least want to know what we’re getting ourselves into. For example, in our first section of the tips below, we’ll talk about some basic configuration that Symfony recommends when running applications. We don’t need to wait for bad performance before implementing those changes.

However, we also don’t want to run through the list of tips like it’s a to-do list that everyone must complete. Some Symfony performance improvements can benefit our application, while others don’t make much of a dent. But how do we know if a performance tip is worth implementing?

We can start by using Retrace to find our current application performance. We can also begin to monitor how that performance changes over time. And most importantly, we can discover where our performance suffers most for customers and hit the most critical spots with performance optimizations. Because if the customer doesn’t experience a performance issue, we shouldn’t waste resources on optimizing those last 40 milliseconds we can shave off. No one will notice that improvement, anyway.

Performance monitoring

You shouldn’t move forward with arbitrary performance enhancements without a baseline of performance metrics. Blind optimizations may not fix the performance issues that affect your customers the most. And they’ll take resources away from improving the real problems.

So if that wasn’t clear enough, let me state it simply. Don’t spend time making performance optimizations until (1) you know you have a problem and (2) you know where the problem is.

Now that’s out of the way, let’s start looking at what performance changes we can make.

What Symfony performance improvements can you make?

Now that we’ve discussed how we’d know that we need to make improvements, where do we make them? The following list will help you find a starting point. Some of these tips, like the first two sections, can and should be done at any time. The rest depends on your application and its performance metrics.

1. Upgrade to the latest and greatest

There are many reasons to upgrade to the newest versions of languages and frameworks. Hot new features make your life as a developer easier, while security improvements keep you and your customers safer from hacks and vulnerabilities. But relevant to today’s post, upgrading to the latest version can also improve performance.

Need some proof? Take a look at the changelog notes from some of the latest PHP versions and see how many times you can find the word “performance.” Almost every new version touches performance in some way. And the further behind in updates you get, the more your application performance can suffer.

Now you may say that upgrading takes time and energy that could be spent delivering new functionality. That’s true. So make sure you have a proper CI/CD pipeline that lets you upgrade your PHP, Symfony, and additional library versions quickly and easily. If things deploy smoothly, you’ll be able to get back to those business value stories after taking on the technical debt of upgrades.

So other than staying up to date, what else can you do?

keyboard

2. Optimize configuration

When looking at the configuration, you should review the documentation to see what settings are recommended. Though some configuration changes make little difference, if the documentation says you should change something by default, then you should take their word for it—that is, unless you have metrics or other proof that indicate you should go against their advice.

With Symfony, their docs provide some helpful information regarding configuration. Let’s look at some of these tips a bit closer.


PHP Retrace

Configure OPcache

Since we’ve covered why you should use the latest and greatest, we’re going to assume you use a newer version of PHP and can use the built-in OPcache.

But what is OPcache for? This cache provides in-memory storage of precompiled bytecode. That way, your application won’t need to load and parse scripts each time they’re used.

Let’s first enable OPcache in our application.

; php.ini

; For unix/Linux:
zend_extension=/full/path/to/opcache.so

; For Windows:
zend_extension="C:\full path\to\php_opcache.dll"

When using it out of the box, OPcache configuration should reflect best practices for Symfony apps. Therefore, we want to update the settings as follows.

; php.ini

; The maximum memory in megabytes that OPcache can use to store compiled PHP files.
; The current default sits at 128MB.
; Before PHP 7, this defaulted to only 64MB!
opcache.memory_consumption=256

; The maximum number of keys (and therefore script files) that can be stored in the cache hash table.
; The current default consists of 10,000.
; Before PHP 7, this defaulted to 2,000.
opcache.max_accelerated_files=20000

This config will provide better caching of scripts right away and give your customers a better performance experience, as the cache won’t fill up as quickly.

Additionally, we’ll want to configure OPcache to not bother with checking for file changes. Once you deploy your application, you shouldn’t be updating your code files on the server. And therefore, there’s no reason to have OPcache repeatedly check to see if your files changed.

; php.ini

; Tell opcache to not validate timestamps.
; Requires that you call opcache_reset() as part of your deployment process.
opcache.validate_timestamps=0

But when doing that, remember to call opcache_reset() as part of your deployment process. Otherwise, your newly deployed files won’t update in the cache.

Optimize Composer autoload

Similar to Laraval, Symfony will register various autoloaders by default. These provide assistance in loading aliases and classes for your application. And Symfony also uses Composer for determining how classes autoload within our application.

To optimize Composer autoload, we can use either composer dump-autoload or composer dumpautoload followed by our options. For my current version of Composer, if I look at the help docs, I can see the following options available.

The -o option will optimize the autoload to load with classmaps. That improves the performance of your application. It’s a bit slower than the non-optimized version, but generally, it’s recommended to use the optimize option in production.

Additionally, the –classmap-authoritative option enforces using only the classmaps and not defaulting to other loading methods if that lookup fails.

One other favorite flag improves performance. When running the application, the –no-dev option excludes test classes that don’t run during the normal application lifecycle.

3. Retrieve data efficiently

Next on our list of performance tips, let’s look at our data retrieval. Now, Symfony’s Doctrine ORM has its own best practices and tips. There’s too many to cover here. But let’s include a few tips that seem to come up often.

Let slow query log show you the way

query

This tip doesn’t apply just to Symfony, as it’s related to your database logs. If you suspect that you have slow queries in your application, you can identify them using a slow query log on the database layer. Now, this isn’t something you should have turned on all the time or in production, as it may cause performance issues of its own. But enabling it in a test environment will allow you to see what queries take the most time and where to focus your efforts.

Use eager loading

Symfony uses the Doctrine ORM to make getting data out of the database easy. However, if we don’t use Doctrine effectively, it can lead to performance issues.

For example, the code below will get all the items in our database with their MSRP price. Let’s say that we have 30 items from our store in the database. Also, the price lives in another table and not the items table. So if you were to profile this code, you’d see that you’re calling the database for every item in our list to get the price. That’s 31 calls in all.

{%for item in items 0%}
<tr>
<td>{{item.name}}</td>
<td>{{item.description}}</td>
<td>{{item.price.msrp}}</td>
</tr>
{%endfor1%}

Instead, if we use eager loading, we’ll pull all the data we need upfront.

$q = Doctrine::getTable('Item')
->createQuery('u')
->leftJoin('u.Price p');
$items = $q->execute();

But as always, only apply eager loading if it makes sense. If you don’t actually need to get additional data from the database or if you only get it for one or two of the entities that come back, then lazy loading works well. Thankfully, our monitoring and metrics can help us make the right decision for our app.

Caching data

In addition to caching scripts, which we already covered, we can also cache data for faster access.

We have a few different options for caching data. First, we can enable the query result cache on frequently-run queries where the data rarely changes. For this, you can use Symfony’s OPcache. Or you can also use caches like Memecached and Redis. For this example, let’s use Redis.

<?php
$redis = new Redis();
$redis->connect('redis_host', 6379);
$cacheDriver = new \Doctrine\Common\Cache\RedisCache();
$cacheDriver->setRedis($redis);
$cacheDriver->save('my_cache_id', 'my_data');

In the example above, we connect to our Redis host and then set our cacheDriver to the Redis cache. Then, whenever we want to save data explicitly to our cache, we can call the save function on our cacheDriver.

Next, we can check to see if we have particular data within our cache.

<?php
if ($cacheDriver->contains('my_cache_id')) {
echo 'cache exists';
} else {
echo 'cache does not exist';
}

Then, let’s get data out of our Redis cache using fetch.

<?php
$my_cached_data = $cacheDriver->fetch('my_cache_id');

And finally, we can remove entries from our cache using delete or deleteAll.

<?php
$cacheDriver->delete('my_cache_id');
$deleted = $cacheDriver->deleteAll();

You can even use multiple caches to take advantage of different performance that they provide.

Consider skipping the ORM

My next tip might seem contrarian. However, we should always remember that different tools and technologies exist to fix specific problems. And if they don’t fix your specific application problem, then it’s valid to reconsider.

So why would we want to skip the ORM, since they make things easy. They map your database to your objects without much thought. But sometimes they can cause issues. That’s because hydrating, or deserializing, objects from the database into your classes takes time. And the larger the objects and their associates, the more of a performance hit you might take.

So once your application slows due to deserialization issues, you should be ready to take a different approach and look for alternatives. And perhaps it’s not an all or nothing solution. For example, you can cache your data in Redis or another cache to reduce the reads from the actual database. Alternatively, you can remove the ORM layer altogether.

But you won’t know if that’s the right move unless you have metrics.

metrics

4. Additional caching

In addition to database results, we have a few more things we can cache.

First, let’s look at HTTP caching. This isn’t specific to Symfony, but it can help give your application an edge in performance. The HTTP cache lives between your customer or client and your backend services.

Though most of our applications require dynamic data and loading, we usually have some parts of the application that don’t change often. By using HTTP caching, we can cache an entire page and skip calling our application server for everything but the first call.

What does this look like? In the example below, we add a @Cache annotation that controls the caching of the page that’s sent back to the client. In the example, we’ve added it to a specific page, but you can also add this annotation to an entire controller.

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;

/**
* @Cache(expires="tomorrow", public=true)
*/
public function index()
{
//....
}

And if you don’t want to cache an entire page, you can cache just fragments of it using edge side includes (ESI).

To use ESI, first, configure your application to enable it.

// config/packages/framework.php

$container->loadFromExtension('framework', [
// ...
'esi' => ['enabled' => true],
]);

And then render components separately with the render_esi function from Twig.

{# templates/static/index.html.twig #}

{# Load the dynamic page separately using a controller reference #}
{{ render_esi(controller('App\\Controller\\MyDynamicController', { 'myParam': 5 })) }}
{# or a URL #}
{{ render_esi(url('my_dynamic_page', { 'myParam': 5 })) }}

Then, each controller you use can be configured to cache differently, while still all loading data on the same page in your application.

Wrapping up Symfony performance

Though this post was about Symfony performance, one main point applies to everything. Without monitoring and metrics around your application, performance tuning won’t provide the best dollar value unless you optimize the right things. So whenever you’re tempted to start optimizing code, take a moment to add metrics or logging so that you can determine if optimizations are really necessary. And then you’ll also be able to see how much of an improvement your optimizations make.

Take a look at Retrace to see how Stackify can help with your monitoring to make sure you’re spending your development time focusing on the right code.

Schedule A Demo

About Sylvia Fronczak

Sylvia is a software developer that has worked in various industries with various software methodologies. She’s currently focused on design practices that the whole team can own, understand, and evolve over time.