8 Ways to Speed Up the Performance of CakePHP Apps

Posted by Matt on Tue, Mar 17 2009

Intro

It's a not so well kept secret that CakePHP is slow. What isn't well know is that this is done by design. I could get in a lot of trouble by revealing this, but I'm willing to take that risk. I have records, a paper trail a mile long, showing members of the Cake dev team investing heavily in the stock of Dell, IBM, Cisco and other server companies. We've all heard the expression "hardware is cheap, and programmers are expensive." The Cake team figure out how to monetize that by making a framework that is fast to develop with, but slow to run. They want you to throw more hardware at it. Ingenious, right? Well I'm here to end all of that. Every time you use one of the tips in this article it's one less gold chain on the neck of a Cake developer.

Notes

  • I assume you're already using the ContainableBehavior and have optimized+indexed your SQL queries.
  • I used ab to benchmark each of these changes and compared to the base benchmark, which is the plain app with debug at 0. I'm not including the actually benchmark numbers since they will vary by the application and the machine. Instead I'll include the approximate change in terms of percentage increase.
  • No, you can't see my sample app.

1) Set Debug to 0

A no brainer, right? Well there are plenty of posts on the google group that say otherwise. Before even thinking about tuning your Cake app make sure debug is 0.

Here's the difference. For the Cake engine to run it generates two cache sections.

The first is /tmp/cache/models. In there you'll find a file for every model your system containing the table schema. You know those "DESC table;" queries you see in the query output? That's what there for. Those queries go away when debug is 0.

The seconds cache is /tmp/cache/persistent. There are a couple different files in there that are used by Cake when running your app. The one that generally causes the most slow down to generate is cake_core_file_map. This file stores the paths to various classes in your app. To build the file Cake does a logical, but still time consuming, search of your directory tree looking for the right file.

So what is the difference between debug 0 and debug >0. Oh, about 2.73517104 years. When debug is >0 the cache lifetime on these files is 10 seconds. Switching debug to 0 pushes the expiration to 999 days.

<tangent>
This actually brings up an important question: if something is a "no brainer," do the people that still don't do it have less then no brain? If people without brains still figured it out, are there people walking around with black holes where there brain would be. Am I in danger of having my brain pulled into it, like light when it's slips past the event horizon?
</tangent>

Approximate Increase

+80% to 100%

2) Cache your slow queries/web service requests/whatever

The Cake cache lib is a great tool for caching single parts of your application. It handles all the gory work of writing to a file or tying into a memory based caching engine. All you need to do is figure out what to cache.

Let's say you have a query that has been indexed and optimized, but is still too slow. The Cookbook provides an example of how to wrap it with the cache lib so that you don't need to run it every request.

Or if you have a part of your site that is filled with data returned from a web service, like a recent tweets block (not a great example, since most of the Twitter widgets are JavaScript, but roll with me here). There really is no reason to make the call to the web service on every request. Just wrap it with the cache lib like the above example.

Approximate Increase

+0% to 1000000% Really depends on your app and what your caching.

3) View Caching

Think of this as entire page caching. The Cookbook covers the basics and since rendering the page still runs through PHP there is some flexibility for maintaining dynamic parts of the page. For example, if you were running a store you could cache the product pages, but still have a block showing the user's shopping cart.

Note

There's a section in the Cookbook mixed in here that covers the various caching engines CakePHP supports. However, at the moment (1.2.1.8004) view caching uses file based caching and is independent of the cache library described in #2 .

Approximate Increase

+130% to 160%

4) HTML Caching

This one is my own creation. It's based on the same principal of the Super Cache for WordPress. Basically it takes the rendered page and writes it to your webroot as straight HTML. The next time the page is hit your web server can serve it directly without even having to go to PHP.

There are obvious limitations for this, such as no dynamic content on the page, and the cache won't be automatically cleared. Still it's great for things like RSS feeds or something like popurls where the anonymous viewers all get the same page.

Approximate Increase

~60000% - This isn't hyperbole, that's the real increase.

