Steven Benner’s Blog

Random articles about programming, computing, and the internet.

Caching with CodeIgniter: Zen, headaches and performance

Warning: This article is very old

This article was written in 2011. Things change over time. So the information presented here may no longer be current and up-to-date, or it may have even become factually inaccurate. There may be useful information here, but you should check recent sources before relying on this article.

CodeIgniter logo

CodeIgniter is already a very fast PHP framework, but that alone might not be fast enough for high traffic web applications. To get to the next level of performance you will need to implement some kind of caching.

Luckily for the CodeIgniter crowd, the framework comes with one of the fastest caching systems possible, Output Caching. Short of writing static HTML files or output caching to memory there is no faster way to serve pages.

However, if you have any degree of interactive or dynamic content, total output caching can be painful (if not impossible) to implement. Now there are other options, such as database caching and third party caching libraries, but none of them will be quite as fast as full output caching. So if at all possible that is what you should use.

Let me provide you with an overview of the caching systems available and a few of my tricks.

CodeIgniter Output Caching

CodeIgniter’s output caching system will take the completely rendered output of your views and save them to disk. It’s a very simple idea, and simple is good for performance. When a page is cached there is no need to talk to the database or process anything.

Output caching is actually built directly in to the CodeIgniter life cycle. Every time CodeIgniter starts up it checks to see if the current URL has a cached version on disk. If it finds it then CodeIgniter will completely skip everything and throw the cached version out to the browser.

It’s also incredibly easy to turn on, just call the cache() method anywhere in in your controller to output cache the page.

$this->output->cache(MINUTES);

Overcoming the problems of output caching

There is one major downside to output caching, when a page is cached you cannot talk to the database or process anything. The greatest benefit is also the greatest curse. This means that anything the least bit dynamic or user derived cannot be cached this way.

Unfortunately CodeIgniter does not come with partial caching, support for dependencies or even a way to evict items from cache. To me at least, this is the single biggest hole in the awesomeness that is CodeIgniter. Just a little bit more love for the output caching system would make the most scalable PHP framework vastly more scalable.

But we can work around these issues with a little bit of extra thought and design.

What output caching does parse

It’s worth mentioning that there are two functions/strings that output caching will parse, even after the file is cached, the benchmark class’ elapsed_time() and memory_usage() functions. These functions will insert text markers ({elapsed_time} and {memory_usage}, respectively) that are parsed by CodeIgniter when it sends the output to the browser.

This always makes for some interesting figures so it’s nice to include a HTML comment in your pages with these functions to watch just how fast the output cache is working.

<!--
	Time: <?php echo $this->benchmark->elapsed_time(); ?>
	Mem: <?php echo $this->benchmark->memory_usage(); ?>
-->

It’s quite common to see these values go from 0.07 seconds with 3.5MB of memory to 0.006 seconds with 0.75MB of memory. Output caching really does work that well!

The small things

For most pages on most web applications there are just a few small items that need to by dynamic, such as Login/Logout links, recently viewed items and pretty dates (x days ago). All of these things can be effectively done via JavaScript without losing any significant functionality.

So build a simple API to make the functions available via JSON. For example this is a basic controller that would allow a JavaScript to get the users current status (logged-in, username and group).

class Api extends CI_Controller
{
	public function user_status()
	{
		$logged_in = $this->user_model->logged_in();
		$data = array(
			'isLoggedIn' => $logged_in,
			'userId' => $logged_in ? $this->session->userdata('user_id') : '',
			'userName' => $logged_in ? $this->user_model->get_user()->username : '',
			'userGroup' => $logged_in ? $this->user_model->get_user()->group : ''
		);
		$this->output->set_header('Content-type: application/json');
		$this->output->set_output(json_encode($data));
	}
}

Now with that data exposed via an API you can use JavaScript to show the user controls you find at the top of many web applications. Here is an example script using jQuery that would do just that.

