Splitting the Initial Payload: Chapter 3 - Even Faster Websites

by Steve Souders
Even Faster Web Sites book cover

This excerpt is from Even Faster Web Sites .

Souders and eight expert contributors provide best practices and pragmatic advice for improving your site's performance in three critical categories: JavaScript, in the network, and in the browser.

buy button

The growing adoption of Ajax and DHTML (Dynamic HTML) means today’s web pages have more JavaScript and CSS than ever before. Web applications are becoming more like desktop applications, and just like desktop applications, a large percentage of the application code isn’t used at startup. Advanced desktop applications have a plug-in architecture that allows for modules to be loaded dynamically, a practice that many Web 2.0 applications could benefit from. In this chapter I show some popular Web 2.0 applications that load too much code upfront, and I discuss approaches for making pages load more dynamically.

Kitchen Sink

Facebook has 14 external scripts totaling 786 KB uncompressed.[5] Figuring out how much of that JavaScript is necessary for the initial page to render is difficult to do, even for a core Facebook frontend engineer. Some of those 14 external scripts are critical to rendering the initial page, but others were included because they support Ajax and DHTML functionality, such as the drop-down menus and the Comment and Like features shown in Figure 3.1, “Facebook Ajax and DHTML features”.

It’s critical to render a web page as quickly as possible. Doing so engages the user and creates a responsive experience for her. Imagine if the Facebook JavaScript could be split into two parts: what’s needed to render the initial page, and everything else. Rather than bog down the user’s first impression with “everything else,” the initial JavaScript download should include only what’s necessary for the initial rendering. The remaining JavaScript payload can be loaded later.

Figure 3.1. Facebook Ajax and DHTML features

Facebook Ajax and DHTML features

This raises several questions:

  • How much does this save?

  • How do you find where to split the code?

  • What about race conditions?

  • How do you download “everything else” later?

The first three questions are tackled in this chapter. How to load “everything else” is the topic of Chapter 4, Loading Scripts Without Blocking.

Savings from Splitting

It turns out that Facebook executes only 9% of the downloaded JavaScript functions by the time the onload event is called. This is computed by using Firebug’s JavaScript profiler to count all the functions executed up to the onload event.[6] The counting stops at the onload event because functionality needed after this point can, and should, be downloaded after the initial page has rendered. I call this a post-onload download. (See Chapter 4, Loading Scripts Without Blocking for various lazy-loading techniques.)

Table 3.1, “Percentage of JavaScript functions executed before onload” shows the percentage of functions downloaded that are not executed before the onload event for 10 top U.S. web sites. On average, 75% of the functions downloaded are not executed during the initial rendering of the page. Thus, if downloading of these unexecuted functions was deferred, the size of the initial JavaScript download would be dramatically reduced.

Admittedly, the 75% estimate might be exaggerated; some of the unexecuted functions might be required for error handling or other special conditions. The estimate is still useful to illustrate the point that much of the JavaScript downloaded initially could be deferred. The average total amount of JavaScript is 252 KB uncompressed. This percentage is in terms of function count, not size. If we assume a constant function size, 75% represents an average 189 KB that doesn’t have to be downloaded until after the onload event, making the initial page render more quickly.

Table 3.1. Percentage of JavaScript functions executed before onload

Finding the Split

Firebug’s JavaScript profiler shows the names of all the functions that were executed by the time of the onload event. This list can be used to manually split the JavaScript code into one file loaded as part of the initial page rendering and another file to be downloaded later. However, because some of the unused functions may still be necessary for error-handling and other conditional code paths, splitting the code into an initial download that is complete without undefined symbols is a challenge. JavaScript’s higher-order features, including function scoping and eval, make the challenge even more complicated.

Doloto is a system developed by Microsoft Research for automatically splitting JavaScript into clusters. The first cluster contains the functions needed for initializing the web page. The remaining clusters are loaded on demand the first time the missing code needs to execute, or they are lazy-loaded after the initial flurry of JavaScript activity is over. When applied to Gmail, Live Maps, Redfin, MySpace, and Netflix, Doloto reduced the initial JavaScript download size by up to 50% and reduced the application load time by 20% to 40%.

