Dec 2010

Performance was a major product and engineering goal in the new Twitter.com, launched in September. To make it fast, our engineering team focused on practical application of many well-understood front-end performance techniques, but also crafted some interesting new solutions along the way. There’s been a lot of interest in what we’ve done, so I’d like to share some of that here.

Twitter API Access

API access is easily the highest cost component in the Twitter front-end. Every API request requires an asynchronous fetch, and Twitter.com is constantly fetching data. We wanted to build a fast JavaScript access layer with aggressive caching, but also make it as easy as possible to write application code that just works. To achieve this, we focused first on building the API layer that worked, and then built caching into it to meet our needs as we discovered them. We found two major bottlenecks that could be removed with smart caching:

  1. Requests for objects we’ve already processed, such as loading a Tweet from a timeline into the details pane.
  2. Asynchronous data fetches that occur almost immediately at page load.

We solved these two problems with an object cache and a request cache, respectively. One of the great things about these solutions is that they don’t require modifications to application code, and the application continues to work just fine if they are disabled or broken. Here’s how they work.

Object Cache

This is a simple key/value store that holds onto specific types of objects. To use Tweets as an example, whenever a Status object is constructed in the API, from any call, we update the entry in our Status object cache with the associated JSON. When a fetch is later requested for that Status (by ID), we’ll pull it out of cache rather than make another network request. Statuses arrive in a variety of calls, including timelines, on User objects, and via single requests. Similar logic is applied to other objects, including User and List.

Request Cache

The request cache is designed to hold on to entire JSON payloads as returned from the Twitter API, and associate them with endpoint/parameter sets. When the endpoint is requested, the JavaScript API first checks the request cache and uses that value as if it were returned from the API asynchronously. We also associate a “request to live” (RTL) value with each entry. This is similar to the “time to live” (TTL) often attached to time-sensitive or expiring data. In general, requests are cached with anRTL of 1, such that subsequent requests will fetch new data.

We’re utilizing this cache by predicting API requests that the client will make soon after page load, and sending down the expected response in the initial page source. For instance, we assume each logged-in page load will result in an authenticated request to api.twitter.com/1/statuses/home/timeline.json, without any parameters. So, in our Ruby on Rails server, we gather the JSON API response for that call, and seed the request cache with it during app load. We’re currently doing this for about 6 different requests, depending on various conditions.

Making a Fast Web Page

We also put a lot of effort into making that parts of our application that run directly against the core browser rendering components as fast as possible. We’ve had to work hard in a lot of areas, but we particularly focused on HTML rendering, event handling, and URL routing as performance bottlenecks.

The Rendering Stack

A fast interface requires DOM fragment generation to be as fast as possible. We’re using Mustache templates to generate HTML fragments, and then using innerHTML (via jQuery) to turn them into DOM fragments. This is generally going to be much faster than building DOM nodes programatically, and has the nice side effect of being very portable and easy to use. Mustache is one of the fastest JavaScript templating systems we’ve seen, so we’re able to churn out a lot of HTML very quickly on the client.

This approach has made our client HTML rendering actually faster than the stack running in our Ruby on Rails server, so we’re deferring almost all rendering until the JavaScript boots up on the client. This is a different approach than many choose, and it does mean there’s a longer period before the user sees any content on the page. However, it’s meant that we can focus all of our efforts on getting the data to the client as fast as possible, instead of splitting focus between generating and flushing partial content, and getting the interaction running. We’ve focused on fastest time to the full application, rather than fastest time to a partial application.

Interaction Handling

At any given time, there may be hundreds of Tweets visible on a single page, each with its own set of Tweet actions (that appear on hover), in addition to other dynamic content like follow buttons, the details pane, and cached fragments of previously-seen timelines or pages. We need to handle interactivity across this complex application without introducing sluggishness, or memory bloat. When adding new elements to the page, they need to be immediately interactive, and we don’t have time to waste in wiring up event handlers.

It’s no surprise that we’re using event delegation almost universally in our application. We’ve broken the entire application into logical components, and each maintains its own DOM tree and delegates all events off its own root node. As interface pieces come and go, the delegated handlers work seamlessly to capture new events. When we do remove elements from the DOM, we ensure that the handlers remain (using jQuery’s .detach) so we can re-attach it later without incurring more work. When things are finally thrown away, we are careful to ensure handlers can be garbage collected.

URL Routing

Many people have noticed that the new Twitter.com brought a small change to the site’s URL structure: the introduction of a /#!/ at the front of the path. This is a simple change that most front-end engineers are already familiar with, but with huge performance savings. We can track changes in the application state using the URL, but without a new page load. We also get history and back button support in modern browsers, and can update state as the user edits the URL due to HTML5’s new hashchange event. The #! is there to comply with Google’s Ajax crawling specification, so we don’t lose out on search engine rankings with the new scheme.

The Big Picture

The combination of a high-performance API caching layer with fast document rendering and no page reloads mean this application works extremely fast. Fast enough that most users will not even realize they’re not using a native desktop application. We’re proud of the work that we’ve done so far, and are equally excited to continue making it faster. All of this was accomplished with significant effort from the entire #newtwitter and @anywhere engineering teams, composed of the following talented front-end engineers: @dsa, @ded, @mracus, @danwrong, @esbie,
@hoverbird, and @bs. Feel free to reach out to me (@bcherry) or any of the other team members for more info on our performance work in the new Twitter.com. Thanks for reading!

Ben Cherry photo

Ben Cherry (@bcherry) is a front-end engineer at Twitter, where he helped build the new Twitter.com.