$.getJSON('/api/user_status', function(data) {
	if (data.isLoggedIn) {
		$('#user-controls').html('Logged in as <b>'+data.userName+'</b> | ' +
			'<a href="/logout">Log Out</a>');
	} else {
		$('#user-controls').html('<a href="/register">Register</a> | ' +
			'<a href="/login">Log In</a>');
	};
});

On an average web app this is still at least an order of magnitude faster than not using caching at all since you are only making one query to the database. The page load will finish much faster, then when the page is ready the supplementary content will be loaded.

Now obviously there is a significant down side to this solution; users with JavaScript disabled will not see the login and register links. You could simply show those links by default and overwrite them with script, but for most applications it is a non-issue since many features that require a user to log in will also require that JavaScript is enabled anyway. It’s a judgment call for you to make.

The big things

For entire pages that are filled with content that changes regularly you will need a different technique. The best solution that I have found is evicting the cache for a page when you change the data. So when someone posts something you will call a function to clear the cache files for any pages affected.

The preserves the massive performance benefit of keeping output caching enabled but still makes the page completely dynamic.

However, as I said earlier, CodeIgniter does not give you any function to delete cache files so you have to do it yourself. Output cache files are saved in your cache folder (config item) as an MD5 hash of the URI they represent. This is the exact code CodeIgniter uses:

$path = $CI->config->item('cache_path');
$cache_path = ($path == '') ? APPPATH.'cache/' : $path;
[ ........ ]
$uri =	$CI->config->item('base_url').
		$CI->config->item('index_page').
		$CI->uri->uri_string();

$cache_path .= md5($uri);

Note: Actually this is for the latest CodeIgniter 2.0. For 1.7 replace APPPATH with BASEPATH.

So you have to search for the cache files you want to purge using that algorithm and unlink them. Here is a simple little helper I wrote to do just that.

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

/**
 * Delete Cache File
 *
 * Evicts the output cache for the targeted page.
 *
 * @author	Steven Benner
 * @link	https://stevenbenner.com/2010/12/caching-with-codeigniter-zen-headaches-and-performance/
 * @param	string	$uri_string	Full uri_string() of the target page (e.g. '/blog/comments/123')
 * @return	bool	True if the cache file was removed, false if it was not
 */
if ( ! function_exists('delete_cache'))
{
	function delete_cache($uri_string)
	{
		$CI =& get_instance();
		$path = $CI->config->item('cache_path');
		$cache_path = ($path == '') ? APPPATH.'cache/' : $path;

		$uri = $CI->config->item('base_url').
			$CI->config->item('index_page').
			$uri_string;

		$cache_path .= md5($uri);

		if (file_exists($cache_path))
		{
			return unlink($cache_path);
		}
		else
		{
			return TRUE;
		}
	}
}

You may want to just download my complete Cache Helper from GitHub. Place cache_helper.php in your helpers directory and you’re good to go. Now you can evict cache files like this:

$this->load->helper('cache');

delete_cache('/blog/comments/123');

CodeIgniter Database Caching

Another caching system that comes with CodeIgniter is the database caching system. This system, as it’s name implies, only caches database responses from your queries. Leaving your PHP code to do all of the dynamic stuff you want without needing to requery the database for every page load.

Implementing query caching

Database caching will greatly reduce the load on your database and increase the performance of your application as a result. It’s also easy to turn on. There are two settings in the database config file you need to modify. Just flip the cache_on switch and set a cachedir that is writable. With those options set you are up and running with database caching.

Once you have query caching up and running you will need to clear the caches whenever you do an update. Thanks to the convenient cache_delete function this too is very easy.

$this->db->cache_delete('controller', 'method');

Possible issues

Database caching has a few quirks that may or may not be a problem for you:

The biggest problem is that the database caching simply isn’t sharp enough for many situations. The all-or-nothing structure can get annoying really quickly. If you’re going to use database sessions then you’re probably going to want to run a cron job to completely purge the cache every night.

Third Party Caching

