#169
Jul 06, 2009

Dynamic Page Caching

Use JavaScript to allow dynamic content in a page cache. In this episode I show you how to insert the user-specific content into a page through JavaScript.
Download (21.7 MB, 11:38)
alternative download for iPod & Apple TV (14.3 MB, 11:38)

Resources

rm public/index.html.erb
# config/environments/development.rb
config.action_controller.perform_caching = true

# forums_controller.rb
caches_page :index

def index
  @forums = Forum.all
  sleep 2
end
<!-- layouts/application.html.erb -->
<%= render 'layouts/dynamic_header' unless @hide_dynamic %>

<!-- forums/index.html.erb -->
<% title "Piano Forums" %>
<% javascript "jquery", "/users/current" %>
<% @hide_dynamic = true %>

<div id="forums">
  <% for forum in @forums %>
    <div class="forum">
      <h2><%= link_to h(forum.name), forum %></h2>
      <p><%=h forum.description %></p>
      <p class="admin" style="display:none">
        <%= link_to "Edit", edit_forum_path(forum) %> |
        <%= link_to "Destroy", forum, :confirm => 'Are you sure?', :method => :delete %>
      </p>
    </div>
  <% end %>
</div>

<p class="admin" style="display:none"><%= link_to "New Forum", new_forum_path %></p>

<!-- users/show.js.erb -->
$(document).ready(function() {
  $("#container").prepend('<%=escape_javascript render("layouts/dynamic_header") %>');
  <% if admin? %>
    $(".admin").show();
  <% end %>
});

RSS Feed for Episode Comments 37 comments

1. gUI Jul 06, 2009 at 00:35

Hey Ryan, do you play the piano ?


2. QuBiT Jul 06, 2009 at 00:38

Great idea and great screencast.

First I thought that it is not a good Idea to only hide the links, because "FireBug"-Users will find them, but as you can restrict the execution of these (often restful) actions it is not that big problem, if someone knows them.


3. Duncan Jul 06, 2009 at 00:51

Thanks for another great screencast, Ryan!

Have you thought about using the session cookie do determine whether a user is signed in? This way a static javascript file could optionally enable the admin links and a request to users/show.js would be eliminated. I'm currently trying to implement this on one of my projects, but there are two issues:

1. The rails session cookie is difficult to decode and parse using javascript.
2. The application could set a cookie on login for this purpose, however I'm still figuring out how to integrate this with Authlogic.

If I make any progress, I'll share it here.


4. Pawel Jul 06, 2009 at 00:54

If you store username in cookie on login, and clear it on logout, you can avoid hitting Rails. Javascript would then use this cookie to hide or display links on the page. This way all can be done entirely client side.


5. Chris Jul 06, 2009 at 01:11

Duncan - I got this working in my Rails app (using Authlogic). It basically looks like this:

