Monday, November 26, 2018

JavaScript caching best practices in ASP.NET

Looking to improve the performance of your site with a few simple tips? Read to understand.

One of the first things a developer should do when inspecting the performance of their ASP.NET website is to understand how the static assets of the site are served and if they are correctly being cached by the visitor's browser. On this post let's review a solution that works well, is built into the ASP.NET framework and helps you reduce costs and improve the performance of your site.

How caching works

Before getting to solution, it's important to understand how web browsers cache static resources. In order to improve performance and reduce network usage, browsers usually cache JavaScript and external resources a web page may depend on. However, under certain circumstances, modifications on those files won’t be automatically detected by the browser resulting in users seeing stale content.

Approaching the Problem

A common solution to this problem is to add query string parameters to the referenced JavaScript files. Assuming that the suffix has changed, the browser treats the new url as a new resource (even if it’s the same file name), issuing a new request and refreshing the content. Whenever that resource changes, the query string changes and the browser automatically refreshes the resource making users always seeing the most up to date content. Also note how we can use that approach for all linked resources: JavaScript files, CSS, images and even icons.

So how to ensure that our pages output references as showed above? In ASP.NET, the solution to that is to use bundles.

Bundling and Minification

According to Microsoft,
Bundling and minification are two techniques you can use in ASP.NET 4.5 to improve request load time. Bundling and minification improves load time by reducing the number of requests to the server and reducing the size of requested assets (such as CSS and JavaScript.)
Another benefit of bundling is that, by default, the Asp.Net framework comes with a cache-prevention feature
Bundles set the HTTP Expires Header one year from when the bundle is created. If you navigate to a previously viewed page, Fiddler shows IE does not make a conditional request for the bundle, that is, there are no HTTP GET requests from IE for the bundles and no HTTP 304 responses from the server. You can force IE to make a conditional request for each bundle with the F5 key (resulting in a HTTP 304 response for each bundle). You can force a full refresh by using ^F5 (resulting in a HTTP 200 response for each bundle.)

Bundling in practice

Before reviewing the actual solution, let’s compare a page serving bundled content and a page showing unbundled content and understand how the browser behaves on each of them.

Reviewing a request with bundled content

Let's take a look at a request using bundles to understand this a little better:
From the above, note that:
  • in yellow, the auto-added suffix. We'll see later why this is there and how to do it ourselves;
  • in red, a confirmation that that resource was cached by the browser and loaded locally, not issuing any request to the server;
  • the time column on the right, shows the time spent by the browser to load that resource. For most files it was 0ms (or, from cache);
Now if you expand the bundle request, we can see that the cache is set to 1 year from the request date, meaning that the file won't be refreshed for the next 365 days:

Serving unbundled content

Now if you take a look at a request to a page that has unbundled content, you'll see that the resources are also being cached (column Size). However, individual requests are still being issued for each file referenced causing two problems:
  1. In HTTP 1.1, there’s a virtual limit of max 6 concurrent requests per domain. On that case, after the 6th request is issued, others will be waiting until a new slot is available
  2. Because after each deploy file names won’t change, the browser will reuse the cached version event if modified (because by seeing the same file name, it understands the resource didn’t change). Our problem is that updates on those files won’t be automatically refreshed making users see stale content.

Bundling in ASP.NET

Because the ASP.NET framework already provides bundling, it's Simply by using bundles and referencing them in our scripts solves the issue. In ASP.NET we do it by registering our bundle in our Global.asax.cs file in the Application_Start method:
protected void Application_Start()
{
    // ...
    BundleConfig.RegisterBundles(BundleTable.Bundles);
    // ...
}
Where the RegisterBundles method, looks like:
public static void RegisterBundles(BundleCollection bundles)
{
    bundles.Add(new StyleBundle("~/Content/css")
        .Include("~/Content/images/icon-fonts/entypo/entypo.css")
        .Include("~/Content/site.css"));
}
Now, build and deploy your site. Just don't forget to remove debug=true on your web.config, compilation element:
<compilation targetFramework="4.7.2" />
Whenever that content changes or a new deploy happens, that querystring value for each bundle is automatically modified by the framework ensuring that users will always have your latest JavaScript, optimized, without risking serving an obsolete version to your users.

Conclusion

Connect with me:

Bruno Hildenbrand      
Software Engineer and open-source enthusiast.
.NET, Go, Linux, Vim, Cloud, Architecture, Docker & Kubernetes.