Skip to content

Jesses Software Engineering Blog

Jan 13

Jesse

Website Performance with Browser Caching: Part 2 – Dynamic Files

As outlined in the previous article, browsers will cache static files by default. However, HTML files and scripts, PHP in this case, do not cache by default because they do not return ETag or Last-Modified values. Also, scripting files are used to generate dynamic content, so caching such requests would be counter productive as the same request can produce different responses. But just because it doesn’t happen by default does not mean you should not turn it on.

PHP offers you a way to set headers for your response with the header() function. I’m not going to go into detail on it’s use here, but note that it must be called before any other output is sent. To set cache headers for your PHP scripts, as seen in the previous article, you could do:

header("Cache-Control: public, must-revalidate"); 
header("Expires: Sat, 28 Jan 2014 05:00:00 GMT"); 

Alternatively you can set it globally in your php.ini file:

session.cache_limiter = public
session.cache_limiter = 180

Be aware that your PHP scripts will be cached based on your routes. If you set a cache time for a dynamic script, whatever data is loaded when the user first views that script, will be what they continue to see until the cache expires. And unlike static files where you can just change a version number, you will have to change your whole route to force the user’s browser cache to pull fresh data. So you have to be very conservative when setting cache times for dynamic scripts to avoid users seeing stale data, as well as plan your caching strategy carefully. Next I will discuss the strategy I used for my portfolio site, jessenet.com.

For my portfolio everything is static except for my blog. Since content does not change, or rather rarely changes, there is no need for the client to make the server request on every page load. Every request is routed to my index file which instantiates my framework, then creates the dispatcher and the router, figures out the route, pulls up the controller, and returns the static view. It is not efficient to have to do this on every request. By implementing header caching in the script, none of that happens, content is just served directly from the browser’s cache. So I decided for all my site, except for the blog, I will cache my dynamic content until midnight every night. That way a user who wants to spend some time browsing through my site will cache all dynamic content for the day, and if I push changes to my dynamic scripts, a user will be out of date for at most a day. I do this by manipulating the Response object of my framework in my index, or bootstrap, file:

/**
 * Setup response caching values
 */
$di->set('response', function(){
    $response = new \Phalcon\Http\Response();

    // we want the cache to expire at midnight Phx time every night
    $s = strtotime('midnight +1 day') - time();

    $ExpireDate = new \DateTime();
    $ExpireDate->modify('+' . $s . ' seconds');

    $response->setExpires($ExpireDate);

    $response->setHeader('Pragma', 'cache');
    $response->setHeader('Cache-Control', 'public, max-age=' . $s . ', s-max=' . $s . ', must-revalidate, proxy-revalidate');
    return $response;
});

The code will set the headers for all requests through my framework and will set the cache for a request to expire at midnight Phoenix, AZ time (date_default_timezone_set() was called earlier). It also specifies that the dynamic content can be cached publicly and that after the expiration the caches must be re-validated both in the local browser cache and in the proxy or CDN caches.

For my blog, all of the dynamic content is stored in a MySQL database. When a user pulls up my blog or an article, not only does the request/dispatch/route logic need to be fired through my framework but the database needs to be connected to and all the WordPress files are included. I don’t write many articles, so there’s no reason for this to happen every time a user loads a blog related page. And when I do make changes they don’t need to show up immediately to my users. So for this I set a different cache time based on my routes. Most PHP frameworks give you very precise control over your headers by making Response objects accessible through out the application. You can manipulate the headers on the application, controller, or even action level.

/**
 * Set a quick cache for more dynamic pages
 */
protected function fastCache()
{	
    $ExpireDate = new \DateTime();
    $ExpireDate->modify('+1 hour');
    $response = $this->di['response'];
    $response->setExpires($ExpireDate);
    $response->setHeader('Cache-Control', 'public, max-age=3600, s-max=3600, must-revalidate, proxy-revalidate');
}

/**
 * Used as a controller constructor
 */
public function initialize()
{
    // set a quicker cache
    $this->fastCache();
}

I made a function in my BaseController that is accessible to all controllers and actions. Then in my blog controller I simply call it via my constructor function, overwriting the response headers for all actions of that controller. Now all of the blog related pages will be cached for an hour. So as a user navigates through various blog posts, when they return to a previous route, the page will be loaded from the browser’s cache. And since different articles have different routes, they can still pull up any article they want.

After implementing the changes and going back to monitoring my website as in the previous article, you notice that the PHP scripts are now also being pulled from the cache:

Name Status Type Size Time
jessesnet.com 200 text/html (from cache) 0 ms
bootstrap.min.css 200 text/css (from cache) 0 ms
slide1.png 200 image/png (from cache) 0 ms
bootstrap.min.js 200 text/javascript (from cache) 0 ms
TOTALS: 20 requests - 165 B 151 ms

We have now achieved the caching of everything involved in our server response. We have a load time of 151 ms, which is 15% of what it was originally, and data transfer of 165 B, 4% of the original transfer size. That is a substantial increase in both efficiency and bandwidth usage.

Hopefully this has shown you just how powerful both static and dynamic browser caching can be. You have as much control over browser caching as you need, and if leveraged correctly you can significantly improve your site’s performance.

Part 1: Increasing Website Performance with Browser Caching: Static Files

Blog Powered By Wordpress