how to avoid browser caching .js and css ?

1,323 views
Skip to first unread message

yakafokon

unread,
Mar 2, 2011, 11:47:10 AM3/2/11
to play-framework
Hi all,

We have made a release of our web site and we are facing pbs with our
users because they have a old version of our .js and css files located
in the play! public directory. So they have to manualy force a refresh
of the page to have the last js and css file version.

How do you handle this issue ?
(we have an idea to fixe the pb but first I would like to keep the
question open to have fresh ideas from all)

Thanks all
:-)

JB

Arnaud Rolly

unread,
Mar 2, 2011, 12:14:21 PM3/2/11
to play-fr...@googlegroups.com
You can version the js and css by including a release number (maybe extracted from application.conf)  in their file names.

2011/3/2 yakafokon <jeanbaptist...@gmail.com>

--
You received this message because you are subscribed to the Google Groups "play-framework" group.
To post to this group, send email to play-fr...@googlegroups.com.
To unsubscribe from this group, send email to play-framewor...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/play-framework?hl=en.


davin

unread,
Mar 2, 2011, 12:39:07 PM3/2/11
to play-framework
Check out http://stackoverflow.com/questions/118884/what-is-an-elegant-way-to-force-browsers-to-reload-cached-css-js-files
You could implement auto versioning.
Shouldn't be too difficult, and maybe should be a feature request for
the framework.

There are some alternate solutions there, some of which are more
guaranteed to work, some of which are less across the board, although
as a short term solution, depending on your user base, maybe the
file.css?v=2 might work. Interesting reading in any case.


On Mar 2, 6:47 pm, yakafokon <jeanbaptiste.claramo...@gmail.com>
wrote:

JeanBaptiste

unread,
Mar 3, 2011, 9:47:30 AM3/3/11
to play-fr...@googlegroups.com
I have quickly written a FastTag.

This allow to write  :

<link rel="stylesheet" type="text/css" href="#{beansight.resource name:'/public/stylesheets/style.css' /}" />

and would result in this
<link rel="stylesheet" type="text/css" href="/public/stylesheets/style.css?lwjUNlPI/QG/bszU0xo4EA==" />
I get the has

What do you think ?

@FastTags.Namespace("beansight")
public class Tags extends FastTags {
   
    private static ConcurrentHashMap<String, String> resourceMap = new ConcurrentHashMap<String, String>();
   
    @SuppressWarnings("unchecked")
    public static void _resource(Map<?, ?> args, Closure body, PrintWriter out, ExecutableTemplate template, int fromLine) {
        String resourceName = args.get("name").toString();
       
        if ( !resourceMap.containsKey(resourceName) ) {
            byte[] content = IO.readContent(Play.getFile(resourceName));
            resourceMap.putIfAbsent(resourceName, resourceName + "?" + hash(content));
        }
        out.print(resourceMap.get(resourceName));
    }
   

