Today I stumbled upon a nice presentation that Rudi Benkovic gave last week at the Slovenian DotNet User Group, about ASP.NET MVC Performance.
The Big Picture
It is an in depth analysis of a Digg-like site and how it went from serving (on a test machine) 6 req/sec to 390 req/sec.
The biggest gain, 74 req/sec to 390 req/sec happened when he introduced data caching and another 25 req/sec where gained when he introduced the compilation of LINQ queries.
Data caching is always the key
Splitting the gains among the various components:
- 29 req/sec optimizing the usage of ASP.NET MVC (I’ll come to this later)
- 14 req/sec caching route resolution
- 340 req/sec caching the data access layer
This proves that, no matter what you do to optimize your code, the biggest performance hit is data retrieval, and the first thing you have to optimize is always this.
[UPDATE: I wrote another post with a better analysis of data]
The original presentation also have some nice graphs, so I recommend you go and watch it.
Can ASP.NET MVC be still optimized a bit?
But apart from this obvious outcome, I just want to take out from this presentation the 3 point that are really specific to ASP.NET MVC.
9 request per second are gained optimizing the route resolution. There are a lot of ways to do route resolution (aka creating a link given the action/controller you want to link to), but they can be summarized with these 3:
- expression tree-based ActionLink
- string-based ActionLink
- RouteLink specifying the RouteName
- (there is also the 4th one, the hard-coded one, but I’d rather not take this into account)
From the presentation it comes out that the first way, the expression tree one, is 30 times slower than the string-based version, which takes twice the time of specifying directly the RouteName.
So, not only the expression tree-based ActionLink is “deprecated” as it doesn’t work for action whose name has been changed through the ActionName attribute, but it’s also slower. We are talking about 1.95 ms versus 0.06 ms, and this can be considered as premature optimization (a 2ms improvement in a request that takes 200ms), but if your views have hundreds of these expression tree-based link, this can become a performance problem, as it is for the sample application analyzed which has more than hundreds of ActionLinks in the page.
Also consider that this is the first optimization he did, and with this he passed from 6 to 15 requests per second, which is a more than 150% gain. So, a pretty big gain here. You can read more about how this method is a problem on the follow up post: The performance implications of the expression tree-based ActionLink helper
Caching Route Resolution
Routes are resolved lots of time during the application lifetime, so caching them could be a nice solution: in the performance test Rudi said that this made him gain 15 req/sec. Again, not a big gain, but since implementing that kind of route resolution caching should not be a difficult task, probably it can be worth writing a small UrlHelper that caches the routes already resolved. Actually, this could be something Microsoft could add to routing engine itself.
Optimize the usage of PartialViews
In the benchmark application, the code does a loop over a list of items, and for each item it calls a RenderPartial to display the detail of each item of the list. In his benchmark the same partial was called 41 times per request.
So he decided to place the for-each loop inside the Partial View, and so have kind of the same encapsulation of markup, but gaining another 10 req/sec. This way you cannot see at a first glace that there is for-loop going on inside the partial, so probably a better solution could have been an Html Helper. Or something that the pre-compiler could have done was to “inline” the loop.
With this I don’t to say that you have to inline everything, but just remember that every RenderPartial you call has a performance hit.
Path to PartialViews
Finally he said he gained another 10 req/sec optimizing how the path to the partial views is specified. You can either specify the view path as only its name (and so the view engine will go and look for it in the current folder or in the shared folder) or you can specify the full path. But if I’m not wrong, the resolution of the real path given the name of the view is cached, so the gain is only achieved in the really first call to the method, and would have probably been averaged out if the benchmark was run for a longer time.
UPDATE: Rudi forgot to set the debug flag to false, thus disabling the view resolution caching. And when running in production you always have to set debug to false. So this is not a performance issue. Read more about this on my follow-up post: Stopping the panic: how to improve HtmlHelper.RenderPartial performances. Don’t run in debug mode
What do we have to learn from this performance analysis?
First and foremost, that data caching is the easiest and most effective way to improve the performance of your applications: without this, every other optimization is a “micro-optimization”.
If then we really want to get some other rules to apply to ASP.NET MVC applications here they are:
- Consider using the RouteName to organize your routes and then use it to generate your links, and try not to use the expression tree based ActionLink method.
- Consider implementing a route resolution caching strategy (and waiting for MS to include something similar in a future version of System.Web.Routing)
- Use your common sense when you put RenderPartials on your views page: if you end up calling the same partial 300 times in the same view, probably there is something wrong with that.
But, again, before fine-tuning these aspects of ASP.NET MVC, remember that your data access has to be cached in some way. If you don’t do to that, everything else is just a waste of time.
I ask Rudi if he could first apply the caching of data only, and then apply all the other optimizations, and see which is the impact of the ASP.NET MVC specific optimizations.
What are your ideas on this? Do you think that there are other ways of optimizing ASP.NET MVC that are not micro-optimizations? Please share your thoughts leaving a comment.