When we started working on Basecamp Next last year, we had much internal debate about whether we should evolve the existing code base or rewrite it. I’ll dive more into that debate further in a later post, but one of the key arguments for a rewrite was that we wanted a dramatic leap forward in speed — one that wouldn’t be possible through mere evolution.
Speed is one of those core competitive advantages that have long-term staying power. As Jeff Bezos would say, nobody is going to wake up 10 years from now and wish their application was slower. Investments in speed are going to pay dividends forever.
Now for the secret sauce. Basecamp is so blazingly fast for two reasons:
#1: Stacker – an advanced pushState-based engine for sheets
The Stacker engine reduces HTTP requests on a per-page basis to a minimum by keeping the layout the same between requests. This is the same approach used by pjax and powered by the same HTML5 pushState.
This means that only the very first request spends time downloading CSS, JavaScript, and image sprites. Every subsequent request will only trigger a single HTTP request to get the HTML that changed and whatever additional images needed. You not only save the network traffic doing it like this, you also save the JavaScript compilation step.
It’s a similar idea to JavaScript-based one-page apps, but instead of sending JSON across the wire and implementing the whole UI in client-side MVC JavaScript, we just send regular HTML. From a programmers perspective, it’s just like a regular Rails app, except Stacker requests do not require rendering the layout.
So you get all the advantages of speed and snappiness without the degraded development experience of doing everything on the client. Which is made double nice by the fact that you get to write more Ruby and less JavaScript (although CoffeeScript does make that less of an issue).
Our Stacker engine even temporarily caches each page you’ve visited and simply asks for a new version in the background when you go back to it. This makes navigation back and forth even faster.
Now Stacker is purposely built for the sheet-based UI that we have. It knows about sheet nesting, how to break out of a sheet chain, and more. We therefore have no plans of open sourcing it. But you can get (almost) all the speed benefits of this approach simply by adopting pjax, which is actually where we started for Basecamp Next until we went fancy with Stacker.
#2: Caching TO THE MAX
Stacker can only make things appear so fast. If actions still take 500ms to render, it’s not going to have that ultra snappy feel that Basecamp Next does. To get that sensation, your requests need to take less than 100ms. Once our caches are warm, many of our requests take less than 50ms and some even less than 20ms.
The only way we can get complex pages to take less than 50ms is to make liberal use of caching. We went about forty miles north of liberal and ended up with THE MAX. Every stand-alone piece of content is cached in Basecamp Next. The todo item, the todo lists, the block of todo lists, and the project page that includes all of it.
This Russian doll approach to caching means that even when content changes, you’re not going to throw out the entire cache. Only the bits you need to and then you reuse the rest of the caches that are still good.
This is illustrated in the picture above. If I change todo #45, I’ll have to bust the cache for the todo, the cache for the list, the cache for all todolists, and the cache for the page itself. That sounds terrible on the surface until you realize that everything else is cached as well and can be reused.
So yes, the todolist cache that contains todo #45 is busted, but it can be regenerated cheaply because all the other items on that list are still cached and those caches are still good. So to regenerate the todolist cache, we only pay the price of regenerating todo #45 plus the cost of reading the 7 other caches — which is of course very cheap.
The same plays out for the entire todolist section. We just pay to regenerate todolist #67 and then we read the existing caches of all the other todolist caches that are still good. And again, the same with the project page cache. It’ll just read the caches of discussions etc and not pay to regenerate those.
The entire scheme works under the key-based cache expiration model. There’s nothing to manually expire. When a todo is updated, the updated_at timestamp is touched, which triggers a chain of updates to touch the todolist and then the project. The old caches that will no longer be read are simply left to be automatically garbage collected by memcached when it’s running low on space (which will take a while).
Thou shall share a cache between pages
To improve the likelihood that you’re always going to hit a warm cache, we’re reusing the cached pieces all over the place. There’s one canonical template for each piece of data and we reuse that template in every spot that piece of data could appear. That’s general good practice when it comes to Rails partials, but it becomes paramount when your caching system is bound to it.
Now this is often quite easy. A todo looks the same regardless of where it appears. Here’s the same todo appearing in three different pages all being pulled from the same cache:
The presentation in the first two pages is identical and in the last we’ve just used CSS to bump up the size a bit. But still the same cache.
Now some times this is not as easy. We have audit trails on everything in Basecamp Next and these event lines need to appear in different context and with slight variations on how they’re presented. Here are a few examples:
To allow for these three different representations of the cached HTML, we wrap all the little pieces in different containers that can be turned on/off and styled through CSS:
Thou shall share a cache between people
While sharing caches between pages is reasonably simple, it gets a tad more complicated when you want to share them between users. When you move to a cache system like we have, you can’t do things like if @current_user.admin?
or if @todo.created_at.today?
. Your caches have to be the same for everyone and not be bound by any conditionals that might change before the cache key does.
This is where a sprinkle of JavaScript comes handy. Instead of embedding the logic in the generation of the template, you decorate it after the fact with JavaScript. The block below shows how that happens.
It’s a cached list of possible recipients of a new message on a given project, but my name is not in it, even though it’s in the cache. That’s because each checkbox is decorated with a data-subscriber-id HTML attribute that corresponds to their user id. The JavaScript reads a cookie that contains the current user’s id, finds the element with a matching data-subscriber-id, and removes it from the DOM. Now all users can share the same user list for notification without seeing their own name on the list.
Combining it all and sprinkling HTTP caching and infinite pages on top
None of these techniques in isolation are enough to produce the super snappy page loads we’ve achieved with Basecamp Next, but in combination they get there. For good measure we’re also diligent about using etags and last-modified headers to further cut down on network traffic. We also use infinite scrolling pages to send smaller chunks.
Getting as far as we’ve gotten with this system would have been close to impossible if we had tried to evolve our way there from Basecamp Classic. This kind of rearchitecture was so fundamental and cuts so deep that we often used it in feature arguments: That’s hard to cache, is there another way to do it?
We’ve made speed the center piece of Basecamp Next. We’re all-in on having one of the fastest web applications out there without killing our development joy by moving everything client-side. We hope you enjoy it!
tl;dr: We made Basecamp Next go woop-woop fast by using a fancy HTML5 feature and some serious elbow grease on them caching wheels
Michael Dubakov
on 17 Feb 12I should say that is a very clever way to use cache. Nice!
Jigar
on 17 Feb 12How do handle the To-Do count in the project cache, when a to-do item is deleted? Bust the entire project cache or simply update the count via JS? Just curious.
DHH
on 17 Feb 12Jigar, we bust the project page cache, but it’s almost free to regenerate because almost none of the other caches changed. It’s the same scenario as if you edit a todo.
Zack
on 17 Feb 12@DHH
You must be using NodeJS (yet you never mention it in this article) ... and I highly suspect you’re minimizing the use of Ruby, let alone Rails.
TL;DR; 37signals is afraid to admit they’re slowing abandoning Rails for Node
DHH
on 17 Feb 12Zack, haha, good one. We use nodejs for local development via Sam’s Pow server, but there’s no nodejs in Basecamp Next itself. It’s all Ruby on Rails. We’re running the default stack using Rails 3.2+, MySQL 5.5, Memcached, a tad of Redis (mostly for Resque), ERB, test/unit, CoffeeScript, Sass, and whatever else is in the default box you get with Rails.
Tomasz Tybulewicz
on 17 Feb 12It’s great! How do you know what to invalidate from cache when todo #45 is changed?
How do you store that information?
Khalid Abuhakmeh
on 17 Feb 12“The JavaScript reads a cookie that contains the current users id, finds the element with a matching data-subscriber-id, and removes it from the DOM.”
Is there any concern that you may leak data that you might not want people seeing in this case?
If someone viewed the source of the page, they would clearly see the name of an admin or their own.
It might not be critical in this scenario, but if your whole site is built on this premise… well mistakes happen.
Aaron
on 17 Feb 12Wow, just awesome how you go down very deep to optimize basecamp. You are a great inspiration for any engineer!
Benjamin Lupton
on 17 Feb 12Good stuff. Stacker and PJAX seem like a very similar idea to that of jQuery Ajaxy – http://github.com/balupton/jquery-ajaxy – back in 2008. Been wanting to upgrade Ajaxy for History.js for some time now, but always felt I was the only one which cared enough about it! Good to know that it is an issue that other people are tackling too.
Keep up the great work guys, basecamp next seems like it is going to be killer!
Richard Nyström
on 17 Feb 12@DHH Do you use Ruby 1.9.3-p125 in production?
Paul
on 17 Feb 12So it’s just partial caching with some JS to make it cover all cases? No JSON?
DHH
on 17 Feb 12Tomasz, the cache key for the todo is based on the updated_at attribute. That attribute is automatically touched when an update to the todo is made, which causes the new key to be generated and everything is automatically updated.
Khalid, yeah, you can’t use this technique for things that need to be secret. That’s not the case where we use it, though.
Piotr Usewicz
on 17 Feb 12Can’t wait to get my hands on the beta ;)
DHH
on 17 Feb 12Paul, that is correct. We do use a few JSON transfers in a couple of places, but the vast majority of the app is just partial caching + JS to file off some edges.
Richard, I think we’re just on 1.9.3-p0 right now, but we’ll be upgrading to latest shortly.
Micheal Moritz
on 17 Feb 12This is an epic post. Way to go
Chris Cachor
on 17 Feb 12Excellent work guys! I’m very impressed with Basecamp Next and think I’ll see a lot of your ideas trickle down into other web applications in the next couple of years. Love the sheets idea—really using your heads there!
Khalid Abuhakmeh
on 17 Feb 12@DHH Ok that makes sense, thanks for the quick reply.
Are you finding there are things that you just can’t cache or is there always an answer to “That’s hard to cache, is there another way to do it?”
Jeff Putz
on 17 Feb 12Have you guys done any measurement on how much RAM you need on a per-user basis for caching? I assume you didn’t buy all of that RAM just to photograph it. :)
DHH
on 17 Feb 12Khalid, there are definitely things where you can’t cache everything as deeply as we can on the project page. In those cases we just cache what we can and allow the rest to render live. But we fight any feature that requires that and try to restate it in a way that allows for better caching.
Speed is such an important feature that it’s worth giving up other things for.
Steffen Hiller
on 17 Feb 12What about handling privileges?
You mentioned if @current_user.admin?, but didn’t go into how you solve that with your caching? Javascript wouldn’t be an option, I guess?
DHH
on 17 Feb 12Jeff, we made some back-of-the-envelope calculations and we should be good with that big 1TB hunk. But if we’re not, we’ll just buy some more. RAM is so cheap that we would be happy to add a few more TB of caching if it meant keeping things fast. Cost of doing fast business!
Zack
on 17 Feb 12@DHH
How can you say this is all Ruby on Rails.
The entire article you wrote is talking about a custom Javascript library you wrote, which is similar to pjax, to be the core engine for Basecamp Next.
Why are you so sensitive to admitting 37signals is moving more towards JavaScript for application development and moving away from a Ruby/Rails platform?
DHH
on 17 Feb 12Steffen, most things can be handled server-side. For example, render the Edit link for everyone and hide it via JS if someone is not an admin. Then check permissions server side if the link is ever clicked.
Martijn
on 17 Feb 12That’s very interesting. Thanks for sharing it.
DHH
on 17 Feb 12Zack, all the views are Ruby on Rails. The stacker engine is a wrapping around those views. So when I say “it’s all Ruby on Rails” I mean it’s as much Ruby on Rails as any of our previous applications have been. We’ve been big proponents of using Ajax and JavaScript to make things faster since launching Tada list back in 2005.
But yes, the client-side MVC JS noise is currently so loud that we’re happy to provide a counter-example in Basecamp Next. You don’t need for your entire application to be done in a client-side UI for it to be fast.
Tonio
on 17 Feb 12Just a question…
Why not use some read/write intensive database like MongoDB or Casandra to avoid the complex cache logic?
I’m asking that because I have a similar cache logic on my application and I’m not sure about replacing MySQL yet.
Tonio
DHH
on 17 Feb 12Tonio, MySQL is plenty fast enough. It’s not the bottleneck. Running all the logic about how to present things is. Thus, we cache it.
Richard Nyström
on 17 Feb 12I’m glad that you are not going all in on the MVC JS hype that’s going on right now. It just adds unneeded complexity in most cases. It’s only useful for solving some specific problems in web apps.
Zack
on 17 Feb 12DHH
Any plans to use the new 70x faster MySQL Cluster?
Or are you still using Percona?
DHH
on 17 Feb 12Richard, MVC JS is great when there’s no way around it. Our brand new calendar is done with Backbone.js and it’s fantastic. But it’s completely overkill for 95% of the rest of the app.
DHH
on 17 Feb 12Zack, MySQL has not been a bottleneck for us for some time. Between SSDs and plenty of RAM, it’s generally a none issue. We’re running a Percona-spec build, yes. And it’s plenty fast for our purposes.
Allan ebdrup
on 17 Feb 12Writing JavaScript doesn’t take my joy away, quite the opposite. Having to work with the caching layers described here. And serverside generated HTML. It sounds like a nightmare to me. Dropping features because they can’t be cached. You’ve got no clothes on!
DHH
on 17 Feb 12Allan, I code naked whenever I can. It’s a key benefit of working from home!
Raymond Brigleb
on 17 Feb 12Wow, great detail David, thanks for sharing. Whatever you’re doing, keep it up, it’s working. It’s fast as hell!
Salman
on 17 Feb 12Aren’t you guys using managed hosting with rackspace? You can’t buy your own RAM so you must bo co-located then?
DHH
on 17 Feb 12Salman, we moved out of Rackspace a couple of years ago. We’re managing our own infrastructure now.
Jon Gjengset
on 17 Feb 12Good to see that some developers still care about carefully craft web solutions, and are willing to scrap existing systems to create a proper solution. It’s true what you say that the responsiveness of is a major usability concern.
The Stacker solution is interesting. I had a similar idea a while back that never received much attention, but your implementation is probably a lot more sophisticated: https://github.com/Jonhoo/snavi/ .
Sakuraba
on 17 Feb 12Hi David,
thanks for the great post. How do you model the “cache-dependencies” between todo-list entries and projects and everything else?
Is the logic for “if a gets updated, flush x and y” hardcoded or did you guys build something to model those conditions in a declarative manner?
Brendan Kemp
on 17 Feb 12How do you guys deal with the lack of/buggy pushState support in IE 9 and Safari 4+?
Brendan
Joe Larson
on 17 Feb 12@DHH – it seems like this caching approach would require considerable up front design to get the data model sorted out to a degree that all the layers work well together. How did this approach change the design and implementation process for 37s? I assume it was less iterative?
Anyway, kudos for doing whatever it takes to make this app so many people rely on screaming fast—you’re saving the world hundreds of thousands of man-hours they can spend doing something else more important!
Oscar Merida
on 17 Feb 12How did you get around the lack of pushstate support in Internet Explorer? Did you use a library like history.js by @Benjamin Lupton?
Dave Brazzle
on 17 Feb 12Very interesting and elegant… but will this complicate/delay the appearance of Basecamp Next mobile device support? Is this area on the 37 Signal’s radar screen, as yo did with your Highrise mobile solution?
Maurizio
on 17 Feb 12Do you plan to release the code of Stacker?
SS
on 17 Feb 12IE 9 users don’t get pushState support—every click falls back to a full page load. IE 10 users will enjoy the full speed of Stacker.
Alexander Zaytsev
on 17 Feb 12Mike Kelly
on 17 Feb 12I’m surprised you haven’t mentioned ESI – is this something you’ve looked at?
David Paquet
on 17 Feb 12Do it mean that you will drop i18n? From my understanding, i18n would require N time more cache (where N is the number of languages supported)
Brian Blocker
on 17 Feb 12That’s pretty slick. I keep running into more and more creative uses of memcache everywhere I look.
So for curiosity’s sake, since node and mongo are so buzzy right now, how could ( or could you at all ) see either of those being put to use here? What are your thoughts about “if we replaced X with Y, it would be MUCH BETTER/NO DIFFERENT” ?
DHH
on 17 Feb 12Alexander, I think the Law of Demeter is shit and never follow it.
Sakuraba, it’s backed into Active Record.
belongs_to :project, touch: true
means that the project will automatically have its updated_at updated when the todolist is.Joe, no special upfront design. Just a set of design constraints (reuse templates across pages, don’t have user-specific bits in there). It didn’t change our process at all, really.
Dave, the simple design works super well on tablets with no changes. It actually works reasonably well on phones too, but we’re exploring other approaches there as well.
Régis Hanol
on 17 Feb 12@David Paquet: I was wondering the exact same question.
DHH
on 17 Feb 12Brian, our data is highly relational, so a relational database is a great fit. I think we’d only be making things harder on ourselves if we tried to somehow shoehorn the domain model into a nosql setup.
I think node is great for building network services. I sure as hell wouldn’t want to use it to build something like Basecamp, though. The callback spaghetti is bad enough just with the sprinkles of JavaScript that we’re currently using.
DHH
on 17 Feb 12David, Basecamp Next will not ship with i18n. It did not make the numbers for Basecamp Classic, so it has not been worth our effort. But yes, you’d need to make the i18n setting part of the cache key if you went that route. Which is not the end of the world. Most users within the same account would probably be using the same language anyway, so it’s fine that it’s part of the key.
Brian Blocker
on 17 Feb 12That makes sense. I like the thought of using node for a few specific cases ( I REALLY like how easy it is to get imagemagick working ), but even being a huge JS fan, I don’t think node is the end-all for everything.
Appreciate your input! Keep it up!
olivier
on 17 Feb 12you load a lot of things not even knowing if they will be accessed … it’s not very green :D … just for the sake of 50ms . =D
Greg
on 17 Feb 12What’s the point of using Memcache AND Redis? You can use Redis for caching too.
Damn, you can store everything in Redis. Like YouPorn does ;-) https://groups.google.com/forum/?fromgroups#!topic/nosql-databases/d4QcWV0p-YM
Eric
on 17 Feb 12How do you manage changes to the pieces of data or htm/css in partials with such extensive partial caching? I use a similar caching strategy and that’s the one place I run into problems.
DHH
on 17 Feb 12Redis cluster is still not there, as far as I’m informed. Also, we wouldn’t want to use the same Redis instance to store our jobs as we do our caches. Different characteristics and criticality.
Jon Bolt
on 17 Feb 12Great post, lot’s to learn from here. One question… how did you guys come up with the cache naming conventions (e.g. what is a to-do actually called in the cache)? Is there anything that worked well for you?
DHH
on 17 Feb 12Eric, that’s what those little v1 and v56 in the cache keys are about. When you change a template, you bump the cache key. We are, however, exploring a unified key config that can handle relationships. At times it can be a bit cumbersome to bump the entire chain.
DHH
on 17 Feb 12Jon, it’s backed into Active Record. We just do cache(todo) do/end. That calls #cache_key on the todo, which takes the name of the class / id – timestamp.
Eric
on 17 Feb 12Cool, thanks DHH. I’m planning to add a version to the cache key. I’m a bit worried about the spikes in db load, but it should be manageable.
I’d be very interested in a unified key config that can handle relationships.
Adam
on 17 Feb 12DHH,
I like the idea of “sharing cache between users” but how do you handle access control? Sometimes you really shouldn’t have access to certain types of information that I have access to, even though we’re looking at the same thing.
It occurs to me that this may not be an issue for BaseCamp Next but I assume you guys are looking to share some wisdom rather than just bragging. This approach is a brilliant one, but the issue of access control would need to be addressed in order to make use of it. I don’t think you can do it half way either, at least not easily.
So lets say the thing we’re caching is the line item of an invoice. As the client I want to see the price, quantity, taxes and totals but as the owner, you’re going to also want to see the margin, base cost, etc. Clearly in this case it would be a bad idea to share the cache between the two of us.
Is it a matter of creating different actions (or maybe controllers) for each situation, putting access control there and then caching different versions of the line item in each case?This breaks sharing content between contexts (although I suppose you could share parts of the line item between both) but perhaps its the least problematic approach.
mikhailov
on 17 Feb 12David, how do you handle unbinding events on DOM elements after pushState page transition to next page with it’s own DOM. Do you use pjax events?
chrismealy
on 17 Feb 12Do nested cache calls just work or did you have to do some extra work? This seems too easy to be true.
Justin Reese
on 17 Feb 12Darnit, now I wish I’d requested an invitation earlier. This would be fascinating to play with in-browser.
jfoxny
on 17 Feb 12@DHH – “For example, render the Edit link for everyone and hide it via JS if someone is not an admin. Then check permissions server side if the link is ever clicked.”
What are you guys using for permissions these days? Never found a great framework for this.
DHH
on 17 Feb 12Adam, yes, permissions are the bane of good caching. So we’ve avoided them wherever we can. Everyone sees the same project page. Either you have access to it or you don’t. No in between.
But there are indeed other pages where we apply permissions and in those cases you just cache what you can. In many scenarios you’re still doing great if you can cache 70% of the page.
Chris, I’m not sure what you mean? It’s pretty easy :).
DHH
on 17 Feb 12jfoxny, we don’t use a framework. Advanced permission systems are generally terrible to work with, so we try to keep it simple. Right now there are very few permissions in Basecamp Next.
jfoxny
on 17 Feb 12@DHH, Amen to that!
Terry A. Davis
on 17 Feb 12Hard drive ATA ISA port I/O is awful compared to Windows DMA PCI. File compression helps and spaces-to-tabs.
VGA memory is slow, so caching the screen and only updating what’s changed helps, unless the whole screen changes. That sucks. Maybe, 35% CPU would be spent, I’m not sure. The 8×8 font works well with bit planes. Bit planes are a penalty, however, for regular graphics. Simply doing 3D rotations and blits by hand will sink any unaccelerated system.
It’s not intuitive, but the answer is, “Why do you think they make GPU’s, moron!?”
Theo Mills
on 17 Feb 12Is this post a glimpse of what might be include in Rails 4?
DHH
on 17 Feb 12Theo, all the basic principles we’re employing to make Basecamp Next so fast are available today. Pjax + Rails 3.2 is all you need to replicate 95% of this.
Brian Armstrong
on 17 Feb 12Thanks for the writeup! Curious, when you expire a cache do you pregenerate the next version right there or wait for the next request I come in and trigger it?
Aquecedor
on 17 Feb 12This subject of caching always fascinates me! Very clever cache tactics!
Brian Morearty
on 17 Feb 12Hi David,
You mentioned that the DB has not been a bottleneck for some time. So then do you go ahead and run the queries and store results in @assigns in the controllers, or did you create helper_methods to run the query only if and when the data is needed by the view?
DHH
on 17 Feb 12Armstrong, next request triggers the render. No pre-rendering.
Morearty, most queries are triggered in the view automatically now that Active Record delays execution until you actually use the collection. So it kinda works by itself.
Jeff Bezos
on 17 Feb 12That’s why I invested some money in 37signals, my employees are doing a great job over there.
And DHH please don’t forget to upload this mechanism on our server (Amazon) as soon as possible.
Cheers.
tobi
on 17 Feb 12Can the user still open things in new tabs? This is a frequent disadvantage of one-page-app designs.
If yes, how does this work and in which browsers?
JF
on 17 Feb 12Can the user still open things in new tabs?
Yes.
Chris Barnes
on 17 Feb 12Seems like Memcached is the real hero in this app. MySQL isn’t an issue because you are pulling everything out of cache. This will only work with a relatively low concurrency model. Where if you have high concurrency you are still going to need a more robust datastore like Cassandra or HBase to handle the load.
MySQL with Memcached is a proven solution. Really good for this kind of app where you more reads then writes.
Really slick setup though, the Stacker thang is pretty hot.
Americo Savinon
on 17 Feb 12Nice post. David is there any particular reason for using mySQL over Postgres?
Csaba Szabo
on 17 Feb 12I think this Basecamp next is pretty cool. But, I don’t understand why everybody thinks these technics are so outstanding? I mean, chrome has stacked settings page, which works almost like here, except of course in chrome, there is no ajax. And the partial page loading is not a new thing too. I use this technic, and figured out by myself how to use it, but I’m far not as clever as these guys at 37signals.
DHH
on 17 Feb 12Chris, BCC has hundreds of thousands of users and it doesn’t really use memcached much. MySQL is more than adequately equipped to handle the load. I don’t know what you mean by saying that Cassandra or HBase are “more robust datastores”. MySQL is an exceptionally “robust” datastore. For 99.999% of all applications out there, it’s all the performance they never need.
Luke
on 17 Feb 12Nice work, that’s really impressive
Tim Branyen
on 17 Feb 12This sounds really awesome overall for Basecamp. I’m curious as to how much time you spent developing this system instead of leveraging existing client side libraries to assemble the layout/views (as it seems most new web applications are doing).
I’m more interested in everything that went wrong for you guys, rather than everything that went right. Since there must have been significant trial and error to develop this and stray from the seemingly more post-modern-traditional-model of web application development.
Tim
on 17 Feb 12HaHa! I love that little “Troll” icon next to Zack’s comment :)
Ryan
on 17 Feb 12I’m sorry, did you just say you’re caching http responses inside the JavaScript interpreter inside my browser? My browser already does that. Sounds like a massive waste of my ram.
Please tell me I’m wrong.
Ryan
on 17 Feb 12Ugh your server side caching sounds like a god damned nightmare of complexity too.
” If I change todo #45, I’ll have to bust the cache for the todo, the cache for the list, the cache for all todolists, and the cache for the page itself. That sounds terrible on the surface until you realize that everything else is cached as well and can be reused.”
shotguns computer, curses ever thinking rails could scale elegantly, turns gun on self
Salman
on 17 Feb 12Where do you guys weave in your caching layer? i.e. where do you perform cache lookups/inserts?
Is it at the view level/controller action/model or custom ruby classes?
I’d imagine with your model it might get a bit complicated hence my question. Are you writing to a queue then will then go and invalidate all necessary caches?
Just trying to get an idea of your strategy at the code level.
DHH
on 17 Feb 12Ryan, you’re wrong. This caching setup is for when you follow links (not just use the back button). Your browser otherwise have to recheck the resource to see if it’s up to date. We show you the cache while it waits for that.
The caching scheme happens automatically and in the background as it’s based on the key-based expiration principle. There is no manual cache busting going on. There’s plenty of good information about this technique available online and how it’s often used in conjunction with Memcached’s last-used eviction policy.
Branyen, we got started on this setup using the pre-existing library of pjax by the Github guys. Calling it a “library” is actually a big word for a very small amount of code. Certainly far, far less than most JavaScript MVC frameworks. Stacker added sophistication to that, but that was to support the sheet-based design—something we would have had to program regardless of whether there was JavaScript MVC going on or not.
So in other words, taking this path did not add additional programming time to the effort. In fact, the whole reason we went this route is that I consider it to be a far faster and more productive one.
Salman, the russian doll setup described here is all view-based fragment caching. There is on manual cache expiration to handle. It’s all automatically taken care off by the use of the key-based expiration scheme.
Jim Jeffers
on 17 Feb 12This is really impressive and I didn’t realize you could get this kind of speed without moving client side. BUT I do have a question—you say: “We’re all-in on having one of the fastest web applications out there without killing our development joy by moving everything client-side.”
Are there some significant advantages to doing things this way over client-side MVC? Also, why do you find moving things client side would kill your development joy? Just wondering why you seem slightly averse to moving more code client-side in your post.
Donald Plummer
on 17 Feb 12@DDH: Are you storing all your audit logs in mysql? Are they all in the same table? Does MySQL have problems with millions of rows? Its a problem I’ve been trying to research, but haven’t had much luck with.
Thanks for sharing the architecture you used and the “CACHE ALL THE THINGS” approach you took.
Fred
on 17 Feb 12I guess no backbone, no spine, emberjs is definitely out. It is just stackr framework plus some pjax inspired pushState utilities for nested push. I was discussing with my colleague about backbone and similar client side MVC, the engineers were kind of expecting much less design changes. Citing that all the views, subviews and events are already nicely backed and mapped into client side objects and handlers. On this, rendering on server side remove a lot of this restrictions, let the design possibly evolve a lot of wider than client side would restrict. Way to go :)
Just wondering, why not go for pushBang(#!) for IE, instead of waiting for pushState on IE10? I am not sure how reliable pushState going to be in IE10.
I suspect we won’t be seeing real-time socket push update in BCX. Actually there is no point doing it. All notifications are nicely sent via emails.
How is the mobile strategy going to be? Are all the cached views shared with the mobile app?
Any chance to ditch jQuery in the future to replace with something like stackQuery ;)
Anyway, forget all these technologies. Technology is something doesn’t really work. Catch-up section is the coolest thing in BCX. Project managers love this shit. That’s probably the best touch of Jason and the willingness of the engineers to go through all the design thoughts as a whole yields the best of Basecamp Next. I hope it is not a complete rewrite of Basecamp ;)
Deliciously,
Fred
Scott
on 18 Feb 12I think the product looks great and obviously had a lot of time and talent put into it.
Yet, “Catch-up section is the coolest thing” “Catch Up feature is mind-blowing as well.” (from the other post). I think it is nice, but isn’t is simply a list of activities that happened on a given day? Doesn’t seem that amazing. Is it just me?
Fred
on 18 Feb 12Catch-up section is to let Jason updated in case he went to sleep while David was working really hard till late in the night, isn’t that cool ;)
mikhailov
on 18 Feb 12It looks like a new paradigm CachingDrivenDevelopment. Every piece of User Interface should be small and cacheble independently, because it’s so hard to build such effective schema over a markup made by just designerns.
Aslam
on 18 Feb 12Any special reason to use ERB instead of HAML or SLIM?
Kaito
on 18 Feb 12This concept about small cached pieces is really a nice idea. I’d love to read more about your implementation of it.
Martin Halamicek
on 18 Feb 12If I understand it right each of 7 todos from previous example can have different updated_at values which you have to know to be able to compose the cache key for each todo.
How do you handle lookups for these updated_at values used for composing caching keys, are these values also cached?
You also have to know ids of all todos in list to regenerate todolist cache, is this list also cached in same manner?
Rob Cameron
on 18 Feb 12Once again 37s blows everyones’ minds by showing how they use features that are already part of Rails! I love the guesses above about they must have some amazing client MVC framework or NodeJS or custom extensions to ActiveRecord…nope, just the bare framework. Rails really is that good everyone!
I use fragment caching all over the place but never thought to take it to this level. And the idea of letting JS do the work of modifying the view when there is data that is user-specific (like hiding yoursel in a list of member of a project) is so simple I can’t believe I didn’t think of it.
Great job guys and thanks so much for sharing. I’d love more technical posts like this, especially if you guys thinks you’re doing something that others might not realize is available to them.
Simon
on 18 Feb 12How do you handle HTML/CSS changes with HTTP caching?
If you use the default AR#cache_key for ETag or AR#updated_at for Last-Modified then changing HTML/CSS won’t change these values and some users who already have an old version in their cache will get 304 and see old HTML/CSS.
Do you handle it somehow or just assume these headers will change eventually or users will eventually reload the page ignoring the cache?
Helmut Juskewycz
on 19 Feb 12Thanks for the insight. I used a similar approach a couple of times, but I always struggled with testing the view. Honestly, mostly we just skipped testing it. How do you do it? Selenium? JavaScript testing framework? Not testing at all?
Ed
on 19 Feb 12@DHH, so basically you are against Node.js for building Web Applications?
Darío
on 19 Feb 12@DHH, great article, thanks!.. Just to clarify what you’ve written: when a todo changes, do you request and redraw the whole container or just the todo bit?
By the look of things you’re redrawing the whole lot (or at least the corresponding sheet), please, correct me if I’m wrong on that. It seems as if Stacker behaves somehow like a big view swapper.
How do you work out the transitions between views? Or you just don’t do transitions and content is just replaced?
On the other hand, how do you handle running some JS like on the calendar when the view is changed? E.g., if you’re looking at a todos view and go to the calendar, how do you run that Backbone app? Do you have some sort of callback system you use in Stacker?
Thanks again! :)
Matt Boynes
on 19 Feb 12Interesting setup, thanks for sharing. In your stacker setup, is there a way to handle stale resources like sprites, CSS, and JS files that have been updated since the template was loaded? With reloading the template on each request, you can of course force-reload files with a parameter, e.g. style.css?v=1.0.2, but if you’re not reloading the template, clearly that isn’t an option. Do you have a way to get the page to reload its template or specific resources if you push a significant update live? Though it may be an edge case, I have to imagine it’s a real world problem when you have the constant flow of activity that BC experiences. Thanks!
Ed
on 20 Feb 12A few post ago, you mentioned Coffeescript is now the same LoC as Ruby, where is Coffeescript used in place of Ruby?
Katie @ @Womens' Magazine
on 20 Feb 12Nice tips David,
Stacker is new for me but i think you had to use because of particular basecamp needs. Then, it eventually boils down to caching .. right?
Ryan
on 20 Feb 12David, Thanks for the reply but it still seems redundant. Seems like you could just set an Expires: header to prevent the browser from re-checking the resource. If you’d like the browser to check for updates after loading the page, do that in an AJAX callback.
This might be ever so slightly more complicated, but it has the benefit that you wouldn’t be storing all your data twice in memory.
DHH
on 21 Feb 12Ryan, expires headers only work if you know in advance when something is going to be stale. You don’t in this case, so you can’t use that for this.
Ryan
on 21 Feb 12I appreciate replies and don’t want to belabor the point, but if the goal (as I understand it) is to
1. have something cached to display NOW while
2. checking to see if the resource has updated
... then why not invert the way you’re doing it now: Let the browser serve the cached/stale page (sent with a long Expire) and let the Javascript poll via AJAX to see if that same resource has been updated.
By doing it the other way, caching the resource Javascript and letting the browser check for an update, you end up with 2x copies of the resource cached. Or so it seems to me.
Ryan
on 21 Feb 12(I suppose you’d lose a touch of elegance because you’ d have to request an altered URL from the AJAX call, e.g. foo.bar/baz?version=uncached or somesuch, otherwise the browser will just hand the cached version back to the AJAX call.)
(I can see where it’s not quite as simple as “you’re duplicating the cache!” but it does seem like some real waste, just my $.02.)
Matt
on 22 Feb 12This sounds like private messages are not going to be supported? That would be a deal breaker for us.
RF
on 22 Feb 1237s probably quite reasonably doesn’t want to do this, but a version of Ryan’s idea:
- Put page skeleton, CSS, JS, etc. in AppCache - Use localStorage for the back/forward cache
With AppCache, you can start rendering the page before a network request happens, at the cost of having to reload the page if CSS/JS/page skeleton have changed. With localStorage, you can see cached content across sessions or when the device is offline (though in offline case, you’d need to indicate offline status + disable form controls + ‘grey’ uncached links). Neat in theory, probably less so in practice.
Anonymous Coward
on 23 Feb 12-1’
Anonymous Coward
on 23 Feb 121
Sergio
on 24 Feb 12I hope answers are not close already, :P
The search box seems to have a relevant position too. Did you guys manage to make it fast, fun and usable? Are you using any special technology or trick there?
Thanks for your time
This discussion is closed.