Symfony Performance tips and tricks
Many a times, there is a knock on Symfony framework that it is slow. Frequently you will hear quips like “Symfony has a great set of features for building enterprise applications but performs worse than <insert your framework here>”. There are also many benchmarks which will take a trivial use case and an untuned Symfony and paint a sorry picture.
But we have rolled out pretty zippy applications that handle tons of loads using Symfony. We will share a few tips and tricks that we have used in our apps.
First off, performance is an important aspect and cannot be an afterthought. Often teams will finish the functionality and then do a ‘performance testing and fixing’ phase. It goes without saying that it is the wrong approach. The overall performance of the application is a culmination of many design decisions and implementation tricks and hence performance should be an exercise throughout the life cycle of the project.
Given that at Ideas2IT, we offer performance and scalability as a service offering itself and invested in a partnership with AppDynamics, we have developed a framework that follows the cycle of Estimate, Measure, Profile, Implement/Improve – Repeat, throughout the life cycle of a project to achieve desirable performance and scalability.
It is important to get an estimate of what is expected of the system in terms of
- Data Volume
- Cost constraints
If you don’t do this exercise, we will under tune the system. And sometimes might over plan which can be a costly exercise.
Never attempt to fix performance without measuring the performance profile of individual components at a detail level.
Use a tool to generate load with the characteristics of our estimate. Our tool of choice for this is JMeter. We have also used SAAS tools like Loadimpact to avoid the effort of setting up load generating hardware. It’s important that the load generator correlates to the estimated load. Don’t go too small or too high. We usually try 2-3X of expected load. One best practice is also to keep increasing the load to know the breaking points for future planning.
Once you have your load suite running and you find bottlenecks in the system, please don’t try to fix it. Though it sounds common sense, often we have seen developers thinking of a clever idea and implementing it which might improve the performance of the component by 20% but not making any dent at all on the overall performance. Many times this is because the component’s contribution will be very small to the overall time taken.
Use profilers for each part of the application and spend enough time on profiling before attempting to fix. Our experience is that on a slow request typically 80-90% will go in a particular part of the request. May be a slow query or slow DOM binding for instance.
Our tools of choice for this:
- FrontEnd – Google Chrome developer tools, PageSpeedInsight, Phantomas
- PHP layer – Blackfire, Symfony profiler
- Database – Depends on the database. For instance, for MySQL, we use the inbuilt profiler and Jet profiler.
There are a lot of strategies for improving each part of a complex application. Parts being frontend, Symfony / App layer and database.
Page speed is very important for user’s experience and often cited as the most important UX element these days. On top of it, if your application needs to be SEO friendly, then page speed is important as Google uses it as one of the variables to compute SEO rank.
There are a lot of things that can be done to improve page speed (independent of the application speed).
In any modern application, a large part of user’s perceived performance is because of the time taken to get the data/assets from server and rendering the UI. A typical profile of a request will look like
Make sure your application gets an A on all metrics tracked by YSlow. Google maintains a nice set of rules for this:
Your biggest bang for the buck in terms of perceived performance will come from frontend tuning rather than Symfony/Appserver.
We will not go into details on what all we can do in the DB layer as it is a series of blogs in itself. But some pointers.
Slow query log:
Enable slow query log and make sure none of your queries do a full table scan and return within a threshold we have defined. Many times, teams will clear the slow query log and then forget about it. Then in one release, a developer will check in a nasty join with full table scan on a couple of big tables which will drag the whole application down. So set up a process as part of your QA to verify this on every release. If you are deploying an NPM tool like New Relic or AppDynamics, there are more sophisticated ways.
Many times, in spite of all right indices, you will find full table scans in slow query log. We will give one example, but there are many such cases.
In this instance it turned out to be because of the comparison to date in the where clause.
Just changing the where clause to fixed it.
Start with a database that is a good fit for most of your use-cases. Often this is a good old RDBMS. Then for specific use-cases which have different characteristics, choose a different DB. For instance, in one of our social apps, the table that tracked cumulative user activity grew by millions per day. We moved this table alone to Cassandra.
In most of our applications, we deal with 2-3 databases.
This is again a much discussed topic but there is only so much you can do to tune a database. From the beginning, follow a share nothing architecture to help you scale horizontally when the need arises.
Take care of the steps called out in Symfony documentation like byte code cache: https://symfony.com/doc/current/performance.html. One thing to note here that is different from what this documentation calls out is that the future of APC seems unstable. Consider using an alternative like Redis.
Avoid n+1 query problem:
Often we will retrieve an object and an association to serve a page. One easy way of doing it is using Doctrine’s findAll method. For instance, to display a category and its products in an eCommerce site:
But if you check the Symfony profiler, there will be one query for category and one query for each product in that category.
Instead, do an eager fetch as below.
Bulk updating entities:
This is a simple one, but novice developers to ORMs often do this to update an attribute in multiple entities:
Instead bulk update using:
Don’t bind to a container and then query for required service. Instead, inject the service directly. If you want to know all the available services in a large code base, you can do it by
Use a gateway cache to improve performance and bring down the load on app server.
Cache actions where you can:
In our experience, the biggest performance impact is often achieved by proper caching.
We already saw caching assets in browser cache, usage of CDN etc in the front end section.
In the app server layer, you can cache entire pages or parts of pages (fragments), and service level data. This drastically brings down the response time of the request. In addition, it brings down the overall load on the system so that even cache misses are served faster.
Caching pages or fragments:
Entire pages or part of pages can be cached with Vanish + ESI. Even in a dynamic application, there are views which is suitable for caching and need not be accurate with real-time information. For instance, comments on blog or recommendations. A good blog on caching fundamentals: https://2ndscale.com/rtomayko/2008/things-caches-do
Though Symfony has an inbuilt proxy, always use a purpose built reverse proxy like Varnish. Varnish consistently outperforms Symfony’s inbuilt proxy. https://www.symfony.fi/entry/symfony-benchmarks-symfony-proxy-vs-varnish
Caching service data:
This can be done either at the ORM level by caching query results etc or/and more higher level caches. Often times we end up using 2 or more cache layers – cache chaining pattern.
Few Symfony specific caching steps:
- Enable query cache (this is different from query result cache). This converts the DQL query into its SQL counterpart. You would think it would be enabled by default, but its not.
- Enable meta data cache. This caches the parse meta data from annotations or XML configurations instead of doing it on every request.
- Cache query results for often requested queries on slow changing data. You can cache raw SQL result data or explicitly cache the hydrated object.
- For really demanding applications, side step the ORM, directly get data as associative arrays using Doctrine DBAL and construct the object graph in the App layer.
- Though declarative object graphs are nice from design perspective, they are performance night mares for ORMs. So use one-to-many and many-to-many associations only as needed. And almost always avoid bi-directional associations. If you do use a lot of associations, consider doing multi-step hydration.