Item 55: Pregenerate content to minimize processing



Item 55: Pregenerate content to minimize processing

While some applications need to be entirely dynamic, a large class of applications doesn't need to render its output completely at runtime. For example, consider Amazon.com for a moment—in essence, the entire Web site is a read-mostly environment, where 99% of the users on the site are viewing exactly the same content over and over again for a given item. While we could probably build a single ViewItem.jsp page to display the details (publisher, price, advertising copy, sample chapters, and reader reviews) for any arbitrary item in Amazon.com's inventory, this would introduce huge amounts of load onto the server, thus reducing the overall performance and scalability of the site.

Despite the huge number of items in Amazon.com's inventory, this is a bound set of data that won't change very often. Granted, prices will change, reader reviews will be added and/or removed, new advertising copy may be inserted, and so on, but these changes will likely be measured by the week rather than the minute or second. Most prospective readers won't really care if the Amazon sales rank is up-to-the-second accurate (although some book authors might).

If the page is entirely dynamically generated, however, every request is going to require at least one trip to the database to fetch the data to dynamically plug into the page (although we would hope to keep the number of trips to a minimum in order to minimize latency—see Item 17). This is not only going to create latency in the page waiting for the data to return but could also conceivably create contention (see Item 29) on the database server if these requests are naïvely transacted. Worse, because the data isn't changing frequently, all the transaction support and database querying is pretty worthless, since 99.9% of the time it's exactly the same as it was last request. Unfortunately, database caching won't be of much use here; the site will likely support thousands of concurrent users at a time, so unless the database is able to cache hundreds of thousands of queries, the likelihood of a cache hit is pretty minimal.

Rather than go through the exercise of pulling the same data back for each request, instead pregenerate a static page that already contains the data fixed in place as constant values. The actual act of pregeneration itself can be buried within the Web application—a servlet can generate a JSP page into the Web application directory, for example, or at worst a generalized JSP page can dynamically include an external resource into place. On modification, then, the editing servlet can simply regenerate the JSP or external resource to reflect the new changes. While this will create a large number of pages, you can usually mitigate this by putting the pages under a subdirectory on the site, identified by the primary key of the item (the ISBN, for example, in the case of books on a bookseller's site).

Note that this approach works well only for read-only or read-mostly presentation resources. Portals, for example, fit into this category, since most users select which parts of the portal they want to see and don't change those preferences very often. Thus, creating a per-user pregenerated portal page can save a tremendous amount of processing, particularly since we often want the home page of the portal to be as fast as possible.

In some cases, we can achieve a certain balance of pregeneration and per-request rendering by creating a cache (via a servlet filter that accesses an in-memory buffer or some other temporary storage) that holds generated content to be handed back from the cache, rather than by going through the entire page logic again. This has the desired benefit of not having to make the numerous calls and buffer-appends necessary to create the content, but you do run into some problems, most notably that you must be sure that the cache doesn't bloat the memory footprint of the system as a whole, thus reducing scalability. In general, if you take a caching approach, make sure that the only items cached are those usable by every user on the site; caching items on a per-user basis is a slippery slope that leads to massive memory requirements per user. Also, when caching rendered output, you run into the same update propagation problem described in Item 4, since now your caching filter needs to be made aware of changes to the presentation layer it's caching (or needs to discover those changes, which takes time and could reduce the efficacy of the cache).

Assuming you can figure out a quick and easy way to know when to invalidate the cache, and assuming it's applied judiciously to various elements of the site—there's no point in putting a caching filter in front of a static GIF or JPG image, for example—a caching servlet filter can yield some of the same benefits of pregeneration.

Pregeneration has other benefits, too. If we can pregenerate to HTML (rather than JSP), the resource in question becomes entirely static and all the caching nodes between the end user's browser and the HTTP server can kick in and reduce the latency of the request even further. Although not all pages can render to static HTML, pregenerating the content when you can, particularly for the entry point of the Web application, can make a powerful first impression on users working with the application for the first time: "Wow, that's fast."