Improve perceived performance of ASP.NET MVC websites with asynchronous partial views

github_icon

[Note: The code for this project is available on GitHub.]

Imagine you’re building an ASP.NET MVC website which has some performance problems. I’m sure this would never actually happen to you, but imagine you’re facing this problem just for the sake of exploring the possibilities. :-)

Now, your web app is mostly fast and responsive, but there are certain types of data that just bring the whole thing to a grind. This article will cover a technique using ASP.NET MVC partial views, along with just a sprinkle of jQuery, JavaScript, and HTML5 to make your site feel nice and responsive even if you cannot increase the speed of certain operations.

First a disclaimer / warning. I’m going to show you how to make your site feel faster without speeding it up. If the users feel that it’s fast, then it’s fast enough. However, it might be better to try to just make it faster in the first place. This article assumes you have either tried or ruled out things like increasing the DB performance by adding the proper indexes, caching data where feasible, optimizing queries, etc. OK, assuming you’ve done as much as you can for pure performance, let’s get to improving the experience for the users.

Consider a basic website that shows data from several sources. In the sample case, it’s kind of an iGoogle landing page with different items: TODO, news, etc. In the screenshot below you can see there are several sets of information shown (my details, news, and most popular items).


(click to enlarge)

It turns out that my details and the main page are actually pretty fast. The news and popular items are not. Yet, we are computing them all at once in the MVC action. Thus the whole page feels sluggish and slow. Here is the underlying controller action method.

This is a perfectly reasonable action method. But after doing some profiling we see that we have the following performance footprint:

  10ms repository.GetUserItems()
 500ms repository.GetNews()
2000ms repository.GetPopular()
2510ms TOTAL

I don’t know about you, but 2.5 seconds seems like an unacceptable page load time. Don’t think so? Check out these amazing facts:

Latency is Everywhere and it Costs You Sales – How to Crush it
Latency matters. Amazon found every 100ms of latency cost them 1% in sales. Google found an extra .5 seconds in search page generation time dropped traffic by 20%.

So a simple solution would be to break the loading of the slower sections (news & popular in our case) apart from the faster sections in the page. We can load the fast data immediately using direct model to razor processing and push the loading of the slow parts to an asynchronous operation using AJAX.

That might be a lot of work in general. But with MVC we can employ a few techniques and really make this simple and foolproof.

First, we’re in luck from the start because we are already using partial views (a key step) for our razor code. If you don’t have partial views, they are easy to add. Here is the relevant page section which renders the data synchronously.

MVC will easily serve the PopularControl’s content up over AJAX and same for any other partial view. So let’s change our controller action in the most simple way to allow us to ask for those elements independently. Here’s the new controller code:

It is important to note that we are returning PartialView (not View) for the parts that were previously managed in the view (news & popular).

Assuming the metrics above, the page will now load in 10ms. That’s vastly better than 2,500ms. With razor rendering time and other overhead it’s more like 50ms, but still vastly better.

But our CSHTML is now messed up. What do we need to do to load this content asynchronously? First, we start by punching “holes” in our razor page where the JavaScript can add the content async. Notice how we use the data-url attribute to simplify coordinating the proper location for the content. We’ll see the JavaScript side of things shortly.

Each section that will be loaded asynchronously now has three changes.

  1. We have a div with the class partialContents. We use this in jQuery to locate them.
  2. Each partialContents section has a data-url for the location of the partialview controller action.
  3. Each partialContents section has a message and image to show an AJAX indicator that we are working on that area. There is no extra effort to show this. It appears by default and will be replaced as the content loads.

So the controller is ready to serve up this partial content. The view is showing a pending action and has a place to put the results. The final step is to just add the smallest amount of jQuery to do the download and update.

Here is the JavaScript file that does the work. Notice we simply select all the partialContents sections on page load with jQuery. The foreach one we pull out the url and call jQuery.load(). This does the download and updates the HTML for the correct partialContents.

That’s it! Now our site loads the slow sections in parallel and asynchronously.

To fully appreciate the difference you should see it for yourself. Check out the live demo I’ve posted here:

Sample: Improve Perceived Performance with Async Partial Views
Slow Version: http://improve-mvc-perf-with-async-views.azurewebsites.net/home
Async Version: http://improve-mvc-perf-with-async-views.azurewebsites.net/async

Be sure to click between them with the nav in the top right. Download the code here:

AsyncPartialViewsSampleKennedy.zip

Finally, here is the original version with the timing measured in FireBug:

Slow, Synchronous Version:

Notice the time: 2.6 seconds. Yuck. Here is the timing after the improvement:

Much Faster Async Version:

