Kip
Kip

Reputation: 109443

How to force browsers to reload cached CSS and JS files?

I have noticed that some browsers (in particular, Firefox and Opera) are very zealous in using cached copies of .css and .js files, even between browser sessions. This leads to a problem when you update one of these files, but the user's browser keeps on using the cached copy.

What is the most elegant way of forcing the user's browser to reload the file when it has changed?

Ideally, the solution would not force the browser to reload the file on every visit to the page.


I have found John Millikin's and da5id's suggestion to be useful. It turns out there is a term for this: auto-versioning.

I have posted a new answer below which is a combination of my original solution and John's suggestion.

Another idea that was suggested by SCdF would be to append a bogus query string to the file. (Some Python code, to automatically use the timestamp as a bogus query string, was submitted by pi..)

However, there is some discussion as to whether or not the browser would cache a file with a query string. (Remember, we want the browser to cache the file and use it on future visits. We only want it to fetch the file again when it has changed.)

Upvotes: 1166

Views: 607347

Answers (30)

Amr Lotfy
Amr Lotfy

Reputation: 2997

Here is my Bash script-based cache busting solution:

  1. I assume you have CSS and JavaScript files referenced in your index.html file
  2. Add a timestamp as a parameter for .js and .css in index.html as below (one time only)
  3. Create a timestamp.txt file with the above timestamp.
  4. After any update to .css or .js file, just run the below .sh script

Sample index.html entries for .js and .css with a timestamp:

<link rel="stylesheet" href="bla_bla.css?v=my_timestamp">
<script src="scripts/bla_bla.js?v=my_timestamp"></script>

File timestamp.txt should only contain same timestamp 'my_timestamp' (will be searched for and replaced by script later on)

