Optimising Zend Framework applications (2) – cache pages and PHP accelerator [updated]

[continue from previous post]

4. Use an op-code cache/accelerator (Apc, XCache)

PHP is very fast but it’s not compiled. A op-code cache helps. See this comparison as an example of what could be the performance increase. That does not mean we can skip the other optimisation, code bottlenecks should be removed anyway.
Optimising code is also helpful to understand what are the common code “slowness” and avoid writing them again in the future.

5. Cache pages (before zend framework bootstrap)

Even though you have optimised the code, you still have to bootstrap and run the zend application, and the whole process takes time (dispatching, controller logic, scripts  render). An solution I’ve recently used is caching the whole HTML as a result of the processing (see another post about caching HTML pages of a generic website).

There are many solutions ways to cache the output (apache modules, reverse proxies, zend server page cache). The best depends on the needs. Moving the logic to the application level usually allows more customisations.

Page caching can be done by using Zend_Cache_Frontend_Page. It basically uses ob_start() with a callback that saves the result into cache. I haven’t found any interesting article about its best use, except the fact that it should be used in a controller plugin. I’d say it’s better to instantiate a separate cache object and activate it directly into the index.php and caching before the zend application actually start (boostrap). In my local enviroment, when the page cache is valid, the response time is 2 ms, against 800 ms required to bootstrap and load the application.
See the following code to see where to place it. Note: I instantiate Zend_Application with no options just to have the autoloader available to load the needed classes.

# index.php

// create APPLICATION_* vars and set_include_path [...]

require_once 'Zend/Application.php';
$application = new Zend_Application(APPLICATION_SERVERNAME);
// Zend_Cache_Frontend_Page
$pageCache = Zend_Cache::factory(
    'Page', 'File',
    array(
        'caching' => true,
        'lifetime'=>300, //5 minutes
        'ignore_user_abort' => true,
        'ignore_user_abort' => 100,
        'memorize_headers' => true,
        // enable debug only on localhost
        'debug_header' => APPLICATION_ENV == 'localhost',
        'default_options' => array(
            'cache' => true,
            // test the following, depends on how you use sessions and browser plugins
            'cache_with_cookie_variables' => true,
            'cache_with_post_variables' => false
        ),
        // whitelist approach
        'regexps' => array(
           '^.*$' => array('cache'=>false),
            /* homepage */
            '^/$' => array('cache'=>true, 'specific_lifetime'=>650),
            /* example of other pages*/
            '^/(pagex|pagey)/' => array('cache'=>true,'specific_lifetime'=>700),
            // []...
        )
    ),
    array(
        'cache_dir' => APPLICATION_PATH . '/data/pagecache',
        'hashed_directory_level'=>2,
        'file_name_prefix'=>'zendpagecache'
    )
);
// start page cache, except for cron job and when user is logged
// note: I haven't tested yet if using Zend_Auth here is a good solution
if (PHP_SAPI !=='cli' && !Zend_Auth::getInstance()->hasIdentity()) {
  $pageCache->start();
}
// the following code is not executed when page cache is valid
$appSettings = new Zend_Config_Ini(APPLICATION_PATH . '/configs/sites.ini', APPLICATION_SERVERNAME);
$application->setOptions($appSettings->toArray());

$application->bootstrap();

if (PHP_SAPI !=='cli') {
    $application->run();
}

Of course page caching must be set carefully, depending on the traffic of the application. Ideally, a customised cron job should fetch the pages, invalidate the cache for each one and rebuild them before they normally expire, so that the user will always find the fast cached page. On my project a similar system has improved the average loading time by 8 times (so an external spider will consider the site in a different way). Links to keep cached should be at least the ones on the sitemap.

Set carefully all the settings of zend page cache frontend: cookie, get and post data, regexpr of URLs to cache. Note that no logic is executed when the cache is valid, so an eventual visitors counter or any other database writing query will not work.