RSS
 
13. Feb. 2012

What REALLY speeds up your cakephp app

13 Feb

There are already many (partly outdated) blog entries (e.g. 8-ways-to-speed-up-cakephp-apps) and stackoverflow questions/answers (e.g. speeding-up-cakephp) regarding this question. But from years of experience I want to outline the more important ones pretty quick.

I will not talk about the little things or the ones that will only affect 5% of the website. And no – debug level 0 is not a speed improvement. It is an absolute requirement for any live application and can therefore be seen as default setting.

One last note: I want to address dynamic websites here that cannot make use of extensive view caching or even html caching as described in the first link – at least for most of the pages. They undoubtedly are the fasted way to serve content but can usually only applied to very few views. The tips below are valid for all requests across your application.

1. Opcode Cache

Opcode Cache will keep your php files in the memory. It will usually also store the compiled bytecode of it to further improve execution time. But it is not only improving speed a great bit, it is also reducing the memory used for a single request by more than 50% allowing your server to serve twice as much requests (and therefore users) before overworking itself. A mediumsized cake2 application of mine uses 13.9 MB memory. With Opcode Cache it is reduced to 7.7 MB. And the speed is pretty much accordingly.

Installation is almost too easy. Example for ubuntu and apache: apt-get install php-apc and restarting the apache should do the trick. But there are other modules, as well (Xcache, Memcached, …)

Gain: in most cases more than 100% (more than twice as fast)

2. Also use memory caching for all temporary data

This mainly includes “tmp files” via CacheEngine which can be cake core tmp files or app tmp files. All your cached elements will be included faster this way, as well. If you cache your sql queries they will also be affected positively compared to a simple file cache (although a file cache itself is better than nothing).

Cake2.x automatically tries to use the fastest CacheEngine if available (and defined in your core.php):

$engine = 'File';
if (extension_loaded('apc') && function_exists('apc_dec') && (php_sapi_name() !== 'cli' || ini_get('apc.enable_cli'))) {
    $engine = 'Apc';
}

Gain: 10-40%

3. Speed up (Reverse) Routing

This will help for sites with many dynamic links. Without caching the generated urls Router class creates them over and over again. You can use my UrlCache plugin which basically does exactly that. I used the ideas of lorenzo and mcurry and applied some fixes and improvements. It now stores commonly used links (without any params) in a global cache file while using single cache files per site for the specific urls (e.g. from pagination/filtering).

If you only got a bunch of those links on a single page, this is wasted time, though. But for most projects it is a real time saver – from 0.3 seconds up to 1 second less request time.

Important note: You need to manually configure your _cake_core_ cache for this:

Cache::config('_cake_core_', array(
    'engine' => $engine,
    'prefix' => 'cake_core_',
    'path' => CACHE . 'persistent' . DS,
    'serialize' => ($engine === 'File'),
    'duration' => $duration,
    'lock' => true, # !!!
));

Note the exclamation marks. If you do not apply 'lock' => true you will end up with quite a few broken cache files. This will reset the whole thing all the time due to concurrent requests/writing and you gain nothing… See this ticket for details. With this setting the problems seems not to occur.

To further optimize performance I invented a dual cache system with reading unlocked and writing locked. See the plugin for details on how to use it for best results.

Gain: 10-50% (the more links on a page the more gain)

4. Optimize data, compress and cache assets

If you got 50 icons on your site, you might want to create a single sprite instead of creating +50 requests to your server here. You also should combine your js/jss and apply some compressing.

In general it is a good idea to send all data gzip compressed – using apache’s mod_deflate module for example. This way only half the amount of bytes need to be sent to the user. Especially with (slow) mobile connections this will increase speed up to 100%.

Using mod_headers you can make sure asset files are not permanently requested if they didn’t change. The website will feel faster for the user and the server has to serve less requests.

Files of the same type (css, js) should be packed and sent combined to a single file (as opposed to maybe 20-30 single files). Undoubtedly Mark Story’s AssetCompress plugin is one of the best ones out there to address exactly these issues.

Gain: 10-30% (might also depend on the browser)

Some stuff I didn’t try yet

In Cake2 you can even store sessions in the Cache (and therefore probably in the memory). This should also speed up the application remarkably compared to database sessions for example.