5) APC (or some other opcode cache)

Wikipedia describes APC as "a free, open source framework that optimizes PHP intermediate code and caches data and compiled code from the PHP bytecode compiler in shared memory." Whatever. It makes shit fast. And you don't have to change any of your code. Fuck yea. Where do I sign up, right?

Approximate Increase

+25% to 100%

6) Persistent Models

This one isn't mentioned in the Cookbook (I'll add it in the next few days if no one beats me to it. I put it on my todo whiteboard, right below "figure out why putting computers in the clouds is more efficient then their traditional ground based counter parts"). This one is simple to turn on. In your controller (or AppController) add the attribute:

var $persistModel = true;

After a page refresh you'll notice two new files in /tmp/cache/persistent for each model included in the controller. One is the cache of the model and the other is a cache of the objects in the ClassRegistry. Like view caching mentioned above, this cache can only be saved on the file system.

Approximate Increase

+0% to 200%
How much this one helps depends on your application. If your controller only has one model and it isn't associated with any others you're not going to see much of a boost. In my demo app there was around 100% increase. There was one model in the controller, which was associated with 3 other models, which had associations of their own.

7) Store The Persistent Cache in APC

To turn enable this you need be using APC and set your "_cake_core_" cache to use APC. In your core.php put:

Cache::config('_cake_core_', array('engine' => 'Apc',
                                   'duration'=> 3600,
                                   'probability'=> 100,
                                  ));

This takes the cache files normally stored in /tmp/cache/persistent (not including the persistent models) and stores them in memory.

Approximate Increase

~25%
This is a hard one to measure. I tried enabling APC without opcode caching to measure just this change, but never found a setting that didn't provide a speed bump over the base setup.

8) Speed Up Reverse Routing

There are two methods for doing this. The first is described in a post by Tim at Debuggable.com. Tim's method only works for certain link types and breaks the reverse routing feature. Mine uses caching and made it to Hollywood week of CakePHP Idol where Nate (the Simon of the core team) called it "clever", but it was ultimately sent home when it forgot the lyrics to Kansas' "Dust in the Wind." Yes, I'm drinking and watching American Idol as I write this.

Approximate Increase

~50%
Like all of these tips, the actual increase depends on your app. If you don't use many custom routes and don't have many links on your page your not going to see much of a benefit.

The End

It's up to you now. Figure out which of these works best for you. Go forth and produce speedy CakePHP apps.

Wait, One More Thing

I'm sure I missed something. Leave your best tips in the comments. If I get enough I'll make a second post and claim credit for myself.

Posted in CakePHP

52 Comments

Mark Story said on Mar 17, 2009
Darn you Matt! I had this exact same idea sitting on a bus today. Since you preemptively stole my glory, I have a few things you might want to add.

Don't use $uses - It causes extra loops and hits on the ClassRegistry, and often is a signal of 'wrongness'.

Also in addition to APC there is Xcache and Memcached which will both boost performance. And lastly concatenate and compress your assets. A lot of 'slowness' can come from assets being loaded/transferred, and while proper use of e-tags can help increase browser cache use, there's always some goof like me who has all his browser caching turned off.
Matt said on Mar 18, 2009
My skills at stealing post ideas have now evolved to the point where I can take ideas before they are even written.

Thanks for the extra tips. It would be really cool if someone wrote a helper that automatically concatenated and compressed css/js.
XXXL said on Feb 08, 2010
Are: var $uses = null;
causes the same problem like var $uses = array('Model');
?
Jamie said on Mar 09, 2012
I read that the new CakePHP v2 release uses lazy model loading. Has anyone noticed how much that has improved load times?
Jamie said on Mar 09, 2012
I read that the new CakePHP v2 release uses lazy model loading. Has anyone noticed how much that has improved load times?
TG said on Mar 17, 2009
Mark instead of using $uses do you recommend just binding every model when needed then unbinding?
saintberry said on Mar 17, 2009
I recently went though the controllers of one of my apps and got rid of all the $users attributes. I access associated models like so (if you are in UsersController) $this->User->Group->find();