Finally here is the script (let's call it cache_buster.sh :D)

old_timestamp=$(cat timestamp.txt)
current_timestamp=$(date +%s)
sed -i -e "s/$old_timestamp/$current_timestamp/g" index.html
echo "$current_timestamp" >timestamp.txt

(Visual Studio Code users) you can put this script in a hook, so it gets called each time a file is saved in your workspace.

Upvotes: 0

Lloyd Banks
Lloyd Banks

Reputation: 36679

Here is a pure JavaScript solution

(function(){
    
    // Match this timestamp with the release of your code
    var lastVersioning = Date.UTC(2014, 11, 20, 2, 15, 10);
     
    var lastCacheDateTime = localStorage.getItem('lastCacheDatetime');
    
    if(lastCacheDateTime){
        if(lastVersioning > lastCacheDateTime){
            var reload = true;
        }
     }

    localStorage.setItem('lastCacheDatetime', Date.now());

    if(reload){
        location.reload(true);
    }
})();

The above will look for the last time the user visited your site. If the last visit was before you released new code, it uses location.reload(true) to force page refresh from server.

I usually have this as the very first script within the <head> so it's evaluated before any other content loads. If a reload needs to occurs, it's hardly noticeable to the user.

I am using local storage to store the last visit timestamp on the browser, but you can add cookies to the mix if you're looking to support older versions of IE.

Upvotes: 15

zyrup
zyrup

Reputation: 717

If you don't want the client to cache the file ever, this solution seems to be quickest to implement. Adjust the part with time() if you e.g. load the file in footer.php:

<script src="<?php echo get_template_directory_uri(); ?>/js/main.js?v=<?= time() ?>"></script>

Upvotes: 1

Phantom007
Phantom007

Reputation: 2249

I am not sure why you guys/gals are taking so much pain to implement this solution.

All you need to do if get the file's modified timestamp and append it as a querystring to the file.

In PHP I would do it as:

<link href="mycss.css?v=<?= filemtime('mycss.css') ?>" rel="stylesheet">

filemtime() is a PHP function that returns the file modified timestamp.

Upvotes: 103

user2305886
user2305886

Reputation: 786

I use the following solution for the described problem. Instead of supplying js and css as separate files, their content simply get inserted in a web page content. It can be done manually, or completely transparent and automatic. It's implemented using a feature of the proprietary framework I use for a page generation. However, a similar functionality should be supported by any good framework. For example:

.....
<script type="text/javascript" src="js/tinysort.js"></script>
<script type="text/javascript" src="js/webfolder.js"></script>
<script type="text/javascript" src="js/ui.js"></script>
<div style="padding-bottom:3px;padding-top:3px"><h3 style="display:inline">
....

Will look in the final web assembly as:

....
<script>
  // from tinysort.js 
  var TINY={};

  function T$(i){return document.getElementById(i)}
  function T$$(e,p){return p.getElementsByTagName(e)}
  ....
  // from webfolder.js
  function selectAll() {
   if (document.forms.folder.elements.files.length == undefined)
      document.forms.folder.elements.files.checked = document.forms.folder.elements.files.checked == false;
   else  
     for(var el=0 ...
  .....
</script>
<div style="padding-bottom:3px;padding-top:3px"><h3 style="display:inline">
....

You shouldn't worry that a web page becomes huge, all this content will be loaded by a browser anyway. Some parallelism in loading doesn't make sense when http 2 is used. As only parts of js or css code get completely debugged, they can be separated back. A version number gets propagated in the file name itself to avoid using old code. The above approach works well for SPA which I use most a recent time.

Upvotes: 0

Kip
Kip

Reputation: 109443

This solution is written in PHP, but it should be easily adapted to other languages.

The original .htaccess regex can cause problems with files like json-1.3.js. The solution is to only rewrite if there are exactly 10 digits at the end. (Because 10 digits covers all timestamps from 9/9/2001 to 11/20/2286.)

First, we use the following rewrite rule in .htaccess:

RewriteEngine on
RewriteRule ^(.*)\.[\d]{10}\.(css|js)$ $1.$2 [L]

Now, we write the following PHP function:

/**
 *  Given a file, i.e. /css/base.css, replaces it with a string containing the
 *  file's mtime, i.e. /css/base.1221534296.css.
 *
 *  @param $file  The file to be loaded. works on all type of paths.
 */
function auto_version($file) {
  if($file[0] !== '/') {
    $file = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', dirname($_SERVER['PHP_SELF'])), '/') . '/' . $file;
  }
  
  if (!file_exists($_SERVER['DOCUMENT_ROOT'] . $file))
  return $file;
  
  $mtime = filemtime($_SERVER['DOCUMENT_ROOT'] . $file);
  return preg_replace('{\\.([^./]+)$}', ".$mtime.\$1", $file);
}

Now, wherever you include your CSS, change it from this:

<link rel="stylesheet" href="/css/base.css" type="text/css" />

To this:

<link rel="stylesheet" href="<?php echo auto_version('/css/base.css'); ?>" type="text/css" />

This way, you never have to modify the link tag again, and the user will always see the latest CSS. The browser will be able to cache the CSS file, but when you make any changes to your CSS the browser will see this as a new URL, so it won't use the cached copy.

This can also work with images, favicons, and JavaScript. Basically anything that is not dynamically generated.

Upvotes: 496

user18889997
user18889997

Reputation:

location.reload(true)

Or use "Network" from the inspector ([CTRL] + [I]), click "disable cache", click trash icon, click "load"/"get"

Upvotes: 0

pi.
pi.

Reputation: 21582

I recently solved this using Python. Here is the code (it should be easy to adopt to other languages):

def import_tag(pattern, name, **kw):
    if name[0] == "/":
        name = name[1:]
    # Additional HTML attributes
    attrs = ' '.join(['%s="%s"' % item for item in kw.items()])
    try:
        # Get the files modification time
        mtime = os.stat(os.path.join('/documentroot', name)).st_mtime
        include = "%s?%d" % (name, mtime)
        # This is the same as sprintf(pattern, attrs, include) in other
        # languages
        return pattern % (attrs, include)
    except:
        # In case of error return the include without the added query
        # parameter.
        return pattern % (attrs, name)

def script(name, **kw):
    return import_tag('<script %s src="/%s"></script>', name, **kw)

def stylesheet(name, **kw):
    return import_tag('<link rel="stylesheet" type="text/css" %s href="/%s">', name, **kw)

This code basically appends the files time-stamp as a query parameter to the URL. The call of the following function

script("/main.css")

will result in

<link rel="stylesheet" type="text/css"  href="/main.css?1221842734">

The advantage of course is that you do never have to change your HTML content again, touching the CSS file will automatically trigger a cache invalidation. It works very well and the overhead is not noticeable.

Upvotes: 7

Drew Noakes
Drew Noakes

Reputation: 311255

I put an MD5 hash of the file's contents in its URL. That way I can set a very long expiration date, and don't have to worry about users having old JS or CSS.

I also calculate this once per file at runtime (or on file system changes) so there's nothing funny to do at design time or during the build process.

If you're using ASP.NET MVC then you can check out the code in my other answer here.

Upvotes: 2

raul7
raul7

Reputation: 201

I've solved this issue by using ETag:

ETag or entity tag is part of HTTP, the protocol for the World Wide Web. It is one of several mechanisms that HTTP provides for Web cache validation, which allows a client to make conditional requests. This allows caches to be more efficient and saves bandwidth, as a Web server does not need to send a full response if the content has not changed. ETags can also be used for optimistic concurrency control,1 as a way to help prevent simultaneous updates of a resource from overwriting each other.

  • I am running a Single-Page Application (written in Vue.JS).
  • The output of the application is built by npm, and is stored as dist folder (the important file is: dist/static/js/app.my_rand.js)
  • Nginx is responsible of serving the content in this dist folder, and it generates a new Etag value, which is some kind of a fingerprint, based on the modification time and the content of the dist folder. Thus when the resource changes, a new Etag value is generated.
  • When the browser requests the resource, a comparison between the request headers and the stored Etag, can determine if the two representations of the resource are the same, and could be served from cache or a new response with a new Etag needs to be served.

Upvotes: 0

Frank Bryce
Frank Bryce

Reputation: 8446

For my development, I find that Chrome has a great solution.

https://superuser.com/a/512833

With developer tools open, simply long click the refresh button and let go once you hover over "Empty Cache and Hard Reload".

This is my best friend, and is a super lightweight way to get what you want!

Upvotes: 9

Jayee
Jayee

Reputation: 562

In ASP.NET Core you could achieve this by adding 'asp-append-version':

<link rel="stylesheet" href="~/css/xxx.css" asp-append-version="true" />

 <script src="~/js/xxx.js" asp-append-version="true"></script>

It will generate HTML:

<link rel="stylesheet" href="/css/xxx.css?v=rwgRWCjxemznsx7wgNx5PbMO1EictA4Dd0SjiW0S90g" />

The framework will generate a new version number every time you update the file.

Upvotes: 1

loretoparisi
loretoparisi

Reputation: 16301

A simple solution for static files (just for development purposes) that adds a random version number to the script URI, using script tag injections

<script>
    var script = document.createElement('script');
    script.src = "js/app.js?v=" + Math.random();
    document.getElementsByTagName('head')[0].appendChild(script);
</script>

Upvotes: 1

Nicolai VdS
Nicolai VdS

Reputation: 13

We have one solution with some different way for implementation. We use the above solution for it.

datatables?v=1

We can handle the version of the file. It means that every time that we change our file, change the version of it too. But it's not a suitable way.

Another way used a GUID. It wasn't suitable either, because each time it fetches the file and doesn't use from the browser cache.

datatables?v=Guid.NewGuid()

The last way that is the best way is:

When a file change occurs, change the version too. Check the follow code:

<script src="~/scripts/[email protected](Server.MapPath("/scripts/main.js")).ToString("yyyyMMddHHmmss")"></script>

By this way, when you change the file, LastWriteTime change too, so the version of the file will change and in the next when you open the browser, it detects a new file and fetch it.

Upvotes: 0

AIon
AIon

Reputation: 13081

For developers with this problem while developing and testing:

Remove caching briefly.

"keep caching consistent with the file" .. it's way too much hassle ..

Generally speaking, I don't mind loading more - even loading again files which did not change - on most projects - is practically irrelevant. While developing an application - we are mostly loading from disk, on localhost:port - so this increase in network traffic issue is not a deal breaking issue.

Most small projects are just playing around - they never end-up in production. So for them you don't need anything more...

As such if you use Chrome DevTools, you can follow this disable-caching approach like in the image below:

How to force chrome to reload cached files

And if you have Firefox caching issues:

How to force asset reload on Firefox

How to disable caching in Firefox while in development

Do this only in development. You also need a mechanism to force reload for production, since your users will use old cache invalidated modules if you update your application frequently and you don't provide a dedicated cache synchronisation mechanism like the ones described in the answers above.

Yes, this information is already in previous answers, but I still needed to do a Google search to find it.

Upvotes: 5

patrick
patrick

Reputation: 16979

For development: use a browser setting: for example, Chrome network tab has a disable cache option.

For production: append a unique query parameter to the request (for example, q?Date.now()) with a server-side rendering framework or pure JavaScript code.

// Pure JavaScript unique query parameter generation
//
//=== myfile.js

function hello() { console.log('hello') };

//=== end of file

<script type="text/javascript">
    document.write('<script type="text/javascript" src="myfile.js?q=' + Date.now() + '">
    // document.write is considered bad practice!
    // We can't use hello() yet
</script>')

<script type="text/javascript">
    hello();
</script>

Upvotes: 5

Jessie Lesbian
Jessie Lesbian

Reputation: 1526

You can use SRI to break the browser cache. You only have to update your index.html file with the new SRI hash every time. When the browser loads the HTML and finds out the SRI hash on the HTML page didn't match that of the cached version of the resource, it will reload your resource from your servers. It also comes with a good side effect of bypassing cross-origin read blocking.

<script src="https://jessietessie.github.io/google-translate-token-generator/google_translate_token_generator.js" integrity="sha384-muTMBCWlaLhgTXLmflAEQVaaGwxYe1DYIf2fGdRkaAQeb4Usma/kqRWFWErr2BSi" crossorigin="anonymous"></script>

Upvotes: 3

Bangash
Bangash

Reputation: 127

One of the best and quickest approaches I know is to change the name of the folder where you have CSS or JavaScript files.

Or for developers: Change the name of your CSS and JavaScript files something like versions.

<link rel="stylesheet" href="cssfolder/somecssfile-ver-1.css"/>

Do the same for your JavaScript files.

Upvotes: 2

Jajikanth pydimarla
Jajikanth pydimarla

Reputation: 1570

Small improvement from existing answers...

Using a random number or session id would cause it to reload on each request. Ideally, we may need to change only if some code changes were done in any JavaScript or CSS file.

When using a common JSP file as a template to many other JSP and JavaScript files, add the below in a common JSP file

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<c:set var = "version" scope = "application" value = "1.0.0" />

Now use the above variable in all locations as below in your JavaScript file inclusions.

<script src='<spring:url value="/js/myChangedFile.js?version=${version}"/>'></script>

Advantages:

  1. This approach will help you in changing version number at one location only.
  1. Maintaining a proper version number (usually build/release number) will help you to check/verify your code changes being deployed properly (from developer console of the browser).

Another useful tip:

If you are using the Chrome browser, you can disable caching when Dev Tools is open. In Chrome, hit F12F1 and scroll to SettingsPreferencesNetwork → *Disable caching (while DevTools is open)

Chrome DevTools

Upvotes: 1

Luis Cambust&#243;n
Luis Cambust&#243;n

Reputation: 41

Just use server-side code to add the date of the file... that way it will be cached and only reloaded when the file changes.

In ASP.NET:

<link rel="stylesheet" href="~/css/custom.css?d=@(System.Text.RegularExpressions.Regex.Replace(File.GetLastWriteTime(Server.MapPath("~/css/custom.css")).ToString(),"[^0-9]", ""))" />

<script type="text/javascript" src="~/js/custom.js?d=@(System.Text.RegularExpressions.Regex.Replace(File.GetLastWriteTime(Server.MapPath("~/js/custom.js")).ToString(),"[^0-9]", ""))"></script>

This can be simplified to:

<script src="<%= Page.ResolveClientUrlUnique("~/js/custom.js") %>" type="text/javascript"></script>

By adding an extension method to your project to extend Page:

public static class Extension_Methods
{
    public static string ResolveClientUrlUnique(this System.Web.UI.Page oPg, string sRelPath)
    {
        string sFilePath = oPg.Server.MapPath(sRelPath);
        string sLastDate = System.IO.File.GetLastWriteTime(sFilePath).ToString();
        string sDateHashed = System.Text.RegularExpressions.Regex.Replace(sLastDate, "[^0-9]", "");

        return oPg.ResolveClientUrl(sRelPath) + "?d=" + sDateHashed;
    }
}

Upvotes: 4

Pawel
Pawel

Reputation: 18252

Disable caching of script.js only for local development in pure JavaScript.

It injects a random script.js?wizardry=1231234 and blocks regular script.js:

<script type="text/javascript">
  if(document.location.href.indexOf('localhost') !== -1) {
    const scr = document.createElement('script');
    document.setAttribute('type', 'text/javascript');
    document.setAttribute('src', 'scripts.js' + '?wizardry=' + Math.random());
    document.head.appendChild(scr);
    document.write('<script type="application/x-suppress">'); // prevent next script(from other SO answer)
  }
</script>

<script type="text/javascript" src="scripts.js">

Upvotes: 2

ajithes1
ajithes1

Reputation: 427

Google Chrome has the Hard Reload as well as the Empty Cache and Hard Reload option. You can click and hold the reload button (in Inspect Mode) to select one.

Upvotes: 8

Karan Shaw
Karan Shaw

Reputation: 1406

Simply add this code where you want to do a hard reload (force the browser to reload cached CSS and JavaScript files):

$(window).load(function() {
    location.reload(true);
});

Do this inside the .load, so it does not refresh like a loop.

Upvotes: 5

Kamil Kiełczewski
Kamil Kiełczewski

Reputation: 92627

In Laravel (PHP) we can do it in the following clear and elegant way (using file modification timestamp):

<script src="{{ asset('/js/your.js?v='.filemtime('js/your.js')) }}"></script>

And similar for CSS

<link rel="stylesheet" href="{{asset('css/your.css?v='.filemtime('css/your.css'))}}">

Example HTML output (filemtime return time as as a Unix timestamp)

<link rel="stylesheet" href="assets/css/your.css?v=1577772366">

Upvotes: 18

readikus
readikus

Reputation: 366

If you're using Git and PHP, you can reload the script from the cache each time there is a change in the Git repository, using the following code:

exec('git rev-parse --verify HEAD 2> /dev/null', $gitLog);
echo '  <script src="/path/to/script.js"?v='.$gitLog[0].'></script>'.PHP_EOL;

Upvotes: 5

Mizo Games
Mizo Games

Reputation: 189

Well, I have made it work my way by changing the JavaScript file version each time the page loads by adding a random number to JavaScript file version as follows:

// Add it to the top of the page
<?php
    srand();
    $random_number = rand();
?>

Then apply the random number to the JavaScript version as follow:

<script src="file.js?version=<?php echo $random_number;?>"></script>

Upvotes: 0

commonpike
commonpike

Reputation: 11185

It seems all answers here suggest some sort of versioning in the naming scheme, which has its downsides.

Browsers should be well aware of what to cache and what not to cache by reading the web server's response, in particular the HTTP headers - for how long is this resource valid? Was this resource updated since I last retrieved it? etc.

If things are configured 'correctly', just updating the files of your application should (at some point) refresh the browser's caches. You can for example configure your web server to tell the browser to never cache files (which is a bad idea).

A more in-depth explanation of how that works is in How Web Caches Work.

Upvotes: 4

GreQ
GreQ

Reputation: 999

I have not found the client-side DOM approach creating the script node (or CSS) element dynamically:

<script>
    var node = document.createElement("script");
    node.type = "text/javascript";
    node.src = 'test.js?' + Math.floor(Math.random()*999999999);
    document.getElementsByTagName("head")[0].appendChild(node);
</script>

Upvotes: 8

statler
statler

Reputation: 1381

I came to this question when looking for a solution for my SPA, which only has a single index.html file listing all the necessary files. While I got some leads that helped me, I could not find a quick-and-easy solution.

In the end, I wrote a quick page (including all of the code) necessary to autoversion an HTML/JavaScript index.html file as part of the publishing process. It works perfectly and only updates new files based on date last modified.

You can see my post at Autoversion your SPA index.html. There is a stand-alone Windows application there too.

The guts of the code is:

private void ParseIndex(string inFile, string addPath, string outFile)
{
    string path = Path.GetDirectoryName(inFile);
    HtmlAgilityPack.HtmlDocument document = new HtmlAgilityPack.HtmlDocument();
    document.Load(inFile);

    foreach (HtmlNode link in document.DocumentNode.Descendants("script"))
    {
        if (link.Attributes["src"]!=null)
        {
            resetQueryString(path, addPath, link, "src");
        }
    }

    foreach (HtmlNode link in document.DocumentNode.Descendants("link"))
    {
        if (link.Attributes["href"] != null && link.Attributes["type"] != null)
        {
            if (link.Attributes["type"].Value == "text/css" || link.Attributes["type"].Value == "text/html")
            {
                resetQueryString(path, addPath, link, "href");
            }
        }
    }

    document.Save(outFile);
    MessageBox.Show("Your file has been processed.", "Autoversion complete");
}

private void resetQueryString(string path, string addPath, HtmlNode link, string attrType)
{
    string currFileName = link.Attributes[attrType].Value;

    string uripath = currFileName;
    if (currFileName.Contains('?'))
        uripath = currFileName.Substring(0, currFileName.IndexOf('?'));
    string baseFile = Path.Combine(path, uripath);
    if (!File.Exists(baseFile))
        baseFile = Path.Combine(addPath, uripath);
    if (!File.Exists(baseFile))
        return;
    DateTime lastModified = System.IO.File.GetLastWriteTime(baseFile);
    link.Attributes[attrType].Value = uripath + "?v=" + lastModified.ToString("yyyyMMddhhmm");
}

Upvotes: 1

Michael Kropat
Michael Kropat

Reputation: 15227

The 30 or so existing answers are great advice for a circa 2008 website. However, when it comes to a modern, single-page application (SPA), it might be time to rethink some fundamental assumptions… specifically the idea that it is desirable for the web server to serve only the single, most recent version of a file.

Imagine you're a user that has version M of a SPA loaded into your browser:

  1. Your CD pipeline deploys the new version N of the application onto the server
  2. You navigate within the SPA, which sends an XMLHttpRequest (XHR) to the server to get /some.template
  • (Your browser hasn't refreshed the page, so you're still running version M)
  1. The server responds with the contents of /some.template — do you want it to return version M or N of the template?

If the format of /some.template changed between versions M and N (or the file was renamed or whatever) you probably don't want version N of the template sent to the browser that's running the old version M of the parser.†

Web applications run into this issue when two conditions are met:

  • Resources are requested asynchronously some time after the initial page load
  • The application logic assumes things (that may change in future versions) about resource content

Once your application needs to serve up multiple versions in parallel, solving caching and "reloading" becomes trivial:

  1. Install all site files into versioned directories: /v<release_tag_1>/…files…, /v<release_tag_2>/…files…
  2. Set HTTP headers to let browsers cache files forever
  • (Or better yet, put everything in a CDN)
  1. Update all <script> and <link> tags, etc. to point to that file in one of the versioned directories

That last step sounds tricky, as it could require calling a URL builder for every URL in your server-side or client-side code. Or you could just make clever use of the <base> tag and change the current version in one place.

† One way around this is to be aggressive about forcing the browser to reload everything when a new version is released. But for the sake of letting any in-progress operations to complete, it may still be easiest to support at least two versions in parallel: v-current and v-previous.

Upvotes: 34

Related Questions