And the list of obligatory tips which should already be well known at last

  • Put recursive = -1 in your AppModel and use containable to restrict the amount of data to be fetched as the database connection usually is the bottle neck of any app
  • Try to load stuff dynamically and only if needed (With Cake2 and lazy loading everywhere via App::uses() this shouldn’t be difficult to do anymore)
  • Try to decouple time consuming stuff from the frontend. So rendering large images, creating complicated pdfs, sending bulk emails can all be passed on to some “Queue” which asynchronously works them off piece by piece without slowing down the response time for the action.
  • As mentioned above you can cache complete pages to the View cache preventing the complete dispatch process to run. Such a pre-rendered html file will be served right away. This is useful for pages with mainly static content.
  • Avoiding $uses in the controller (except for its own model, of course) – meaning we should never have more than the primary model attached to it*.
  • Make sure you send the right headers for proper client-side caching. Some browsers might not respect it, but the ones that do will profit from the increase in speed.
  • Use a subdomain and cookie less delivery for static assets (see here).
  • Don’t use requestAction

Final words

All in all Cake can be sped up to a similar level than other frameworks. In general it will never be as fast as plain PHP, but that is not the point. You chose a framework not for the speed but the reduced development time as well as clean and future proof structure. Server costs are nothing compared to the money and time you need to spend on developers if you are chosing PHP over Cake. Think about that before you open a new discussion on how slow CakePHP supposedly is :)

Update – 2012-02-20 – Benchmark

There is a benchmark site comparing all kinds of frameworks.. Unfortunately, using 1.3 instead of 2.x. But either way it displays how important it is to apply above optimization. If that is the case I bet the results will be way less dramatic. Bottom line: Benchmarking is quite useless in the normal way of just comparing basic output examples.

* with lazyloading in 2.x not that important anymore (now more an advice to write clean code).

Update – 2014-02

Check out this twitter status! Self-explanatory.

What REALLY speeds up your cakephp app
7 votes, 4.71 avg. rating (94% score)
 
20 Comments

Posted by Mark in CakePHP, PHP

 

Tags: , , , ,

Leave a Reply

Tip:
If you need to post a piece of code use {code type=php}...{/code}.
Allowed types are "php", "mysql", "html", "js", "css".