Just follow the association along the chain from whatever your default $users attribute is populated with.

Anyway this article was a good read. Cheers. In regards to Persistent Models if I enable this is there going to be any adverse effect on my application? What is the impact?
Matt said on Mar 18, 2009
Regarding persistent models: I just started playing with this one the other day. So far I haven't had any adverse effects.
damien said on Feb 11, 2011
Well, I did find some issues regarding $persistModel ! Suddenly, a lot of my formerly perfectly fine functions turned to the following message:

( ! ) Fatal error: BehaviorCollection::trigger() [behaviorcollection.trigger]: The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition "MetaBehavior" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide a __autoload() function to load the class definition in /Users/damien/Sites/cake.loc/cake/libs/model/model_behavior.php on line 494
Call Stack
# Time Memory Function Location
1 0.0003 669872 {main}( ) ../index.php:0
2 0.5161 6011712 Dispatcher->dispatch( ) ../index.php:87
3 0.5685 6559144 Dispatcher->_invoke( ) ../dispatcher.php:171
4 0.8720 15333872 call_user_func_array ( ) ../dispatcher.php:204
5 0.8720 15334256 SongsController->listen( ) ../dispatcher.php:0
6 0.8720 15334400 Model->read( ) ../songs_controller.php:232
7 0.8720 15335168 Model->find( ) ../model.php:1129
8 0.8721 15338272 BehaviorCollection->trigger( ) ../model.php:2108
Ovidiu Liuta said on Jan 27, 2012
I see nobody mentioned here the Varnish caching server https://www.varnish-cache.org/ , it might be a good option to run in conjunction with your existing server and your already super optimized cake application ...

Why write pages on disk, when you can serve them from server's RAM memory directly ...:)
Richard@Home said on Mar 18, 2009
Excellent article. One of the few 'speed up CakePHP' that contains real, practical advice. Thanks for sharing.
primeminister said on Mar 18, 2009
Nice article Matt!
In addition to Mark Story's tip on concatenate and compress your assets: Read about it on the yahoo developer website: http://developer.yahoo.com/performance/rules.html
Brian D. said on Mar 18, 2009
Your opening paragraph explains why I've seen Nate Abele driving that Rolls Royce through Brooklyn.

Good article, well written. Kudos.
Mark Story said on Mar 18, 2009
TG: I would use associations as saintberry said. If that fails I would use Controller::loadModel() or ClassRegistry::init('RandomModel');
Andrew said on Mar 18, 2009
Mark, What's the difference between Controller::loadModel and CR::init?
Andrew said on Mar 18, 2009
Actually when I have to load a model I use App::import('Model', 'modelName')... is this the same as using the uses class variable?
Matt said on Mar 18, 2009
Check out this thread in the google group. Third post down.
Andrew said on Mar 18, 2009
Awesome, amazing... thanks, gotta love Gwoo ;)
oli said on Mar 18, 2009
i do most of the things you mentioned.

but some weeks ago we had performance isses anyway. so checked my code with the xdebug profiler.
cookies are encryped by default and the cipher crypt method is very slow. i don't store sensitive data in these cookies - so i turned encryption off - now the sites runs about 0.3s faster.
Matt said on Mar 18, 2009
These are some good tips that can be applied to cakePHP: http://developer.yahoo.com/performance/rules.html

It bothers me that cakePHP adds both javascript and css to the same location($scripts_for_layout). Why is that? CSS goes at the top and javascript goes at the bottom. The only time you would put javascript at the top is if you were injecting it into the DOM so it didn't block and loaded asynchronously... a more complicated matter which cakePHP doesn't do.
Joel Perras said on Mar 19, 2009
There are several methods available to you for accomplishing this goal; they simply require a bit of thought. Take a look at View::set, and see what you can come up with.
echo said on Mar 21, 2009
you can put your script at the bottom o page very easy with $javascript helper echo $javascript->link('prototype-1.6.0.3', true); first parameter is the file name and second if true put your js in $scripts_for_layout else if is false prints the js link in the place you write it :)
Matt said on Mar 21, 2009
@Joel