Doloto’s decisions about where to split the code are based on a training phase and can result in the JavaScript being split into multiple downloads. For many web applications, it is preferable to define a single split at the onload event, after which the remaining JavaScript is immediately downloaded using the nonblocking techniques described in Chapter 4, Loading Scripts Without Blocking. Waiting to start the additional downloads on demand after the user has pulled down a menu or clicked on a page element forces the user to wait for the additional JavaScript to arrive. This wait can be avoided if all the additional JavaScript is downloaded after the initial page rendering. Until Doloto or other systems are publicly available, developers need to split their code manually. The following section discusses some of the issues to keep in mind when doing this.

Undefined Symbols and Race Conditions

The challenge in splitting your JavaScript code is to avoid undefined symbols. This problem arises if the JavaScript being executed references a symbol that has, mistakenly, been relegated to a later download. In the Facebook example, for instance, I suggest that the JavaScript for drop-down menus should be loaded later. But if the drop-down menu is displayed before the required JavaScript is downloaded, there’s a window in which the user can click on the drop-down menu and the required JavaScript won’t be available. My suggestion would then have created a race condition where the JavaScript is racing to download while the user is racing to click the menu. In most cases, the JavaScript will win the race, but there is a definite possibility that the user may click first and experience an undefined symbol error when the (yet to be downloaded) drop-down menu function is called.

In a situation where the delayed code is associated with a UI element, the problem can be avoided by changing the element’s appearance. In this case, the menu could contain a “Loading…” spinner, alerting the user that the functionality is not yet available.

Another option is to attach handlers to UI elements in the lazy-loaded code. In this example, the menu would be rendered initially as static text. Clicking on it would not execute any JavaScript. The lazy-loaded code would both contain the menu functionality and would attach that behavior to the menu using attachEvent in Internet Explorer and addEventListener in all other browsers.[7]

In situations where the delayed code is not associated with a UI element, the solution to this problem is to use stub functions. A stub function is a function with the same name as the original function but with an empty function body or temporary code in place of the original. The previous section described Doloto’s ability to download additional JavaScript modules on demand. Doloto implements this on-demand feature by inserting stub functions in the initial download that, when invoked, dynamically download additional JavaScript code. When the additional JavaScript code is downloaded, the original function definitions overwrite the stub functions.

A simpler approach is to include an empty stub function for each function that is referenced but relegated to the later download. If necessary, the stub function should return a stub value, such as an empty string. If the user tries to invoke a DHTML feature before the full function implementation is downloaded, nothing happens. A slightly more advanced solution has each stub function record the user’s requests and invokes those actions when the lazy-loaded JavaScript arrives.

Case Study: Google Calendar

A good example of splitting the initial payload is Google Calendar. Figure 3.2, “Google Calendar HTTP waterfall chart” shows the HTTP requests that are made when Google Calendar is requested. I call these charts HTTP waterfall charts. Each horizontal bar represents one request. The resource type is shown on the left. The horizontal axis represents time, so the placement of the bars shows at what point during page load each resource was requested and received.

Figure 3.2. Google Calendar HTTP waterfall chart

Google Calendar HTTP waterfall chart

Google Calendar requests five scripts totaling 330 KB uncompressed. The payload is split into an initial script of 152 KB that is requested early (the third bar from the top). The blocking behavior of this script is mitigated by the fact that it contains less than half of the total JavaScript. The rest of the JavaScript payload is requested last, after the page has been allowed to render.

By splitting their JavaScript, the Google Calendar team creates a page that renders more quickly than it would have if all of the JavaScript were loaded in one file. Splitting a web application’s JavaScript is not a simple task. It requires determining the functions needed for initial rendering, finding all required code dependencies, stubbing out other functions, and lazy-loading the remaining JavaScript. Further automation for these tasks is needed. Microsoft’s Doloto project describes such a system, but as of this writing, it’s not available publicly. Until tools such as this are made available, developers will have to roll up their sleeves and do the heavy lifting themselves.

This chapter has focused on splitting JavaScript, but splitting CSS stylesheets is also beneficial. The savings are less than those gained by splitting JavaScript because the total size of stylesheets is typically less than JavaScript, and downloading CSS does not have the blocking characteristics that downloading JavaScript has.[8] This is another opportunity for further research and tool development.

[5] Fourteen scripts are downloaded when logged-in users visit this page. If the user is not logged in, fewer scripts are used.

[6] Firebug is the preeminent web development tool, available at http://getfirebug.com/.

[8] Firefox 2 is the one exception.

If you enjoyed this excerpt, buy a copy of Even Faster Web Sites .