Please do not escape your post (leave all ", <, > and & as they are!). If you have encoded characters and need to reverse ("decode") it, you can do that here!
 

 
  1. WyriHaximus

    February 13, 2012 at 22:59

    For the past year or so I'm using a variant of the url cache and I must say it is a must have. One site has between 100 to 300 links per page with a lot of custom routing. Not caching the URL parsing slowed down the page drastically. Trying out your plugin atm. Like what I'm seeing, the code is a lot cleaner and leaner then the old AppHelper code I'm using now.

    What are you using as Queue? Done a lot in the past with [1]. Lately I'm looking for a more robust and up to date system and I picked [2] for now. (Same system cakepackages uses.) Was wondering what you use for it. It might have some good idea's/better ways to tackle certain issues.

    [1]: https://github.com/MSeven/cakephp_queue
    [2]: https://github.com/josegonzalez/cake_djjob

     
  2. Mark

    February 13, 2012 at 23:03

    I totally agree :)
    Don't know why I didnt address this sooner…^^

    As you can see from my github account I use Msevens' https://github.com/dereuromark/cakephp_queue – at least a forked version of it. I enhanced it for 2.0.

     
  3. Luís Armando

    February 13, 2012 at 23:20

    Always good to review this performance tips.
    Very nice Mark!

    Also, I've noticed that in pages with many links, such as index pages where it has several links for each row, reverse routing is certainly useful.
    For that I use my own workaround to improve performance.
    More details in the link: http://bakery.cakephp.org/articles/luisarmando/2011/07/05/optimize_url_generator

    Grüße

     
  4. Mark

    February 13, 2012 at 23:28

    But I do have do say that your approach will break most of the time with some specific Reverse Routing set up.
    In those cases you cannot say for sure to what point the url stays the same if you only route controller/action.
    So if you used it on top of mine you would gain some additional speed for the first "uncached" page hit in the cases where your method works but on the second the cached file would jump in and yours woudn't even be affected.
    Taking into consideration that these url string concatenations have a high potential for breaking the url I like to let Router handle it completely.

     
  5. Luís Armando

    February 14, 2012 at 00:25

    Yes, that's right.
    That is really an issue.
    It is only useful when building URLs for the same controller/action for different, let's say, ids.
    This is the case in administrative index pages. There a page with 20 rows, would build 20 times a url to edit and 20 times a url to delete.
    Yeah, I know it's really a specific case.
    But, caching seems to be not so useful in this case. Because many URLs are stored in the cache, and they are not used so much.

     
  6. Mark

    February 14, 2012 at 01:23

    I see your point. It would speed things up without creating cache overhead.
    For a site with reverse routing only for frontend urls this could indeed work out for the admin urls – attaching only the additional params.

     
  7. WyriHaximus

    February 16, 2012 at 19:49

    The Cron features you added look good. Been looking into a way to build such thing myself for a while :).

     
  8. Mark

    February 16, 2012 at 19:51

    I don't think it is usable just yet, though. But feel free to help me adjust it to 2.0

     
  9. Lucho Molina

    June 13, 2012 at 22:22

    Hi Mark – Thanks for this post. I have a question regarding your advice to avoid using $uses in a controller, except for its own model:

    I have a Model1 associated with Model2 which is associated with Model3. In Model1Controller, I want to access Model3 directly.

    Would you suggest I do the following even if it looks horrible?

    $this->Model1->Model2->Model3

    Or in that case I'm better off adding Model3 to $uses?

     
  10. Mark

    June 13, 2012 at 22:38

    Since 2.x and lazyloading this is not that important anymore.
    But it is still not a good approach to put all the models in $uses (especially if the models are related). You can always load unrelated dynamically on demand with

    $this->loadModel()

    for example.

    Yes, for related models you are better off using the chaining.

     
  11. Leo

    June 22, 2012 at 12:18

    Good tips. However I think its a bit harsh to say don't use requestAction. It's perfectly acceptable within an element for something like 'latest comments' provided its cached

     
  12. Mark

    June 22, 2012 at 14:53

    Well, maybe if its cached. but even then it could and should be avoided doing it this (dirty) way of triggering another request inside this request – as mentioned on many other sources, as well, like http://mark-story.com/posts/view/reducing-requestaction-use-in-your-cakephp-sites-with-fat-models/

     
  13. melissa

    June 29, 2012 at 21:21

    could you explain this a bit more?

    Avoiding $uses in the controller (except for its own model, of course) – meaning we should never have more than the primary model attached to it.

    I'm looking in app/controllers/site_pages_controller.php for our site and i see this

    public $uses = array('Content', 'SitePage', 'ContentAttribute');

    Are you saying this shouldn't be there – in that controller? I am a noob at CakePHP and that's probably being generous. Thank you.

     
  14. Mark

    June 30, 2012 at 20:46

    Well, it's just to note that $uses shouldn't be abused. For me it would only make sense to include models (besides the main one) which are not chained (related) to the main model and will be used throughout the controller – and not just a single action.
    For single actions you can dynamically load the models and for related models you can call them using the Main->Foo->bar() way.

    So use cases where you can use it without problems would be some navigation model or other globally used models that are pretty much independent.

     
  15. Benjamin Allison

    September 8, 2012 at 15:58

    The requestAction is still up for debate, when it comes to optimizing:

    http://mark-story.com/posts/view/how-using-requestaction-increased-performance-on-my-site

     
  16. David

    February 15, 2013 at 22:26

    I've no idea why `requestAction()` gets such a bad rep. It's a fantastic tool.

    I think it's because it's used incorrectly all the time. As people have mentioned Mark's post and about the caching, I'll just say +1 for that ;)

     
  17. Mark

    February 16, 2013 at 02:21

    Even if used correctly, in most cases it is not needed and only invokes the dispatcher more often it is necessary.
    So I never used it once and don't plan to ever start using it :)

     
  18. Jippi

    April 6, 2013 at 20:07

    If you have a production environment, setting "apc.stat" to "0" will increase performance quite a lot – no stat() will be done for each file to check if they are changed on every request.
    To refresh files you must restart apache or flush apc optcode cache.

    Redis as a session store can also increase performance quite a lot. Though don't use it for any _cake_ caches, it will eventually slow your site down in high load (2000+ req/sec)

    Moving all .htaccess into your apache or nginx configuration can also limit your stat()s and improve request performance.

     
  19. Florian

    May 8, 2013 at 12:45

    Hi, really interesting article, thanks for the tips. I noticed in CakePHP 2.3 that the default caching engine is File. So you can redefine it in core.php (for cakephp related cache) as you made using apc and also in the bootstrap.php (for your application cache) else the default cache used will be File ! If you have defined $engine in core.php, you can use it again in your bootstrap.php, this way

    Cache::config('default', array('engine' => $engine));

    , so you would use the same engine everywhere. You can also define other type of cache in your bootstrap.php with different engine. Also you may use the CacheHelper to cache the views, more about it there http://book.cakephp.org/2.0/en/core-libraries/helpers/cache.html, I hope this would help :)

     
  20. REnuVarghese

    March 27, 2014 at 07:57

    I have more than 1 lakh of records, i need to store it in cache file using cakephp.

    Which is the best way?

    A) Store all data(records) in a single file and fetch data from that file.
    B) Store each record in single cached file with its id and retrieve.
    C) Store it in different cached files with a bundle of ids (store 1000 records in each files).

    May i get a good solution for this please?
    Thanks.