Thanks for the tip. I think the simplest solution would be for me to write a new helper(CssHelper) and utilize View::set as you mentioned to set $stylesheet_links_for_layout and $styles_for_layout.

@echo

The problem with that is html->css('file.css', null, null, false) would add a css link element to $scripts_for_layout also.
jlarmstrong said on Mar 19, 2009
FYI, "var $persistModel = true;" in 1.2 RC1 breaks the app at each requestAction call. With my very basic initial testing it appears the be working fine with the latest stable release.
Jeff Seibert said on Mar 19, 2009
Just FYI - I run a *very* complex CakePHP app with 30+ highly inter-related models and turning on persistModel did not help anything. In fact, it made the app over 100% *slower*. Requests to our heaviest page went from averaging 233ms to averaging 435ms over many tests.

I am curious about removing $uses per the comment above and will benchmark that when I get a chance.

Thanks for the article, though. Good stuff.
Matt said on Mar 19, 2009
Hey Jeff, I hope you post back your results when you remove $uses. I think you'll be very happy with the change.

I'd also be interested in hearing more about what happened w/ persistModel. I would guess the unserializing of the cached files could take awhile if they were really big. You'd probably have to run it through a profiler to know for sure.
Jeffrey said on Mar 23, 2009
I too have a fairly complex app with ~30 models and lots of relationships. I tried turning on persistModel and our app went from 1.3s to 7.3s (decent to unbearably slow). This was on by dev machine running WinXP, Apache 2.2, PHP 5.2.0. I'm going to try again on our production linux server and see if the OS makes a difference.
Wil Sinclair said on Apr 03, 2009
Shameless plug: I'm wondering if anyone has tried performance optimization of a CakePHP app on Zend Server. Of course, it provides the opcode caching and page caching. There's the Data Cache, too, but requires app changes. Obviously we've tried Zend Framework on it, but how does it serve the CakePHP community? Mail me your experiences. Anyways, great article.

,Wil
John said on Apr 03, 2009
Cheers for the article. I'm in the process of tweaking an app at the moment. The front end is really nice and quick (mostly thanks to query caching of things like menus), but the admin end has got some real slow bits that I'm trying to work through at the moment.

