Monday, November 26, 2018

JavaScript caching best practices with Asp.Net

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.

As a result, when visiting the page the regular user see stale content. The only way to get those would be clearing the browser’s cache or forcing a full refresh (Ctrl+F5 / Ctrl-Shift-R), solutions that most users ignore.

Why does it happen and how can we fix it? Let´s take a look.

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.

For example, here’s how StackOverflow, does it:

Note in red above the query string suffixed to the icon. 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 review two scenarios - a page serving bundled content and a page showing unbundled content - and 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. Note that the request below was screenshot using Chrome Developer Tools:
Where:
  • 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 I expand the bundle request, we can see that the cache is set to 1 year from the request date. Or, that file won't be refreshed for the next 365 days:

Serving unbundled content

Now let's take a look at a request to a page that has unbundled content:

We can see in the example above that:
  • The resources are also being cached (column Size)
  • However, individual requests are issued for each file referenced. There are two problems there:
  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). The problem is that updates on those files won’t be automatically refreshed.
Let's take a look at how to address these problems using Asp.Net's bundling mechanism.

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.

See Also

For more Asp.Net posts on this blog, please click here

References

Microsoft Docs - Bundling and Minification