Performance Research: Browser Cache Usage - Exposed!

Michael Schwarz on Friday, January 5, 2007

Tenni Theurer posted a new blog entry on the YUIBlog [1] about performance research with browser cache usage [2].

In an earlier post, I described What the 80/20 Rule Tells Us about Reducing HTTP Requests. Since browsers spend 80% of the time fetching external components including scripts, stylesheets and images, reducing the number of HTTP requests has the biggest impact on reducing response time. But shouldn’t everything be saved in the browser’s cache anyway? [...]

40-60% of Yahoo!’s users have an empty cache experience and ~20% of all page views are done with an empty cache. To my knowledge, there’s no other research that shows this kind of information. And I don’t know about you, but these results came to us as a big surprise. It says that even if your assets are optimized for maximum caching, there are a significant number of users that will always have an empty cache. This goes back to the earlier point that reducing the number of HTTP requests has the biggest impact on reducing response time. The percentage of users with an empty cache for different web pages may vary, especially for pages with a high number of active (daily) users. However, we found in our study that regardless of usage patterns, the percentage of page views with an empty cache is always ~20%.

Conclusion: Keep in mind the empty cache user experience. It might be more prevalent than you think!

As you may read on my blog I have done (little different) experiments [1 [3]] [2 [4]] in the past and did came to a very similar result, that you should have to merge files whenever possible. Below you will find some of my tips to get web sites running faster.

<li>For mouseover images or images that have several states use only one image and re-position the image using CSS (background-position, background-repeat). <li>Core JavaScript files can be combined to one big file. If you generate the JavaScript files on-the-fly add the ETag and Modified http header values to the response and check them when generating the output (if you have to return http status 304 or 200). <li>The same does work for CSS files, of course. <li>Try to do not include JavaScript files in the html output. Use wherever you can external JavaScript files. <li>Don't load live data using external files or AJAX after window.onload when they are already ready while processing the main page, simple include them to save an http request.</li> Last year I build something like a ScriptMerger module for ASP.NET. You can simple generate a big file from several static JavaScript or CSS files (note: this is only the main part of the httpHandler):

<style> <!-- .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /white-space: pre;/ }

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #006080; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; }

.csharpcode .lnum { color: #606060; }

--> </style> <pre class="csharpcode"><span class="kwrd">public</span> <span class="kwrd">void</span> ProcessRequest (HttpContext context) { <span class="kwrd"> string</span> etag = context.Request.Headers[<span class="str">"If-None-Match"</span>]; <span class="kwrd">string</span> modSince = context.Request.Headers[<span class="str">"If-Modified-Since"</span>];

<span class="kwrd">string</span>[] files = context.Request[<span class="str">"path"</span>].Split (<span class="str">','</span>);

DateTime lastChange = DateTime.MinValue;

<span class="kwrd">foreach</span> (<span class="kwrd">string</span> file <span class="kwrd">in</span> files) { <span class="kwrd">if</span> (file.Length &lt; 3) <span class="kwrd">continue</span>;

<span class="kwrd">if</span> (!File.Exists (context.Server.MapPath (<span class="str">"~/"</span> + file))) <span class="kwrd">continue</span>;

FileInfo fi = <span class="kwrd">new</span> FileInfo (context.Server.MapPath (<span class="str">"~/"</span> + file)); <span class="kwrd">if</span> (fi.LastWriteTimeUtc &gt; lastChange) lastChange = fi.LastWriteTimeUtc; }

<span class="kwrd">if</span> (modSince != <span class="kwrd">null</span>) { <span class="kwrd">try</span> { DateTime modSinced = Convert.ToDateTime (modSince.ToString ()).ToUniversalTime (); <span class="kwrd">if</span> (modSinced.Ticks / 10000000 == lastChange.Ticks / 10000000) { context.Response.StatusCode = 304; <span class="kwrd">return</span>; } } <span class="kwrd">catch</span> (Exception) { } }

context.Response.ContentType = <span class="str">"text/plain"</span>;

<span class="kwrd">if</span>(!String.IsNullOrEmpty(context.Request[<span class="str">"type"</span>])) context.Response.ContentType = context.Request[<span class="str">"type"</span>].ToLower();

context.Response.Cache.SetLastModified (lastChange); context.Response.Cache.SetCacheability (System.Web.HttpCacheability.Public);

<span class="kwrd">string</span> content;

<span class="kwrd">foreach</span> (<span class="kwrd">string</span> file <span class="kwrd">in</span> files) { <span class="kwrd">if</span> (file.Length &lt; 3) <span class="kwrd">continue</span>;

<span class="kwrd">if</span> (!File.Exists (context.Server.MapPath (<span class="str">"~/"</span> + file))) <span class="kwrd">continue</span>;

TextReader tr = File.OpenText (context.Server.MapPath (<span class="str">"~/"</span> + file)); content = tr.ReadToEnd (); tr.Close ();

<span class="rem">//context.Response.Write ("// " + file + "\r\n\r\n");</span> context.Response.Write (content); context.Response.Write (<span class="str">"\r\n"</span>); } } </pre>