Support

Account

Home Forums Feature Requests Improve ACF performance – implement a simple caching (an example provided)

Solving

Improve ACF performance – implement a simple caching (an example provided)

  • Hi,

    I’ve noticed a performance issue when getting a value of a repeater field or a gallery field (generally speaking: a field that returns a large array of data).
    Example:

    
    if (get_field('gallery')) {
        foreach (get_field('gallery') as $image) {
            // Do something;
        }
    }
    

    The reason is simple: get_field('gallery') runs twice.
    There’s a simple workaround:

    
    $gallery = get_field('gallery');
    if ($gallery) {
        foreach ($gallery as $image) {
            // Do something;
        }
    }
    

    But I don’t like this solution because it requires a global variable. Global variables are considered as a bad practice and I try to avoid them in my templates.

    here’s a simple test I run. A single call to get_field('gallery') field containing about 300 images.

    
    $start = microtime(true);
    
    get_field('gallery');
    
    $end = microtime(true);
    
    var_dump($end - $start);
    

    Ther result was ~3.22s

    I’ve increased the number of calls to get_field('gallery'):

    
    $start = microtime(true);
    
    get_field('gallery');
    get_field('gallery');
    
    $end = microtime(true);
    
    var_dump($end - $start);
    

    Ther result was ~6.44s

    I also tried with more calls to get_field('gallery'). Each call to get_field('gallery') function multiplied the end result by ~2. So when there were 4 calls the result was 12.8s.

    So I came up with an idea to cache the values returned by the get_field() function. The caching class is very simple. I’ve assumed that the result of get_field() function call is determined only by the arguments passed to the get_fiedl() function. So if the arguments are the same the result will be the same as well.

    Here’s the implementation of the Cache() class. It’s a singleton, because I need only one instance of this class.

    
    class Cache
    {
    
        private static $instance;
    
        private $cachedFields = [];
    
        private function __construct()
        {
        }
    
        private function __clone()
        {
        }
    
        public static function getInstance()
        {
            if (self::$instance === null) {
                self::$instance = new Cache();
            }
            return self::$instance;
        }
    
        public function get($serializedArguments)
        {
            return $this->cachedFields[$serializedArguments];
        }
    
        public function exists($serializedArguments)
        {
            if (!array_key_exists($serializedArguments, $this->cachedFields)) {
                return false;
            }
    
            return true;
        }
    
        public function add($serializedArguments, $fieldValue)
        {
            $this->cachedFields[$serializedArguments] = $fieldValue;
        }
    
    }
    

    and here’s the implementation of the get_cached_field() function:

    
    function get_cached_field($fieldName, $postId = false, $formatValue = true)
    {
        // Get an instance of Cache class.
        $cache = Cache::getInstance();
    
        // Get valid post ID using acf core function
        $validPostId = acf_get_valid_post_id($postId);
    
        // Serialize function arguments. I can't simply 'use serialize(func_get_args())' because of $postId which if not passed as an argument defaults to current $post->ID
        $serializedArguments = serialize([$fieldName, $validPostId, $formatValue]);
    
        // Check if the value is already cached and return the cached value if the condition is evaluates to <code>true</code>
        if ($cache->exists($serializedArguments)) {
            return $cache->get($serializedArguments);
        }
    
        // Otherwise use get_field() function to retrieve the vale
        $fieldValue = get_field($fieldName, $validPostId, $formatValue);
    
        // Save the value to cache
        $cache->add($serializedArguments, $fieldValue);
    
        // And return the value
        return $fieldValue;
    }
    

    And the benchmark:

    
    $start = microtime(true);
    
    get_cached_field('gallery');
    get_cached_field('gallery');
    
    $end = microtime(true);
    
    var_dump($end - $start);
    

    Ther result was ~3.22s

    I tried to add more calls to get_cached_field('gallery');. The result was pretty much the same. No matter how many calls there were. For example, when I increased the number of calls to 25 the result was 3.3s.

    I think this kind of optimization could make into ACF core. I can’t see any side effects (correct me if I’m wrong). Of course, I don’t suggest adding a new function, I’d like it to be implemented in the get_field() function.

    PS
    I didn’t implement it for the get_sub_field() function yet, but it shouldn’t be complicated either.

  • Hi @wube

    Thanks for sharing the code.

    I’ll pass it to the plugin author so he can check it and implement it if it is suitable.

    Thank you very much 🙂

  • This is an awesome idea!

  • Hi guys

    This is a great topic.

    Currently, ACF will cache any value loaded from the DB, but after loading, the $value is sent to another function that formats it based on the field type.

    When dealing with the repeater / gallery field, this results in a lot of extra logic / DB calls.

    I have caching setup for the loading of values, but not for the formatting of values.
    I wonder if we adding caching to the format function, this will fix the issue.

    The following assumes you are using ACF PRO (or ACF v5 in the future):

    Open the file api/api-value.php and finc the acf_get_value function. Below this function is the acf_format_value function. Note that the format function contains no caching, but the load function does.

    Try changing the format function to this:

    
    /*
    *  acf_format_value
    *
    *  This function will format the value for front end use
    *
    *  @type	function
    *  @date	3/07/2014
    *  @since	5.0.0
    *
    *  @param	$value (mixed)
    *  @param	$post_id (mixed)
    *  @param	$field (array)
    *  @return	$value
    */
    
    function acf_format_value( $value, $post_id, $field ) {
    	
    	// try cache
    	$found = false;
    	$cache = wp_cache_get( "format_value/post_id={$post_id}/name={$field['name']}", 'acf', false, $found );
    	if( $found ) return $cache;
    	
    	
    	// apply filters
    	$value = apply_filters( "acf/format_value", $value, $post_id, $field );
    	$value = apply_filters( "acf/format_value/type={$field['type']}", $value, $post_id, $field );
    	$value = apply_filters( "acf/format_value/name={$field['name']}", $value, $post_id, $field );
    	$value = apply_filters( "acf/format_value/key={$field['key']}", $value, $post_id, $field );
    	
    	
    	// update cache
    	wp_cache_set( "format_value/post_id={$post_id}/name={$field['name']}", $value, 'acf' );
    	
    	
    	// return
    	return $value;
    	
    } 
    

    Does it work? I’ll do some testing too, but would be great to hear back from you @wube

  • Hey guys

    I’ve done some testing and can confirm this cache addition to the acf_format_value function solves this issue completely!

    I’ve tested using the above double get_field, and the output microtime is halved!

    I’ll include this in the next version

  • This is cool! Awesome work @wube, and huge props to @elliot for being so responsive to contributions 🙂

  • Hi @elliot,
    Sorry for not responding to your previous message. I didn’t notice your post.

    It’s a great news! Thank you for implementing this feature in the ACF core. Your solution is much simpler than mine. I’ve completely forgotten about the native wp_cache and tried to re-invent the wheel introducing my own caching class 🙂

  • Hi guys

    This simple caching idea has been added into recent versions of ACF PRO!

Viewing 8 posts - 1 through 8 (of 8 total)

The topic ‘Improve ACF performance – implement a simple caching (an example provided)’ is closed to new replies.