When you update javascript or css files that are already cached in users’ browsers, most likely many users won’t get that for some time because of the caching at the browser or intermediate proxy(s). You need some way to force browser and proxy(s) to download latest files. There’s no way to do that effectively across all browsers and proxies from the webserver by manipulating cache headers unless you change the file name or you change the URL of the files by introducing some unique query string so that browsers/proxies interpret them as new files. Most web developers use the query string approach and use a version suffix to send the new file to the browser. For example,
<script src="someJs.js?v=1001" ></script> <link href="someCss.css?v=2001"></link>
In order to do this, developers have to go to all the html, aspx, ascx, master pages, find all references to static files that are changed, and then increase the version number. If you forget to do this on some page, that page may break because browser uses old cached script. So, it requires a lot of regression test effort to find out whether changing some css or js breaks something anywhere in the entire website.
Another approach is to run some build script that scans all files and updates the reference to the javascript and css files in each and every page in the website. But this approach does not work on dynamic pages where the javascript and css references are added at run-time, say using ScriptManager
.
If you have no way to know what javascript and css will get added to the page at run-time, the only option is to analyze the page output at runtime and then change the javascript, css references on the fly.
Here’s an HttpFilter
that can do that for you. This filter intercepts any ASPX hit and then it automatically appends the last modification date time of javascript and css files inside the emitted html. It does so without storing the whole generated html in memory nor doing any string operation because that will cause high memory and CPU consumption on webserver under high load. The code works with character buffers and response streams directly so that it’s as fast as possible. I have done enough load test to ensure even if you hit an aspx page million times per hour, it won’t add more than 50ms delay over each page response time.
First, you add set the filter called StaticContentFilter
in the Global.asax
file’s Application_BeginRequest
event handler:
Response.Filter = new Dropthings.Web.Util.StaticContentFilter( Response, relativePath => { if (Context.Cache[physicalPath] == null) { var physicalPath = Server.MapPath(relativePath); var version = "?v=" + new System.IO.FileInfo(physicalPath).LastWriteTime .ToString("yyyyMMddhhmmss"); Context.Cache.Add(physicalPath, version, null, DateTime.Now.AddMinutes(1), TimeSpan.Zero, CacheItemPriority.Normal, null); Context.Cache[physicalPath] = version; return version; } else { return Context.Cache[physicalPath] as string; } }, "http://images.mydomain.com/", "http://scripts.mydomain.com/", "http://styles.mydomain.com/", baseUrl, applicationPath, folderPath); }
The only tricky part here is the delegate that is fired whenever the filter detects a script or css link and it asks you to return the version for the file. Whatever you return gets appended right after the original URL of the script or css. So, here the delegate is producing the version as “?v=yyyyMMddhhmmss” using the file’s last modified date time. It’s also caching the version for the file to make sure it does not make a File I/O request on each and every page view in order to get the file’s last modified date time.
For example, the following scripts and css in the html snippet:
<script type="text/javascript" src="scripts/jquery-1.4.1.min.js" ></script> <script type="text/javascript" src="scripts/TestScript.js" ></script> <link href="Styles/Stylesheet.css" rel="stylesheet" type="text/css" />
It will get emitted as:
<script type="text/javascript" src="scripts/jquery-1.4.1.min.js?v=20100319021342" ></script> <script type="text/javascript" src="scripts/TestScript.js?v=20110522074353" ></script> <link href="Styles/Stylesheet.css?v=20110522074829" rel="stylesheet" type="text/css" />
As you see there’s a query string generated with each of the file’s last modified date time. Good thing is you don’t have to worry about generating a sequential version number after changing a file. it will take the last modified date, which will change only when a file is changed.
The HttpFilter
I will show you here can not only append version suffix, it can also prepend anything you want to add on image, css and link URLs. You can use this feature to load images from a different domain, or load scripts from a different domain and benefit from the parallel loading feature of browsers and increase the page load performance. For example, the following tags can have any URL prepended to them:
<script src="some.js" ></script> <link href="some.css" /> <img src="some.png" />
They can be emitted as:
<script src="http://javascripts.mydomain.com/some.js" ></script> <link href="http://styles.mydomain.com/some.css" /> <img src="http://images.mydomain.com/some.png" />
Loading javascripts, css and images from different domain can significantly improve your page load time since browsers can load only two files from a domain at a time. If you load javascripts, css and images from different subdomains and the page itself on www subdomain, you can load 8 files in parallel instead of only 2 files in parallel.
Read here to learn how this works:
http://www.codeproject.com/KB/aspnet/autojscssversion.aspx
Appreciate your feedback.
It is a great article. Nicely organized.
Ray Akkanson
Omar,
His approach was very well implemented.
I’d love to put that in my application, but my CSS and Js are hosted on another server.
Do you see any way out for me?
You need to use another approach to get the version number then since you won’t be able to read the last modification date of the files from another server. Maybe you can create an xml file that contains the list of all the JS, CSS files relative path and the last modified date and then read from the xml file what’s the last modified date of a certain relative path. You update the xml file whenever you change JS, CSS on another server.
Omar,
Thanks.
I will try to schedule something for that.
Another thing:
You could change the way you access the cache to avoid errors in the cache access.
See explanation in “Scott Cate Weblog”
http://scottcate.mykb.com/Article_5CB26.aspx
/* ——————— Changing this code ——————— */
if (Context.Cache[relativePath] == null)
{
var physicalPath = Server.MapPath(relativePath);
var version = “?v=” + new System.IO.FileInfo(physicalPath).LastWriteTime.ToString(“yyyyMMddhhmmss”);
Context.Cache.Add(relativePath, version, null,DateTime.Now.AddMinutes(1), TimeSpan.Zero,CacheItemPriority.Normal, null);
return version;
}
else
{
return Context.Cache[relativePath] as string;
}
/* ——————— For this——————— */
string relativePathCache = Context.Cache[relativePath] as string;
if (relativePathCache == null)
{
var physicalPath = Server.MapPath(relativePath);
var version = “?v=” + new System.IO.FileInfo(physicalPath).LastWriteTime.ToString(“yyyyMMddhhmmss”);
Context.Cache.Add(relativePath, version, null,DateTime.Now.AddMinutes(1), TimeSpan.Zero,CacheItemPriority.Normal, null);
return version;
}
else
{
return relativePathCache;
}
Omar,
You can also place reliance on linked directly to the cache file. Thus, the cache will only expire when the contents of the file changes.
Sincerely,
Caique Dourado
Are their really need to write this code. i use squishit who manage many thing for css or js file and i never need to care about those thing
I recently wrote a similar post about how I combine, minify, compress and auto-generate file names for CSS to be able to get around the caching problem and make it as efficient as possible http://guyellisrocks.com/coding/optimizing-css-in-asp-net-mvc/
I also ran into this recently.
I want to point out that this might cause problems if you have query string parameters on your javascript or css files already. Imagine for instance a tracking script . This would become . I have no idea how common this is.
I wonder if you could do something similar for .css files that do @include or have url(…) values?
How can i apply this on an asp.net mvc 2 application?
Omar,
I like the knowledge you and it is very helpful.
Thanks
Mark
Great Article Omar.
Thanks
Ray Akkanson
i have one problem with viewstate it replace the viewstate id on runtime (press F12 of browser the viewstate look like this )
Nice One Omar
But i need to know how to implement resolve url() while returning relative path dynamically…
for example
Right Now Output Scripts/javascript.js
Needed Output ../cripts/javascript.js
for example
Right Now Output Scripts/javascript.js
Needed Output ../../Scripts/javascript.js
Hi This approach is working perfect for me when i am running the website through code. But when i publish the website then the webpage does not recognize the js and css files which i have versioned. Do i need to do something additional while publishing it?