(trying out Pastie, let's see if it works...)

In application_controller.rb:

<script src='http://pastie.org/535397.js'></script>

Then, in my site javascript file, I use the last part of this jQuery cookie plugin (the first two-thirds are for writing cookies with jQuery, which we won't need):

http://plugins.jquery.com/project/Cookie

And then:

<script src='http://pastie.org/535398.js'></script>

Now, whenever you're logged in, jQuery seeks out the p tag with an id of "account" and fills it with the user's information.


6. Chris Jul 06, 2009 at 01:15

Aaah, Pastie didn't work like I'd thought. Well, I'll post it inline. It goes:

(in application_controller)
before_filter :set_user_info_cookie

private

def set_user_info_cookie
  cookies[:user_info] = { :value => current_user.email, :expires => 1.hour.from_now } if current_user
  cookies.delete :user_info unless current_user
end

jQuery snippet:
if ($.cookie('user_info')) {
  $("p#account").html($.cookie('user_info') + " | <a href='/myaccount'>My Account</a> | <a href='/logout'>Logout</a>");
};


7. Andy Jul 06, 2009 at 01:16

Pawel: My thoughts exactly. People, wake up. Put your presentation logic on the client-side -- yes, that means JavaScript.

I wonder how much longer it will take the majority of Rails programmers to realize this.


8. Andy Jul 06, 2009 at 01:26

Chris: Looks like a good example. But you can set the cookie through JavaScript/jQuery as well. Even less work for the server.


9. Victor Moroz Jul 06, 2009 at 02:03

Why not use fragment caching for the central part (parts) and generate layout (with header) each time the page is accessed? Not much difference in speed, but it might be cleaner. The only exceptions are Edit / Destroy links, which can be added through JS. This way is also unobtrusive (except admin links, which are not so important for unobtrusiveness).


10. Luca Guidi Jul 06, 2009 at 02:07

Hi Ryan, I agree with the other comments above. You can use a cookie to store the username and the role, to avoid to hit the users controller.

Of course this technique doesn't expose the app to any security hole, because of the server-side check when an administrative link will be clicked.

@Duncan here a snippet (and a related blog post too) for parse cookies with Prototype: http://bit.ly/yPStP


11. Alex Jul 06, 2009 at 03:29

Really useful, thank you!


12. Andy Jul 06, 2009 at 03:38

Victor Moroz: I'm probably going to be crucified for saying this, but I no longer care about the 0.5% (or less) weirdos who have JavaScript disabled.


13. Wilbert Jul 06, 2009 at 05:22

It's a technique. You may need this in situations where cookies/JS is not a viable or elegant solution. Calm down.


14. Victor Moroz Jul 06, 2009 at 06:00

Andy: Search engines? Other than that I don't care about it either.
 
Meanwhile, if you don't care about customer not having JS why do you need page caching in the first place? You can make all links as JS calls and reload just required part(s) of the page (http://vh-daf.ru/service -- it is in Russian, but try to click on the top submenu and you get the idea). Just because of 0.5% (or less) weirdos who still want to press reload button? :)

In fact on this site you can disable JS and still get (almost) the same result, but I would not say it is easy for more complicated cases and I have done non-JS links for Google, not for humans.

A bit confusing topic -- hard to realize where the border line is between traditional and JS design.


15. Ryan Bates Jul 06, 2009 at 07:29

@gUI, yes.

@Pawel, Storing the username in a cookie and parsing it in JavaScript is a great idea, although it is quite specific to this solution. Here I try to give a more generic solution that will work for any kind of dynamic content (such as something that may not be dependent on the current user's actions).

I'll play around with this a bit more and possibly cover the cookie solution in a future episode.


16. Brian Armstrong Jul 06, 2009 at 12:24

Thanks Ryan...great screencast. I'm curious how this looks in production. Is there a slight "jump" or flicker to the page as it appears to have loaded, and then a second update is made? Or is not noticeable. Thanks!
Brian


17. Michael Hasenstein Jul 06, 2009 at 12:42

@Duncan - to "1": you cannot use session cookies at all from Javascript, since for security purposes they are sent as HTTP-only cookies (not just by Rails but by nearly all servers these days). If you want to access something from JS, you must set a "regular" cookie.

I used this technique two years ago for different reasons:

When you log out and go back in the browser using the page cache you go back to pages where you are logged in. The problem was I had pages loading dynamic content only accessible while logged in in the session, and going back to such a page would bring up the server-generated code for "logged in" from the browser's page cache, but the AJAX would produce a failure.
So I turned to the store-login-info-in-extra-cookie solution proposed in the comments above.

However, I removed the code again, among other things (like complexity) because of this...

The disadvantage of any cookies used as a replacement for browser-side storage is that they are uselessly sent back to thee server with each and every HTTP request. Apart from the bytes going over the wire, I believe I saw a presentation (from the YUI guys in the collection of great tech. videos in the YUI theater) where this was identified as having a significant (meaning noticeable overall) performance impact. One of the 14 main pieces of advice for website performance is to reduce the number of cookies as much as possible.

I suggest not to use cookies for client-side storage, as tempting as it is. There already are tools (e.g. a module in Dojo, persist-js, a module planned for by and for YUI in the near future) trying to give a consistent API for the many client-side storage implementations (ranging from Flash to HTML5-type global and local storage in Firefox 3(.5)).

The problem with hacks is they tend to persist and byte you later, when you forgot you had implemented them. So let's use *real* client-side storage if you must, especially since the problem you are trying to solve is not THAT pressing.


18. Michael Hasenstein Jul 06, 2009 at 12:45

I found the presentation about the impact of cookies on website performance:

http://yuiblog.com/blog/2007/03/01/performance-research-part-3/


19. Michael Hasenstein Jul 06, 2009 at 12:50

(sorry for posting sooo muuuch, but I think it's valuable content???)

...and another excellent performance presentation, this one also from a YUI guy:
http://www.slideshare.net/natekoechley/high-performance-web-sites-2008 (Cookies starting on slide 57)


20. Michael Hasenstein Jul 06, 2009 at 13:30

@Brian: There won't be any flicker with THIS example in (even mildly) modern browsers.

Main reason: the Javascript is loaded from the HEAD part of the page, and that means it is fully loaded and executed BEFORE the page is loaded and rendered. Of course, because of the wait for the DOM-ready event in the Javascript the actual work done by the script happens at about the time the good old <body onLoad="..."> would execute, but there is so little to do for the script execution will be instantaneous for all practical purposes.

I guess you still could get a flicker, but only if the layout change was major, i.e. the page has a complicated layout with lots of elements in it, and a lot of areas on the page would be impacted. In THIS example only thus far empty areas are affected, no other elements already on the page change in any way.


21. Squiddhartha Jul 06, 2009 at 16:36

Perhaps this is my unfamiliarity with JavaScript showing, but I'd be concerned with the security of this -- what's to prevent somebody from using a client-side override to enable the hidden admin links? Or do you not care if the links become active because other checking will prevent them from working if the user isn't an admin?


22. Michael Hasenstein Jul 06, 2009 at 17:04

About Security

(responding to the concerns of some people, again longer since several commentators voiced concerns)

There seems to be a BIG misunderstanding about security in a web application.

Hiding (JS) code or links does not provide any security. Why?

Well, let's look at an analogy in the real world. This argument needs two steps.

1) Is your house more secure because you don't tell anyone you hid the key under the second big stone from the left?

2) If you just said yes, would you still say so if I change the scenario by saying "if there are hundreds of people searching around your house for a way in 24 hours a day, 7 days a week?

My point is, in a world of a village hiding links on the webpage is like hiding the key under the stone and may be "secure" enough in that environment.
On the Internet, where hundreds of thousands of automated (often hijacked) computers probe all reachable servers and services all the time (which does not mean they are all on *your* server at the same time, but some of them always are probing your systems!), this is NOT secure in ANY way.
On the Internet, the only way to achieve security is by securing the services themselves, not by hiding that those services exist.

In this context it means you add a before_filter where you check if there is a user with the appropriate rights logged in in the current session. If you do that you don't need to care if anyone finds your links. Finding admin-links in a Rails application is VERY easy - unless you deviate from standards such as common naming conventions and REST significantly - which in turn makes your life so much more difficult in the long run.


23. Duncan Jul 06, 2009 at 18:37

Thanks for the snippets everyone. I've forked Ryan's code and implemented the cookie solution: http://github.com/lemonbbq/railscasts-dynamic-page-caching/

It seems to work quite well as rails doesn't even get hit on the cached requests. Like the session cookie, the caching cookie doesn't have an expiry date set. This means when the user quits their browser, both cookies are cleared and the user is signed out normally.

Currently the user's name and admin status are stored in separate cookies. I'd like to combine these (possibly along with other attributes) into a single cookie using JSON. Everything works fine on the rails side, but I'm having problems getting JavaScript to unescape the JSON string - the spaces get replaced by plus signs. I'll have to work on this.

Integrating with Authlogic should be a breeze: one would simply need to add callbacks for after_save, after_destroy and after_persisting to set and destroy the caching cookie.

@Michael: You raise an interesting point about the performance hit from using cookies. From YUI's results it seems that only cookies larger 500 bytes really slow down the response time. Surely the increase in performance from page caching would outweigh the effect of slightly larger cookies. Though I agree that the increased complexity would be nice to avoid.


24. Ben Scofield Jul 07, 2009 at 07:17

Glad to see more people paying attention to this technique! I've been presenting on this (which I've been calling progressive caching) for a few months now, and have a couple of blog posts:

http://www.culann.com/2009/04/progressive-caching

and

http://www.viget.com/extend/progressive-caching-in-depth/


25. Mina Jul 07, 2009 at 08:04

The above issues notwithstanding, you don't actually need to use JS to search for the DIVs you want to unhide.

Instead of setting "display: none" on every element, just add a class to it like "adminContent".

The main CSS file should specify that *.adminContent should have display: none.

It should also specify that a common parent, say body, when assigned an isAdmin class, should affect its children:

body.isAdmin *.adminContent {
 display: block;
}

The dynamic JS would then need to simply add the "isAdmin" class to the body tag and the browser will take care of the rest due to CSS cascading and a higher specifity score of the above rule that unhides than the generic one that hides. It's also significantly faster than doing it manually with JS dom searching if you have a large DOM tree.


26. Adrian Jul 08, 2009 at 16:48

Hey Ryan,

Great episode, as usual.

I was wondering how much of this you can achieve with the recently added support for caches_action ..., :layout => false.

Regards


27. Kristian Jul 11, 2009 at 10:02

Hi Ryan,

Please do an episode on the new nested forms patch, introduced in Rails 2.3. All examples I have seen so far are for 1-M associations. Please demonstrate for both 1-1 and 1-M. I guess it is really no difference apart from a hasOne instead of hasMany in the model, but just for completions sake, would be nice with both examples included ;)
Thanx for some great screencasts...