Several other more advanced and more flexible caching systems have been created by other CodeIgniter developers around the world. For some situations you may find them to be far better alternatives than the caching systems that CodeIgniter offers.

Things I wish CodeIgniter supported

One day I’ll probably stop being lazy and extend the caching system to add these features, but until that day these are the biggest things that I wish EllisLab would implement in CodeIgniter’s caching system:

Conclusion

The caching systems in CodeIgniter have their pros and cons, and quirks, but the simplicity of the functions is nice in it’s own way, almost zen like in their simplicity. Of course I wish EllisLabs would improve and expand on this excellent base. Fortunately, thanks to the work of other developers we have some options for more complex caching needs.

So did I miss anything? Do you know of any other CI caching libraries that are worth mentioning here. Or do you know of any other techniques to squeeze some more flexibility out of the CI caching systems? I’d love to hear how you made caching work for your needs. Please add a comment below.

Comments

Mitchell McKenna’s avatar Mitchell McKenna

Great article on the state of caching in the codeigniter world, glad you mentioned a couple of the good 3rd party libraries. I’m betting improvements to the built-in cache functionality will be one of the focuses for the new CodeIgniter Community Branch, it’s in the top 15 feature requests.

John_Betong’s avatar John_Betong

Very interesting article and I will try to implement more of your suggestions.

I had an infuriating problem with caching. I was using “Email a friend” feature which sent an email with an included URL. Unfortunately the popup window was not sending any URL. I managed to get round this by trapping the URL and not setting the cache:

// MY_Controller
//==================================================
function __construct()
{
parent::__construct();

$pages_not_cached = array
(
‘test_to_see_if_it_works’,
‘popup_email_new’
);

if( in_array($this->uri->segment(1), $pages_not_cached) )
{
// do nothing
}else{
$minutes = LOCALHOST ? 0 : 11;
$this->output->cache($minutes);
}

}//endfunc

Tim Cason’s avatar Tim Cason

Great post!

I have been using caching with CI for a bit now. It helped me lighten the load on my tiny server. Was certainly psyched about hitting better benchmarks! Keep writing good articles.

.tc

Frank’s avatar Frank

Like others have said, great post!

The problem I face with trainingmobs.com is that virtually everything is user-specific. Users have other users following them, they in turn follow other users, each user could potentially get a different view of the main page depending on their interests, physical location, usage history with the site, local time, etc.

Most of the caching work I have implemented so far is at the model level, using memcached and the excellent Multicache library (http://www.haughin.com/2007/10/09/codeigniter-multicache-library-01/) by Eliot Haughin.

A model record is pulled out of the DB, turned into a row and then cached. Updates to that record (and updates to its relationships through many-to-many tables) cause cache invalidation. There is some more domain-specific considerations around this to squeeze every last bit of performance I can out of the most expensive operations, but thats the general idea.

An interesting corollary of this is that it lets me use the config files to set TTLs for certain components on the page. So, for example, the most expensive operation on the site happens on the main page once a user logs in. I am willing to accept at most 5 minutes delay on new information shown to the user on this page, so I cache the entire output in memory (over and above caching the individual models that go into building this thing for a longer period of time). Since I control the logic of when and under what circumstances the cached records fall out of the in-memory cache, I can cache them for a day or whatever.

Nish’s avatar Nish

Thanks for the ideas. I have implemented caching in group and with conditions after referring the Codeigniter caching. But I want to extent my library to cache individual views rather than output, before sharing my library in to the public. Any one have any idea to start up?

Antonio Gallo’s avatar Antonio Gallo

The way i did isby overriding the Output core class. You can copy & paste my class from here: http://www.badpenguin.org/codeigniter-cache-disable

Wayne Smallman’s avatar Wayne Smallman

Steven, thanks for the article.

However, I do need to clarify something. I’ve enabled caching on the navigation (which is quite complex), but only within the display method for one controller; that being the one where most of the donkey work in the application is done.

Would that one instruction to caching suffice, or would I need to do the same thing wherever the navigation is called?