    public static String hash(byte[] input) {
        try {
            MessageDigest m = MessageDigest.getInstance("MD5");
            byte[] out = m.digest(input);
            return new String(Base64.encodeBase64(out));
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }
   
}

Guillaume Bort

unread,
Mar 3, 2011, 4:38:41 PM3/3/11
to play-fr...@googlegroups.com
That's a cool implementation. We should probably use it as standard
for the @{...} notation.

> --
> You received this message because you are subscribed to the Google Groups
> "play-framework" group.
> To post to this group, send email to play-fr...@googlegroups.com.
> To unsubscribe from this group, send email to
> play-framewor...@googlegroups.com.
> For more options, visit this group at
> http://groups.google.com/group/play-framework?hl=en.
>

--
Guillaume Bort, http://guillaume.bort.fr

For anything work-related, use g...@zenexity.fr; for everything else,
write guillau...@gmail.com

Ike

unread,
Mar 3, 2011, 5:52:50 PM3/3/11
to play-fr...@googlegroups.com
I agree, but I would prefer if it's optional, maybe with a parameter like 'versioning: true'. And only for static resources?

Guillaume Bort

unread,
Mar 4, 2011, 3:21:37 AM3/4/11
to play-fr...@googlegroups.com
Only for static resources of course.

On Thu, Mar 3, 2011 at 11:52 PM, Ike <ike...@gmail.com> wrote:
> I agree, but I would prefer if it's optional, maybe with a parameter like
> 'versioning: true'. And only for static resources?
>

JeanBaptiste

unread,
Mar 4, 2011, 5:27:39 AM3/4/11
to play-fr...@googlegroups.com
thanks, of course the code can be use freely

What do you think about the use of the ConcurrentHashMap ?

JB
www.beansight.com

Dirk

unread,
Mar 4, 2011, 8:16:14 AM3/4/11
to play-fr...@googlegroups.com
Would it be possible to use a combination of file path and last modified date instead of reading the whole file into memory and producing a hash? It might be quicker for large js/css files

JeanBaptiste

unread,
Mar 4, 2011, 8:53:26 AM3/4/11
to play-fr...@googlegroups.com
At first I thought to do it this way but  last modified date could be the date of copying the files on another file system.
And finally loading the whole file is only done once for the first request.

JB
www.beansight.com

Ike

unread,
Mar 4, 2011, 6:10:48 PM3/4/11
to play-fr...@googlegroups.com
You mean once per application instance/run. Upon server restart these files will need to be rescanned. That's why I think it should be optional, so we can control which files really need versioning (e.g. JS, CSS, etc.). Most applications probably don't need to do versioning for most static files and rescanning all is not very desirable.

Ricardo Nascimento

unread,
Mar 5, 2011, 1:55:43 AM3/5/11
to play-fr...@googlegroups.com
Hello,

Why not just use routes file : 

GET     /public/                                staticDir:public

and when you change static files : 

GET     /public-v2/                                staticDir:public

Guillaume Bort

unread,
Mar 5, 2011, 5:13:26 AM3/5/11
to play-fr...@googlegroups.com
That's also a very good solution.

> --
> You received this message because you are subscribed to the Google Groups
> "play-framework" group.
> To post to this group, send email to play-fr...@googlegroups.com.
> To unsubscribe from this group, send email to
> play-framewor...@googlegroups.com.
> For more options, visit this group at
> http://groups.google.com/group/play-framework?hl=en.
>

--

Steren

unread,
Mar 6, 2011, 8:41:25 AM3/6/11
to play-fr...@googlegroups.com, Ricardo Nascimento
This solution works great, but the idea is to not bother about this. It is something that needs to be done 100% automatically to stay in the Play! philosophy, I think.
I personnaly don't want to have to think about creating a new route everytime I do a release.

Moreover, I don't want all the files to be reloaded by the client. If only a .js needs to be reloaded, I don't want the client to reload every CSS and images. So a global route is not optimal I think.

Steren
http://www.beansight.com

On Sat, Mar 5, 2011 at 7:55 AM, Ricardo Nascimento <ricardo...@gmail.com> wrote:

--

ogregras

unread,
Dec 19, 2011, 6:41:23 PM12/19/11
to play-fr...@googlegroups.com
Here's my solution. It's based on from Ricardo's idea in this thread (thanks!).

My first try was to use this route:

====================
GET /public__contentbuster__                    staticDir:public
==================== 

and to have a deployment script which would automatically replace the" __contentbuster__" token by a random string for each new deployment. It was working great but I din't really like the idea to depend on this deployment script.

But today I was looking at Play documentation and realized I had never worked with what the doc calls "Variables and scripts". After a couple of tries I came up with this:

====================
%{ contentBuster = System.currentTimeMillis() }%
GET /public_cb${contentBuster}                          staticDir:public
GET /public_cb${contentBuster}/                         staticDir:public
GET /public                              staticDir:public
GET /public/                              staticDir:public
==================== 

It seems to work great! The routes without the ${contentBuster} token are for resources that have an harcoded path (not reverse routed).

A drawback I see in this method is that the cache is busted every time Play's server is restarted, not each time a new version is deployed. Also all resources under /public (that are reverse routed) are cache busted. I can live with that!







ogregras

unread,
Dec 19, 2011, 7:15:18 PM12/19/11
to play-fr...@googlegroups.com
Hummm.... Play doesn't seem to like my idea in DEV mode, when reloading classes. Some errors occure, paths are not good anymore, etc.

I'll try to find something better.

jhice

unread,
Dec 19, 2011, 7:41:36 PM12/19/11
to play-fr...@googlegroups.com
A simple idea not far from the apache/rewrite solution :

Add the timestamp of the concerned files after them :

js/myscript.js?1323112478 <=> js/myscript.js?${jsTimestamp}
css/layout.css?1324299306

Where jsTimestamp = path + "myscript.js".lastModified().toTimestamp(); // <= not Java syntax... but you've got the idea ^^
Put this somewhere in your application controller, so that when a file is modified, the timestamp change automatically.

The "counter-part" is that this is always executed (perhaps execute this only in DEV mode ?)

ogregras

unread,
Dec 19, 2011, 7:58:52 PM12/19/11
to play-fr...@googlegroups.com
I think I finally got it!

The ${contentBuster} token shouldn't change when Play reloads classes. So I made a plugin (AppBootstrapPlugin), which hooks on "onLoad()" to initialize a static "routeCacheBuster" variable to System.currentTimeMillis() inside itself.

And my routes are now:

====================
%{ contentBuster = com.xxx.AppBootstrapPlugin.getRouteCacheBuster() }%
GET /public_cb${contentBuster}/                        staticDir:public
GET /public/                             staticDir:public
==================== 

And this seems to do the trick!! It works even when classes are reloaded.

Note that I removed the routes that were not ending with "/", from my last example, since Play logged this for them: "WARN  ~ The path for a staticDir route must end with / (GET /public -> staticDir:public)".



ogregras

unread,
Dec 20, 2011, 9:45:59 AM12/20/11
to play-fr...@googlegroups.com
jhice, I like your solution.. Way simpler than mine! And it gives a better control on what resources are cachebusted and when.

But I heard it's better the modify the filename of a resource (or its path) to do cachebusting, since some proxies may continue to cache the resource even if the parameters changed (after the "?"). I'm not 100% sure about this though.


jhice

unread,
Dec 20, 2011, 2:54:03 PM12/20/11
to play-framework
Perhaps rename the file by adding the timestamp in it, like said
before in this topic ?
Dont know if your VCS software will like it, doing a "SVN rename"
manually would break the "magic"...

Andreas Bjärlestam

unread,
Feb 8, 2012, 3:04:31 AM2/8/12
to play-fr...@googlegroups.com
I ended up with the following solution:

In our project we have a file that contains the current release version that is updated by our release script.
I changed the /public/ route so that it reads the version number and incorporates it in the url

GET     /public/%{out.write(play.getVirtualFile(".version").contentAsString())}%/    staticDir:public

Now when I refer to a static resource, for example href="@{'/public/stylesheets/style.scss'}", I will get the version number in the url automatically.


R. Rajesh Jeba Anbiah

unread,
Feb 8, 2012, 3:54:19 AM2/8/12
to play-framework
(Disclosure: I haven't yet tried). Are you sure that above code
works without any further changes anywhere?

Andreas Bjärlestam

unread,
Feb 9, 2012, 3:50:44 PM2/9/12
to play-fr...@googlegroups.com
Yep, seems to work fine in my project on Play 1.2.4. Havent seen any problems with it so far. I prefer to have the version in the url-path rather than as a url-parameter. Otherwise I would go for the FastTag solution described earlier in this thread. If you find any problem with my solution, please let me know.

R. Rajesh Jeba Anbiah

unread,
Feb 9, 2012, 11:50:59 PM2/9/12
to play-framework
I tried your solution and doesn't work as I expected (doesn't add
anything in URL).
href="@{'/public/stylesheets/style.css'}"
Outputs just
href="/public/stylesheets/style.css" (nothing added in URL) Note that
I have "1" in the version file

Andreas Bjärlestam

unread,
Feb 12, 2012, 9:53:24 AM2/12/12
to play-fr...@googlegroups.com
Did you name your file .version and put it in the application root folder? Try to just have the following route to rule out problems reading the file.
GET     /public/%{out.write("123")}%/    staticDir:public

R. Rajesh Jeba Anbiah

unread,
Feb 13, 2012, 1:13:50 AM2/13/12
to play-framework
Thanks for your follow up. I exactly tried as yours. BTW, I can't
really understand how the routing will work reverse? You seems to
define route for /public/123, but you're referring just @{'/public/
stylesheets/style.css'} (I'm also little confused here apart from that
it's not working for me).

Jim

unread,
Feb 13, 2012, 9:39:23 PM2/13/12
to play-fr...@googlegroups.com
I think the  @{'/public/stylesheets/style.css'} refers to the literal path on disk, and it is converted to a URL based on the routes file: /public/123/....

So I can't tell why I shouldn't just do this:
GET     /public/%{out.write(""+System.currentTimeMillis())}%/         staticDir:public

That just inserts the timestamp when the server starts... it seems to work great.  Would there be a problem with this?  I can imagine if you're running on multiple servers, you'll want a load balancer with sticky sessions.. But beyond that, it seems like every time you restart the server, clients will be guaranteed (forced) to get the latest resource files.

Thanks.

R. Rajesh Jeba Anbiah

unread,
Feb 14, 2012, 5:54:41 AM2/14/12
to play-framework
On Feb 14, 7:39 am, Jim <slattery....@gmail.com> wrote:
> I think the  @{'/public/stylesheets/style.css'} refers to the literal path
> on disk, and it is converted to a URL based on the routes file:
> /public/123/....

<snip>

Thanks both of you for the tireless help. I finally got it working
in plain new project. Possible culprit seems to be some module and
more specifically I doubt play-config.

Andreas Bjärlestam

unread,
Feb 16, 2012, 7:35:06 AM2/16/12
to play-fr...@googlegroups.com
That is of course a simple solution, but as you say it wouldn't be good if you have several servers behind a load balancer (with or without sticky sessions). Also the cached resources would be invalidated whenever you take down your server for whatever reason. Hopefully that should not happen too often of course, but still. 

tazmaniac

unread,
Feb 16, 2012, 7:52:22 AM2/16/12
to play-fr...@googlegroups.com
For anyone using the Greenscript module the latest version, v1.2.8, now includes support for this mechanism, GET     /public/%{out.write(play.getVirtualFile(".version").contentAsString())}%/    staticDir:public

need to define this property in application.conf:

greenscript.router.mapping=true

Reply all
Reply to author
Forward
0 new messages