28. Gavin Morrice Jul 24, 2009 at 14:26

Hey! Great episode Ryan.

I've also translated it to Prototype for those who don't want to install the JQuery library:

http://handyrailstips.com/tips/18-dynamic-page-caching-with-prototype

Cheers


29. Anders E Aug 02, 2009 at 05:49

The main drawbacks some of the above suggestions are:

1) Link to pages that shouldn't be indexed will have a negative impact for you when google does it's link matrix magic - PageRank (and no, rel="nofollow" does not help you!).

2) Stuff like hidden links and javascript inserts might cause problem for programs used by the blind.

3) Cluttering your html with a lot of stuff you'll instantly hide and keep hidden during the entire stay on the page just isn't clean - KISS, Broadband, Rendering, (and some would even argue, Security through obscurity).

Keep up the nice work Ryan, you make Mondays a bit nicer!


30. Sergiy Sep 04, 2009 at 02:21

Про что статейка?


31. Tonyura Sep 16, 2009 at 06:16

А здесь вообще про што?


32. Ala Sep 24, 2009 at 06:52

Ни фига не понял


33. fun photos Nov 18, 2009 at 21:11

Create every day new photo effects,sketches, avatars


34. vvpo18 Dec 02, 2009 at 18:42

Do you want one <A href="http://elinksoflondon.co.uk/" target=_blank>links of london</A>Lipstick Charm? Come with us! You will have many choices here. All kinds of Links of London jewelry are waiting for you. Links of London jewelry will lead an agitation restoring ancient ways. The new Links of London will come to you soon. As usual, it will surprise all of us.
More information from links of london online shop:http://elinksoflondon.co.uk


35. UGG 取扱店 Dec 24, 2009 at 23:27

tast bud...


36. wholesale nike shoes Jan 13, 2010 at 23:08

A very good article, I will always come in.


37. wholesale scarves Jan 13, 2010 at 23:08

Such a good article, caught my sympathy!
-

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player
Give Back to Open Source