Now it’s 50ms! That is pretty awesome. Of course, if you look at the AJAX requests, they still take the same amount of time. But we do get the added benefit of natural parallelism from AJAX:

Now the whole page is ready in around 2 seconds which is in some sense truly faster (not just perceived) than the original.

So to recap, here are the steps we performed.

  1. Identified the segments of data that are slow.
  2. Took the original action method and change it to only load the fast content in the controller.
  3. Moved the CSHTML responsible for rendering each slow section to a partial view if it was not already done.
  4. Added an action method returning a PartialViewResult for each async section.
  5. Added a partialContent pending section with loading message and data-url to parts of the original page which are going to be loaded asynchronously.
  6. Use jQuery to find and call .load() for each section.

That’s it. Hopefully you will find this a useful technique for your slower pages.

Cheers,
@mkennedy

74 thoughts on “Improve perceived performance of ASP.NET MVC websites with asynchronous partial views

  1. Nice and clear article Michael. I did something similar to this in a page that included widgets where the page widgets references were stored in a database. Basically the initial page viewmodel has a list of the widgets and then I use AJAX to load them dynamically. I might incorporate your ‘loading …’ feature to make it look a little slicker. Thanks.

  2. Nice article. I just did a similar dashboard, and now profiling EF and making optimizations. Most of my performance issues were usually related with “SELECT N+1″ queries.

    But even if I make optimizations to each part, the sum effect of all will be significant. I will probably use your approach for all the parts, not just the slow ones.

    • Glad you liked it. I agree that you should look to obvious perf issues first before making them just run in the background.

      BTW, if possible, look at Scott’s recommendation (previous comment). If you can use async and await as you wait on the DB you’ll be even better off from a scalability perspective (won’t affect single user perf much).

      Cheers,
      Michael

  3. Scott McNeany says:

    This is exactly what I was looking for! I am sort of surprised that the default behavior of Html.Partial() and Html.RenderAction() are not asynchronous.

  4. norgie says:

    Michael,

    Perhaps a stupid question, but how would you handle calling the partial view methods with a parameter? E.g. let’s say the News method was changed to News(string subscriberId).

    • Hi,

      You can leverage url route data or query strings and model binding for must data. For example, if you want pass a category to the news action, then:

      would pass 72 to the action. For more complex data, use query strings. For example, to pass a class such as:

      class NewsViewModel {
      public string Category {get; set;}
      public string SortBy {get; set;}
      public int? Page {get; set;}
      }

      you can just use:

      Does that help?

      Cheers,
      Michael

      • Right, sorry! The html I put in there was entered AS HTML because I”m the editor on the blog. Sorry. think angle bracket when you see [div] etc.

        First example:

        [div data-url=”/home/news/72″ ][/div]

        Second example:

        [div data-url=”/home/news?Category=recent&SortBy=views&Page=2″ ][/div]

      • Yes, you can use AsyncControllers as Scott Hanselman was commenting above (async & await is the new way). But that doesn’t significantly increase the load time. It does parallelize it but in my example this would drop the load time from 2.6s to 2.0s. The technique in the article takes it from 2.6s to 0.05s.

  5. It doesn’t work with enabled Session. If Session is enabled (by default) then all requests in Session are executed consequentially and performance will be same time + time to round-trip for each ajax call.
    Code sample below shows how to prepare data in parallel:
    await Task.WhenAll(
    () =>
    {
    var repository = Factory.Create(); // get repository
    items = repository.GetUserItems();
    },
    () =>
    {
    var repository = Factory.Create();
    news = repository.GetNews();
    },
    () =>
    {
    var repository = Factory.Create();
    popular = repository.GetPopular();
    });

  6. Joe Reynolds says:

    Unless it is necesary to have any new content available immediately on every page hit, would it be faster to cache these slow page elements.

    • I totally agree, which is why I added this as one of the first paragraphs:

      First a disclaimer / warning. I’m going to show you how to make your site feel faster without speeding it up. If the users feel that it’s fast, then it’s fast enough. However, it might be better to try to just make it faster in the first place. This article assumes you have either tried or ruled out things like increasing the DB performance by adding the proper indexes, caching data where feasible, optimizing queries, etc. OK, assuming you’ve done as much as you can for pure performance, let’s get to improving the experience for the users.

  7. Nobody has talked about the possible impact of this technique regarding SEO as the bot don’t see the lazy loaded content, I’d use this for content not to be indexed by search engines, or in web applications where nothing should be indexed. Thanks for sharing!

    • Hi Abel,

      A very good point I overlooked in the article. My original use-case was for an internal app so SEO wasn’t top of mine. But yes, if the content needs to be found via search you’ll need to do a little extra work to provide something. But the async stuff won’t be indexed.

  8. Adam says:

    Neat solution. Thanks. Have an idea tho. Wouldn’t it be nice to turn this into a HtmlHelper extensions method to keep it as simple as possible? Something like this. Html.PartialAjax or PartialAsync (which could be misleading given async keyword). Are there any cons?
    Thanks!

  9. tfierens says:

    Let’s try again… (Re-post!)… First of all, I thought It is a great article… Only wished I had found it sooner! Now I’m trying to load and run your sample code but it’s trying to force me to download IIS Express. I’d rather if possible use the build-in web server that runs when you run and mvc app in .net or use the regular IIS which I already have on my pc. Is there a way to disable this? I’ve tried setting up UseIIS to false in the .csproj file but it didn’t like that! Any ideas?

    Many thanks.

    • Hi,

      Thanks! And you should be able to run the project without IIS express. Just go to the project properties, web tab and choose development web server rather than IIS express.

      Regards,
      Michael

      • tfierens says:

        Hi, Thanks for the reply. I tried what you suggested, but it just wouldn’t let me load the project unless I accepted to download IIS Express, so I gave in ;) .. All is still working, so no worries.

  10. David says:

    Michael, any thoughts on how to make the jquery load call happen only when the div’s are in view for the user’s screen? IE. if it is below the fold then don’t load the data/html.
    I am using a jquery plugin to lazy load some images but this same idea would be great to use on the div elements, etc.

  11. Hi Michael,

    Is there a way to use this mechanism in conjunction with an actionlink and passing a parameter to it? I want to have a few with a list of items and when the user clicks on one of the item listed, I want to display the data for this item in a view on the right inside? Thanks.

    • Hi,

      Yeah, totally. But I don’t think you need actionlink (although you could use it). Where I had data-url=”/some/url” just pass the data there. Either by constructing the URL explicitly or using Url.ActionLink().

      Cheers,
      Michael

  12. Jay says:

    I very much like your approach. I was looking for something similar, though my requirement is bit different. I am going to try your way Nd if this works, you are my super hero.

    Cheers,
    Jay

  13. Michael – this approach works really well for me! I have a situation now where the partial view that I’m loading itself requires some Javascript. This is because the partial view is displaying a grid which must be configured in Javascript (which would normally be run on $(document).ready()).

    I would prefer not to “inline” the Javascript for the partial view (since it gets spit out in the middle of my page), so I’m trying to figure out the best way for the main page (the one asynchronously loading the partial view(s)) to invoke the Javascript on behalf of the partial view.

    Any ideas?

    • Glad it’s working well for you! Interesting twist for sure.

      You might be able to just inline enough JS to append a script file to the end of the body when the partial loads. Something like:

      [script] $(document).ready(function() { $(“body”).append(“[script src=… ]”) } ) [/script]

      Worth a try!

  14. Try loading in partials from different controllers/View folders via Ajax and let me know how you get on with that..because it’s not happening for me :(

  15. Michael Munnis says:

    Michael, your approach has really helped an application I am working on in so many ways, thank you! There is one scenario I need help with if you have the time.

    Background – my primary view contains a PartialView that loads some modal content, and a button to trigger the modal to pop up. The modal content includes the div tag with the “partialContents” class and url.

    The issue – when I load the primary view, the partial view renders my modal content and the javascript triggers to call the url. What I would like to do is only call the url to retrieve the data when the button that triggers the modal is pushed.

    This is probably just a tweak to the javascript, but I am drawing a blank.

    Cheers

    • Michael Munnis says:

      I’m embarrassed to admit that the solution was simple. I just wrapped your javascript function in a new javascript function that triggers from an onclick event, then looks for a partialContentByTrigger class.

      $(“#partialContentTrigger”).click(function() {
      $(“.partialContentByTrigger”).each(function(index, item) {
      var url = $(item).data(“url”);
      if (url && url.length > 0) {
      $(item).load(url);
      }
      });
      });

  16. Al CoPaine says:

    Nice and helpful article.
    I use it with additional parameters to pass data to the ^called controller action:

    cshtml:

    Loading Scan Data…

    Script:
    /*partial loaders*/
    $(“.spPartialContents”).each(function (index, item) {
    // get data values
    var url = $(item).data(“url”);
    var filter = $(item).data(“filter”);
    var start = $(item).data(“start”);
    var end = $(item).data(“end”);
    var location = $(item).data(“location”);

    if (url && url.length > 0) {
    $(item).load(url, { filterProject: filter, startTime: start, endTime: end, location: location });
    }
    });

    works perfectly.
    greets AL

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s