Article

Cache it! Solve PHP Performance Problems

Page: 1 2 3 4 5 Next

Discussion

For an example that shows how to use PHP's output buffering capabilities to handle errors more elegantly, have a look at the PHP Freaks article Introduction to Output Buffering, by Derek Ford.

What About Template Caching?

Template engines often include template caching features--Smarty is a case in point. Usually, these engines offer a built-in mechanism for storing a compiled version of a template (that is, the native PHP generated from the template), which prevents us developers from having to recompile the template every time a page is requested.

This process should not be confused with output--or content--caching, which refers to the caching of the rendered HTML (or other output) that PHP sends to the browser. In addition to the content cache mechanisms discussed in this chapter, Smarty can cache the contents of the HTML page. Whether you use Smarty's content cache or one of the alternatives discussed in this chapter, you can successfully use both template and content caching together on the same site.

HTTP Headers and Output Buffering

Output buffering can help solve the most common problem associated with the header function, not to mention the issues surrounding session_start and set_cookie. Normally, if you call any of these functions after page output has begun, you'll get a nasty error message. When output buffering's turned on, the only output types that can escape the buffer are HTTP headers. If you use ob_start at the very beginning of your application's execution, you can send headers at whichever point you like, without encountering the usual errors. You can then write out the buffered page content all at once, when you're sure that no more HTTP headers are required.

Use Output Buffering Responsibly
While output buffering can helpfully solve all our header problems, it should not be used solely for that reason. By ensuring that all output is generated after all the headers are sent, you'll save the time and resource overheads involved in using output buffers.

How do I cache just the parts of a page that change infrequently?

Caching an entire page is a simplistic approach to output buffering. While it's easy to implement, that approach negates the real benefits presented by PHP's output control functions to improve your site's performance in a manner that's relevant to the varying lifetimes of your content.

No doubt, some parts of the page that you send to visitors will change very rarely, such as the page's header, menus, and footer. But other parts--for example, the list of comments on your blog posts--may change quite often. Fortunately, PHP allows you to cache sections of the page separately.

Solution

Output buffering can be used to cache sections of a page in separate files. The page can then be rebuilt for output from these files.

This technique eliminates the need to repeat database queries, while loops, and so on. You might consider assigning each block of the page an expiry date after which the cache file is recreated; alternatively, you may build into your application a mechanism that deletes the cache file every time the content it stores is changed.

Let's work through an example that demonstrates the principle. Firstly, we'll create two helper functions, writeCache and readCache. Here's the writeCache function:

smartcache.php (excerpt)    
 
<?php    
function writeCache($content, $filename)    
{    
$fp = fopen('./cache/' . $filename, 'w');    
fwrite($fp, $content);    
fclose($fp);    
}

The writeCache function is quite simple; it just writes the content of the first argument to a file with the name specified in the second argument, and saves that file to a location in the cache directory. We'll use this function to write our HTML to the cache files.

The readCache function will return the contents of the cache file specified in the first argument if it has not expired--that is, the file's last modified time is not older than the current time minus the number of seconds specified in the second argument. If it has expired or the file does not exist, the function returns false:

smartcache.php (excerpt)  
   
function readCache($filename, $expiry)    
{    
 if (file_exists('./cache/' . $filename))    
 {    
   if ((time() - $expiry) > filemtime('./cache/' . $filename))    
   {    
     return false;  
   }  
   $cache = file('./cache/' . $filename);  
   return implode('', $cache);  
 }  
 return false;  
}

For the purposes of demonstrating this concept, I've used a procedural approach. However, I wouldn't recommend doing this in practice, as it will result in very messy code and is likely to cause issues with file locking. For example, what happens when someone accesses the cache at the exact moment it's being updated? Better solutions will be explained later on in the chapter.

Let's continue this example. After the output buffer is started, processing begins. First, the script calls readCache to see whether the file header.cache exists; this contains the top of the page--the HTML <head> tag and the start <body> tag. We've used PHP's date function to display the time at which the page was actually rendered, so you'll be able to see the different cache files at work when the page is displayed:

smartcache.php (excerpt)    
 
 ob_start();    
 if (!$header = readCache('header.cache', 604800))    
 {    
?>    
<!DOCTYPE html public "-//W3C//DTD XHTML 1.0 Transitional//EN"    
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">    
<html xmlns="http://www.w3.org/1999/xhtml">    
 <head>    
   <title>Chunked Cached Page</title>    
   <meta http-equiv="Content-Type"    
     content="text/html; charset=iso-8859-1"/>    
   </head>    
   <body>    
     <p>The header time is now: <?php echo date('H:i:s'); ?></p>    
<?php    
   $header = ob_get_contents();    
   ob_clean();  
   writeCache($header,'header.cache');  
 }

Note what happens when a cache file isn't found: the header content is output and assigned to a variable, $header, with ob_get_contents, after which the ob_clean function is called to empty the buffer. This allows us to capture the output in "chunks" and assign them to individual cache files with the writeCache function. The header of the page is now stored as a file, which can be reused without our needing to rerender the page. Look back to the start of the if condition for a moment. When we called readCache, we gave it an expiry time of 604800 seconds (one week); readCache uses the file modification time of the cache file to determine whether the cache is still valid.

For the body of the page, we'll use the same process as before. However, this time, when we call readCache, we'll use an expiry time of five seconds; the cache file will be updated whenever it's more than five seconds old:

smartcache.php (excerpt)    
 
if (!$body = readCache('body.cache', 5))    
 {    
   echo 'The body time is now: ' . date('H:i:s') . '<br />';    
   $body = ob_get_contents();    
   ob_clean();    
   writeCache($body, 'body.cache');    
 }

The page footer is effectively the same as the header. After the footer, the output buffering is stopped and the contents of the three variables that hold the page data are displayed:

smartcache.php (excerpt)    
 
 if (!$footer = readCache('footer.cache', 604800)) {    
?>    
   <p>The footer time is now: <?php echo date('H:i:s'); ?></p>    
 </body>    
</html>    
<?php  
   $footer = ob_get_contents();  
   ob_clean();  
   writeCache($footer, 'footer.cache');  
 }  
 ob_end_clean();  
 
 echo $header . $body . $footer;  
?>

The end result looks like this:

The header time is now: 17:10:42  
The body time is now: 18:07:40  
The footer time is now: 17:10:42

The header and footer are updated on a weekly basis, while the body is updated whenever it is more than five seconds old. If you keep refreshing the page, you'll see the body time updating.

Discussion

Note that if you have a page that builds content dynamically, based on a number of variables, you'll need to make adjustments to the way you handle your cache files. For example, you might have an online shopping catalog whose listing pages are defined by a URL such as:

http://example.com/catalogue/view.php?category=1&page=2

This URL should show page two of all items in category one; let's say this is the category for socks. But if we were to use the caching code above, the results of the first page of the first category we looked at would be cached, and shown for any request for any other page or category, until the cache expiry time elapsed. This would certainly confuse the next visitor who wanted to browse the category for shoes--that person would see the cached content for socks!

To avoid this issue, you'll need to incorporate the category ID and page number in to the cache file name like so:

 $cache_filename = 'catalogue_' . $category_id . '_' .  
   $page . '.cache';    
 if (!$catalogue = readCache($cache_filename, 604800))    
 {  
   ...display the category HTML...  
 }

This way, the correct cached content can be retrieved for every request.

Nesting Buffers
You can nest one buffer within another practically ad infinitum simply by calling ob_startmore than once. This can be useful if you have multiple operations that use the output buffer, such as one that catches the PHP error messages, and another that deals with caching. Care needs to be taken to make sure that ob_end_flush or ob_end_clean is called every time ob_start is used.

If you liked this article, share the love:
Print-Friendly Version Suggest an Article

Sponsored Links