Anyway your reverse routing seems to work great - I reckon its knocking off about 0.5 seconds (times from firebug). I tried the var $persistModel = true, it seems to work for about 3 requests then breaks the app completely - really odd (that's using 1.2.1.8004)

John
John said on Apr 06, 2009
The white screen was just debug 0 (working too late after a few drinks... duh)

Anyway upgraded the core to 1.2.2.8120 - anyway $persistModel = true definitely breaks 2 of my models and I spent hours trying to figure out why (but no answer yet). I'm getting the following errors:

Catchable fatal error: Object of class __PHP_Incomplete_Class could not be converted to string in C:\xampp\htdocs\cake1.2\cake\libs\debugger.php on line 451

And on my other model:

Fatal error: BehaviorCollection::trigger() [
Cheers
ics said on Apr 06, 2009
http://be.php.net/manual/en/migration52.incompatible.php

To my understanding you just have to define __toString().

E.g.
function __toString() {
return "something";
}

Works for me on PHP < 5.2.9.

I still have this error on PHP 5.2.9 and I'm still searching.

Hope this helps.
John said on Apr 06, 2009
@ics - Thanks I'll have a look at that, we're running 5.1.6, so fingers crossed.

John
Matt said on Apr 06, 2009
Hey John,
I think there are some limitations to the way that CakePHP handles cached models. The tmp file for the model and it's registry is just based on the model name, without any context as to which it was stored. So you may use the same model in multiple places in your app, but the registry may not match.

I've been working on this code lately:
http://bin.cakephp.org/view/2029293743

It attempts to create the related model objects as needed. If you have time I'd be interested in seeing how it works for you. Thanks.
ics said on Apr 06, 2009
Hi Matt,

I've tried your fix. I'm not using ACL on this app, only Auth.


Notice (8): Indirect modification of overloaded property User::$Aro has no effect [CORE/cake/libs/model/behaviors/acl.php, line 60]

Code | Context

$model = User
User::$name = "User"
User::$actsAs = array
User::$validationSets = array
User::$validate = array
User::$hasMany = array
User::$belongsTo = array
User::$recursive = -1
User::$__definedAssociations = array
User::$__loadAssociations = array
User::$useDbConfig = "default"
User::$useTable = "users"
User::$displayField = "name"
User::$id = false
User::$data = array
User::$table = "users"
User::$primaryKey = "id"
User::$_schema = array
User::$validationErrors = array
User::$tablePrefix = ""
User::$alias = "User"
User::$tableToModel = array
User::$logTransactions = false
User::$transactional = false
User::$cacheQueries = false
User::$hasOne = array
User::$hasAndBelongsToMany = array
User::$Behaviors = BehaviorCollection object
User::$whitelist = array
User::$cacheSources = true
User::$findQueryType = NULL
User::$order = NULL
User::$__exists = NULL
User::$__associationKeys = array
User::$__associations = array
User::$__backAssociation = array
User::$__insertID = NULL
User::$__numRows = NULL
User::$__affectedRows = NULL
User::$_findMethods = array
User::$_log = NULL
$config = array(
"requester"
)
$type = "Aro"

uses('model' . DS . 'db_acl');
}
$model->{$type} =& ClassRegistry::init($type);

AclBehavior::setup() - CORE/cake/libs/model/behaviors/acl.php, line 60
BehaviorCollection::attach() - CORE/cake/libs/model/behavior.php, line 291
BehaviorCollection::init() - CORE/cake/libs/model/behavior.php, line 252
Model::__construct() - CORE/cake/libs/model/model.php, line 418
AppModel::__construct() - APP/models/app_model.php, line 32
ClassRegistry::init() - CORE/cake/libs/class_registry.php, line 140
Controller::loadModel() - CORE/cake/libs/controller/controller.php, line 478
Controller::constructClasses() - CORE/cake/libs/controller/controller.php, line 423
Dispatcher::_invoke() - CORE/cake/dispatcher.php, line 224
Dispatcher::dispatch() - CORE/cake/dispatcher.php, line 211
[main] - APP/webroot/index.php, line 88


Fatal error: Cannot assign by reference to overloaded object in /cake/libs/model/behaviors/acl.php on line 60
Matt said on Apr 06, 2009
Hey ics,
Thanks for giving it a shot. If you remove the "&" from that line (CORE/cake/libs/model/behaviors/acl.php, line 60) it won't give you the error. Weird that you'd be even hitting that if you're not using ACL. If you're using PHP5 the "&" doesn't make a difference. You'll see other places in the Cake code where they do this:

if (PHP5) {
$this->{$name} = new $class;
} else {
$this->{$name} =& new $class;
}
ics said on Apr 06, 2009
Thanks Matt.
There was also a brain fart from my behalf.

$config = array(
“requester”
)

I initially had ACL and forgot to remove the ACL behavior.

However:

Fatal error: BehaviorCollection::trigger() [
Matt said on Apr 06, 2009
Are you using persistModel as well? I'm hoping this code will make persistModel unnecessary. I use the containable behavior as well and haven't hit that issue, although containable is quite powerful so we certainly could be using it in different ways. Feel free to hit me up on email (matt@pseudocoder.com) if you get it to work for your app.
John said on Apr 06, 2009
@Matt - Thanks I'll have a look. What you say about the cached models rings true. My gut feeling is that is binding and unbinding models on the fly is the root of my problem.

I have Node and SharedItem but between them is a Polymorphic join table which is represented by two LeftNodesSharedItem and RightNodesSharedItem.

(By way of explanation Node is basically just a page of content for a webpage, SharedContent is data that can populate side columns)

If I access a node (which is the usual way) directly, $persistModel = true breaks the app, but if I access the relevant node methods indirectly via ClassRegistry::init and then use $this->render() - which I occasionally need to do from an unrelated controller - it still works.

I'm a bit swamped at the moment as we are trying to get this live tomorrow - but latter this week I want to dig into this and find out what is going on.
Gian said on Apr 28, 2009
Whoa. My app seemed to speed up by 200%. I think it's the persistentModel thing. And also APC. By the way I'm receiving this error when debug != 0.

Catchable fatal error: Object of class __PHP_Incomplete_Class could not be converted to string in cake\libs\debugger.php on line 451

Any ideas? Other than that everything is working fine.
Matt said on Apr 28, 2009
persistModel seems to be giving a lot of people problems. Check out my latest post - the lazy load section. Using that should remove the need for persistModel.
online library management said on Jan 12, 2010
thanks for nice information,

i have lots of repeated data in every pages like categories, site names and some settings variable mey be putting them in session will boost the speed but then....when update any of those values from admin panel i need to refresh those values from session....

i am thinking what to do
Greg said on Feb 05, 2010
Hi Matt !

thanks for the tip, persistModel really worked well for me.

Just one thing that took a loong time to figure out : if your cake app is VERY slow (like 3-4 seconds for each page load), check that you have all the tmp directories set up, cause Cake won't create them for you :

app/tmp/cache
app/tmp/cache/models
app/tmp/cache/persistent
app/tmp/cache/views
app/tmp/sessions
app/tmp/logs
app/tmp/tests

That may sound obvious, but I developped my site locally and when I uploaded it to the webserver, I did not upload the tmp folder, as I knew it was only temporary data.

As a result, my site was running fine locally, but with HORRIBLE performance on the server.
Julian said on Mar 02, 2010
I implemented persistModel, and it did noticeably speed up my app.

However, validation errors were not showing for one of my models (but not others - weird).

I removed persistModel and the validation errors started showing again.

I post this here to help anyone else who might come across the same problem!
Meltdata said on May 06, 2010
Thanks for the helpful and valuable information.

I have one query :
Can somebody tell me what is the difference between Cake1.1 and 1.2 as aplication codes are almost same, but the results are different.
Shouvik said on Jun 02, 2010
We can also use the gzip compression algorithm which compresses the files upto 90%.

This is also being used by popular portals like yahoo, yatra etc
Y-Love said on Jun 17, 2010
I've used almost all of these tips on here, and yes, like the above commenter said, there's no substitute for gzip and sometimes its just a matter of plain old slimming down js code.
cosmin said on Jul 19, 2010
It's the second time it happened to me so I'll put it here cause it seems to be the most relevant article on CakePHP speed. Make sure you set Cache.disable to false in the core.php. This will allow Cake to skip de DESCRIBE queries for models and make stuff 3x faster at least on slow CPUs. Only drrawback is that if u change the database you have to manually delete the files inside app/tmp/cache.
Mason said on Sep 13, 2010
Thanks for the tips, it has helped me a great deal.

One problem I continue to have is that my AJAX calls won't cache (view cache). If I access the url directly, the cache file is created, but when I access it with an AJAX call it does not. Does anyone know if this is by design and if there is a way around it?
Jan said on Sep 17, 2010
Thanks for your post. I already knew some of the caching methods within cake, but it was nice to have them listed like this :)
anew said on Feb 24, 2011
override loadModel like so:
app_controller.php

function loadModel($model, $id){
if (!isset($this->{$model}){
parent::loadMOdel($model, $id)
}
Supratall said on Dec 08, 2010
I tought about implementing a feature to send a maximum number of mails per day and/or to send a mail after a custom number of comments have been selected. Additionally I would implement a admin comment feed with all the neccessary liks to approve, edit, delete or mark as spam.
Kunal said on Dec 21, 2010
Great post Matt. What I'd like to know, and I'm sure others would find interesting, is what tools you used to benchmark your app. Currently experimenting with siege. What do you think of siege/httperf/apache bench/jmeter?
Maybe that could warrant a blog post.