You are hereBlogs / moshe weitzman's blog / Cache any element in a drupal_render() array

Cache any element in a drupal_render() array

By moshe weitzman - Posted on 29 May 2009

Below I present a recipe for caching any element within a drupal_render() array. These arrays are even more prevalent in Drupal7 than they were in Drupal6. In fact, the whole page is one big drupal_render() array.

This recipe has huge performance implications. It works for authenticated users. It works when you are running node access modules. It supports custom cache tables and custom expiration rules. I think we can ditch block caching; blocks that want caching will implement this technique (note that blocks can be renderable arrays now in D7). We should probably keep the handy cache key builder that block module has.

The code below is for D7, though it needs minimal changes to run on D6. Surprisingly, this technique is equally valid on D6 as well. The innovation is clever use of #pre_render and #post_render. Drop this code into the root of a drupal site and then visit the page. I am appending some debug text just so it is clear when an element is coming from cache or not.

('DRUPAL_ROOT', getcwd());

DRUPAL_ROOT . '/includes/';

// Imagine that we are building a renderable object like a node or block or page.
$build['bar'] = array('#markup' => 'I am Bar. I don\'t do caching.<br />');
$build['foo'] = array(
'#markup' => ' I am Foo.',
'#pre_render' => array('drupal_render_cache_get'),
'#cache_key' => 'foo:bar:baz', // Think block cache keys. Needs thought.
'#cache_table' => 'cache', // Can be a custom cache table.
'#cache_expire' => REQUEST_TIME+3600, // Anything that cache_set() supports.

// Try to retrieve from cache.
function drupal_render_cache_get($elements) {
  if (
$cache = cache_get($elements['#cache_key'], $elements['#cache_table'])) {
// Replace everything with what we just successfully retrieved.
$elements['#markup'] = $cache->data . ": I was retrieved from cache. <p>To redo this experiment, run DELETE FROM cache WHERE cid='foo:bar:baz';</p>";
  else {
// We have a cache miss. Let rendering happen normally. Then cache it.
$elements['#post_render'][] = 'drupal_render_cache_set';

// Cache the rendered HTML. Better luck next time.
function drupal_render_cache_set($children, $elements) {
cache_set($elements['#cache_key'], $children, $elements['#cache_table'], $elements['#cache_expire']);
$children . ' I was not retrieved from cache. Next time, I will be.';

I was inspired by recent discussions with chx and catch. Kudos also to adrian and eaton and frando who helped drupal_render() rock so much. And many more.



So I tried caching everything in index.php then profiled it quickly to get a bit more of an idea where the gains can be made.

For reference, HEAD was about 70 queries and 550ms for the request.

First, cache_set() the output of drupal_render() in index.php, and cache_get() it before the call to menu_execute_active_handler() (so both content generation and rendering are cached - basically return straight after full bootstrap). With this, it's about the same as page caching for auth users - 43ms and 15 queries.

Then I did the same thing, but called menu_execute_active_handler() anyway (so we cache rendering, but still generate the page content) - this was about 50 queries and 200-odd ms. (In this case we save about 170 calls to drupal_render() down from 250 in HEAD).

So the final call to drupal_render_page() is about half the request time. And on /node with 10 nodes, the menu callback is the other 50%. I don't think there's many places to optimize the actual rendering process, and we need the flexibility. So if we can at least get a framework into core for caching it before freeze it opens up a lot of possibilities.

Right - the code I showed only saves the themeing of an element. But it is pretty easy to check cache *before* building up the element. See this issue for a proposed techique. That technique maps to the following in my example above.

// We have a cache miss. Let rendering happen normally. Then cache it.
$elements['#markup'] = call_user_func_array($elements['#cache_callback'], $elements['#cache_callback_arguments']);
// Perhaps just cache now, instead of using #post_render.
$elements['#post_render'][] = 'drupal_render_cache_set';

So, we can save not only the theming but also the building of the data as well.

Strategies like these are the #1 reason why I discourage side-effects.