Using Advanced Custom Fields without frontend dependency

The performance issues in ACF have been resolved. You can use either ACF or core WordPress functions for accessing the data with the same result performance-wise.

Advanced Custom Fields is one of the most popular methods of creating metaboxes. It provides a simple visual interface for creating the metaboxes, and the resulting metaboxes look great and are easy for clients to edit.

ACF also includes a bunch of functions you can use in your theme to retrieve the values of the meta fields. But I recommend you don’t use these.

For some fields like images, this will introduce additional database queries. See the comment below where switching from ACF functions to the WP core function decreased database queries from 146 to 22!

There’s is no reason to use an ACF function like get_field() when you can use a WordPress core function like get_post_meta(). Using these ACF functions introduces what I’m calling “frontend dependency”.

If you use an ACF field and the ACF plugin is removed or deactivated (because clients…), visitors to the website will see the white screen of death instead of the website. If you go to the bother of wrapping all the ACF functions in function_exists(), the site will load but none of your metadata will be shown.

But the metadata still exists in the WordPress database. If you had used WordPress functions, there would be no changes to the frontend of the website. On the backend the metaboxes would be gone so they couldn’t edit those fields (unless they use the WP built-in Custom Fields metabox), but this is a much less serious issue than a broken website.

I’ve also found that ACF can cause performance issues on the frontend, even if you’re not using their fields. You can add this code to prevent the plugin from loading on the frontend.

This works fine for simple fields, but it might not be clear how the more complex fields are stored in the database. I’ll walk you through some examples.

Repeating Fields

On this page, as many videos can be included as they like (or none at all). I’m using a repeater field which includes a Title, Video URL and Thumbnail (sample fields).

repeating-videos

In your theme, if you look at the meta field for the repeater field ( get_post_meta( get_the_ID(), 'be_attorney_video', true ); ), it will give you the number of items within that field. You can then loop through and access all of them. Here’s how I’m assembling those videos:

<?php
$videos = get_post_meta( get_the_ID(), 'be_attorney_video', true );
if( $videos ) {
for( $i = 0; $i < $videos; $i++ ) {
$title = esc_html( get_post_meta( get_the_ID(), 'be_attorney_video_' . $i . '_title', true ) );
$video = esc_url( get_post_meta( get_the_ID(), 'be_attorney_video_' . $i . '_video', true ) );
$thumbnail = (int) get_post_meta( get_the_ID(), 'be_attorney_video_' . $i . '_thumbnail', true );
// Thumbnail field returns image ID, so grab image. If none provided, use default image
$thumbnail = $thumbnail ? wp_get_attachment_image( $thumbnail, 'be_video' ) : '<img src="' . get_stylesheet_directory_uri() . '/images/default-video.png" />';
// Displayed in two columns, so using column classes
$class = 0 == $i || 0 == $i % 2 ? 'one-half first' : 'one-half';
// Build the video box
echo '<div class="' . $class . '"><a href="' . $video . '">' . $thumbnail . '</a>' . $title . '</div>';
}
}
view raw functions.php hosted with ❤ by GitHub

This snippet also shows you how to use ACF image fields. They store the image ID, so you can get the actual image markup using wp_get_attachment_image( $image_id, 'image_size' );.

Flexible Content

In my last post I described setting up the HTML markup for Full Width Landing Pages. This section is a bit like “Part 2” of that post, since almost every page I set up in that way I’m using ACF’s Flexible Content field to populate the content area.

outboundengine

Flexible Content allows you to create different layouts, which are collections of fields. For instance, a Testimonial layout might have fields for Quote, Person’s Name, and a Photo. A website editor can then create a page by assembling these layouts in whatever order they like, and use them as many times as they like.

Take a look at the OutboundEngine homepage. The header contains a menu and call to action (graphic, text and form). The footer is the dark blue part at the bottom starting with “Sign up and watch your business grow”. Everything in between is the flexible content area. This page is made up of the following layouts: Features, Small Quote, Full Width Content, and Full Width Content.

For this example, my Flexible Content field has a name (meta_key) of ‘be_content’. I have two layouts: full width content (fields: title and content) and content with image (fields: title, content, image). Assuming you set up your page template like I did in my Full Width Landing Pages tutorial, this would be your template-landing.php file:

<?php
/**
* Your Child Theme
*
* Template Name: Landing
*/
/**
* Flexible Content
*
*/
function be_landing_page_content() {
$rows = get_post_meta( get_the_ID(), 'be_content', true );
foreach( (array) $rows as $count => $row ) {
switch( $row ) {
// Full Width Content layout
case 'full_width_content':
$title = esc_html( get_post_meta( get_the_ID(), 'be_content_' . $count . '_title', true ) );
$content = get_post_meta( get_the_ID(), 'be_content_' . $count . '_content', true );
echo '<div class="flex-content full-width-content"><div class="wrap">';
if( $title )
echo '<h2>' . $title . '</h2>';
if( $content )
echo '<div class="section-content">' . apply_filters( 'the_content', $content ) . '</div>';
echo '</div></div>';
break;
// Content with Image layout
case 'content_with_image':
$title = esc_html( get_post_meta( get_the_ID(), 'be_content_' . $count . '_title', true ) );
$content = get_post_meta( get_the_ID(), 'be_content_' . $count . '_content', true );
$image = get_post_meta( get_the_ID(), 'be_content_' . $count . '_image', true );
echo '<div class="flex-content content-with-image"><div class="wrap">';
if( $title )
echo '<h2>' . $title . '</h2>';
if( $image )
echo '<div class="section-image">' . wp_get_attachment_image( $image, 'medium' ) . '</div>';
if( $content )
echo '<div class="section-content">' . apply_filters( 'the_content', $content ) . '</div>';
echo '</div></div>';
break;
}
}
}
add_action( 'be_content_area', 'be_landing_page_content' );
// Remove 'site-inner' from structural wrap
add_theme_support( 'genesis-structural-wraps', array( 'header', 'footer-widgets', 'footer' ) );
// Build the page
get_header();
do_action( 'be_content_area' );
get_footer();

The ‘be_content’ meta field will be an array of layouts used on this page, in order. The fields in each layout will be stored based on that order. So if you have a field named ‘title’ and it’s in the first layout on the page, it will be stored as ‘be_content_0_title’ (counting starts at 0, so the first one is 0).

Options Pages

Metaboxes you add to options pages will store data as options instead of post meta. It will be the meta_key you specify, prefixed with ‘options_’. So, if you had an options page with a Call to Action box (sample fields), you would access those fields like this:

<?php
function be_call_to_action() {
$title = esc_html( get_option( 'options_be_cta_title' ) );
$button_text = esc_html( get_option( 'options_be_cta_button_text' ) );
$button_url = esc_url( get_option( 'options_be_cta_button_url' ) );
if( $title && $button_text && $button_url )
echo '<div class="call-to-action"><div class="wrap"><p>' . $title . '</p><p><a href="' . $button_url . '" class="button">' . $button_text . '</a></p></div></div>';
}
add_action( 'genesis_before_footer', 'be_call_to_action' );
view raw functions.php hosted with ❤ by GitHub

Term Metadata

Let’s say you added a Subtitle field to categories (sample fields). ACF stores these as options as well, with the following format: [taxonomy]_[term_id]_[field name]

So with a field name of ‘be_subtitle’, you’d use the following to access it on a category archive page:

<?php
/**
* Category Subtitle
*
*/
function be_category_subtitle() {
// Make sure this is a category archive
if( ! is_category() )
return;
$category = get_term_by( 'slug', get_query_var( 'category_name' ), 'category' );
$subtitle = esc_html( get_option( 'category_' . $category->term_id . '_be_subtitle' ) );
if( $subtitle )
echo '<p class="subtitle">' . $subtitle . '</p>';
}
add_action( 'genesis_before_loop', 'be_category_subtitle' );
view raw archive.php hosted with ❤ by GitHub

Bill Erickson

Bill Erickson is the co-founder and lead developer at CultivateWP, a WordPress agency focusing on high performance sites for web publishers.

About Me
Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Reader Interactions

Comments are closed. Continue the conversation with me on Twitter: @billerickson

Comments

  1. Vasili says

    Hi Bill

    Thank you for this post.

    I love ACF and i think it’s a great tool, but to bad it was not built to be used with get_post_meta() as a default. I was able to reduce a lot of queries by re-writing all fields values to get_post_meta() on my two music sites.

    I also have fields in widgets and i now see that each field, when used with get_field(), creates a minimum of 5 queries. Unfortunately i was not able to make it work with get_post_meta() as ACF field in widget require widget id as a second parameter which is not possible with get_post_meta() and i always get an empty string.

    Did you have a chance to check how ACF field can be pulled in widget with get_post_meta()?

    • Bill Erickson says

      get_post_meta() is only used for post metadata. I haven’t used ACF fields in widgets before, but I assume ACF is storing that as options. Take a look at the wp_options table to see how the data is stored, and then you can access it using get_option().

      • Vasili says

        Thank you for your reply.

        I was able to change field value call from get_field() to get_option() using this syntax:

        echo get_option( 'widget_' . $args['widget_id'] . '_field_name' );

        The only thing is that now the value is not changing via the Customizer, only after i hit the “Save & Publish” button and refresh the page on the front end.

  2. Damien Carbery says

    I changed my get_field/the_field calls for a ‘gallery’ field type. I use wp_prepare_attachment_for_js( $attachment_id ) to get an array with all the image info (alt, title, url, sizes).
    When I disable ACF the ‘featured-image’ size disappears from the list of available sizes despite being in the list returned by get_image_sizes(). As soon as ACF is enabled it’s back!

    So, while I have been able to change to WordPress API functions (query count dropped from 58 to 41 on single post and 92 down to 53 on category page), I cannot disable ACF on the front end. Bizarre and frustrating.

    • Bill Erickson says

      I’ve never used wp_prepare_attachment_for_js(). Why use that instead of wp_get_attachment_image( $attachment_id, $size ) to generate actual markup, or wp_get_attachment_image_src( $attachment_id, $size ) to get all the pieces?

      • Damien Carbery says

        The array returned by wp_prepare_attachment_for_js() had everything I needed, including title and alt. I thought that I had tried the wp_get_attachment_image* functions and that they didn’t give me enough info, but I’ don’t see them in my svn log history.

        I have changed my code to use them. It’s even easier to read now. And it works when ACF is disabled. Down to 38 queries and definitely faster (~1.5sec vs 2.1 with get_field()).
        Thanks for the function suggestion.

  3. Michael says

    I wonder, is there any real benefit of bothering doing this. Since users on the front end should be hitting a html cache version anyway. Those queries run only once.

    • Bill Erickson says

      The bigger issue is what happens to your site if/when ACF gets deactivated. Your options are:

      1. Use ACF functions like get_field(), and have your site fatal error if the plugin is deactivated.
      2. Wrap every custom field area in if( function_exists( 'get_field' ) ) which prevents the white screen but nothing shows up when ACF is deactivated.
      3. Use core WP functions like get_post_meta(), no issue if plugin isn’t active

      It also makes it easier to transition your code to another metabox solution in the future. If you later decide to use Carbon Fields or CMB2, you migrate the data and update the meta keys, but the underlying functionality stays the same. If you’re using plugin-specific functions you’ll need to rebuild more.

      It’s not a big deal either way. It’s unlikely the client will deactivate ACF or that you’ll need to migrate between metabox plugins.

      I frequently with all three metabox plugins (I prefer Carbon Fields but stick with ACF/CMB2 if the site already uses it) and so it’s easiest for me to build general code that works with any solution rather than leverage their plugin-specific functions.

      I typically use my own helper function, ea_cf(), for retrieving a custom field. Then if the metabox solution changes I can update the functions in there, once, and everything in the theme just works, rather than updating everything in the theme.

      • Michael says

        I don’t have problems 1 or 2 cos i don’t deal with clients but i confess keeping queries at a minimum is becoming some sort of fetish for me, it’s fun to watch.

        I keep reading “ACF doesn’t scale”. I guess unless you aren’t doing stupid things like filter by custom fields “conditions” you are good.

        But this intrigues me, i want to see how much total time can be saved disabling ACF on front end. I didn’t got rid of all get_field yet because logic needs some tweaks.

        Maybe i’m doing this the dumb way but what i did was a single database call for:
        $all_fields = get_post_meta($post_id);

        And then i just hand pick what i was displaying before from my big array:
        $manufacturer = $all_fields[‘manufacturer’][0];

        True It gets tricky on repeater and gallery and for loops but it works.

        What do you think of this approach? i confess i didn’t pay much attention to your examples but i think you do 1 get_post_meta for every get_field.

        • Bill Erickson says

          The metadata attached to all posts in the main loop (ex: 10 posts on archive, 1 post on single post view) is cached. Every call to get_post_meta() does not result in another database query – it just does what you’re doing manually.

          I believe (but have not tested) that when you run get_post_meta() for a post that hasn’t been cached yet, it retrieves all that post’s metadata, and any additional get_post_meta() calls to that post are also using cached data.

          There should be no performance . difference between get_post_meta( $id, 'manufacturer', true ); and $meta = get_post_meta( $id ); $manufacturer = $meta['manufacturer'][0];

          • Michael says

            Glad to hear ‘cos my brain is pretty limited and it was getting confusing (i’m not a programmer). So i’m gonna finish up with get_post_meta then. Thanks for the great post and even replying to my babbling! Cheers!

  4. Michael says

    I got stuck on the gallery field. Well not really stuck, i was able to replace each of ACF array values with WP functions in doing so, the query number goes to the roof, except now is core doing them. I was pulling these values from the gallery array:

    [‘caption’]
    [‘url’]
    [‘title’]
    [‘sizes’][‘thumbnail’]
    [‘alt’]

    Getting these from WP functions is more expensive (ACF array is VERY complete).
    So i guess there foes my dream of disabling ACF on front end.-
    PS: i’m using the blueimp lightbox which i think is pretty cool 😛 https://github.com/blueimp/Gallery

  5. Clay Teller says

    Hey Bill, after reading this, you’ve gotten me interested in making the switch from ACF Pro to Carbon Fields. On the Carbon Fields website (https://carbonfields.net/about/), implicit in their “Comparison with other plugins” section is that Carbon Fields offers a “flexible content alternative”. If that’s true, it would probably seal the deal for me switching over. Haven’t found in their documentation what the flexible content alternative is though. Do you know? Thanks!

  6. K says

    Curious – how does the Local JSON affect the overall performance (positively?) — because, truth be told, as much as I love performance and speed, in most cases ACF makes things happen quickly and efficiently and the websites run fine. I mean the added value of the ACF backend for Editors is still overpowering the performance hit in my opinion, but I am sensitive to it and I want to do better. Do you have a way of benchmarking / testing with Local JSON in tow?

    • Bill Erickson says

      I really need to update this post. I’ve heard that ACF has fixed most of the performance issues that caused the slow load time in the tests I ran for this post (published 4 years ago), but I haven’t re-run the tests.

      I’m adding it to my to-do list 🙂

      • Jamie says

        Hi Bill

        I’m just starting to use your method here to implement ACF’s instead of the get_field.

        So far I’ve got a fair bit working using your code examples in this post.

        But for the likes of me I can’t work out how to get images from the ACF gallery field using the get_post_meta. (to show a list of images)

        Thought you might have a code snippet handy? I can learn from that.

        Thanks!

        • Bill Erickson says

          I’m pretty sure the gallery field stores an array of attachment ids. So $images = get_post_meta( get_the_ID(), 'my_gallery', true ); should get you something like $images = array( 123, 456 );

          In that case, you can loop through them and call wp_get_attachment_image() like so:

          foreach( $images as $image ) {
          	echo wp_get_attachment_image( $image, 'medium' );
          }
          
  7. Andy says

    Hypothetically , what happens if a client deletes ACF wanting to remove all the data and discovers all the data is still showing on the front end ?

    They’ll find nothing in the back-end to edit or delete as the boxes are gone. What’s the workload in correcting that.

    I understand the need for speed in query counts , if that’s what’s required why install ACF to start with or why stick with it if it’s so bad ? It can be included in the theme or plugin (or even MU) , therefore cannot be deleted.

    if you use if( function_exists( ‘get_field’ ) ) or if( !class_exists(‘acf’) ) then no white screen anyway.

    • Bill Erickson says

      That’s an interesting thought. I think that’s a feature, not a bug.

      I would never expect nor want my metadata to be deleted when a metabox plugin is removed. The removal might be temporary, like when trying to find a plugin conflict on site, or downgrading from a beta version to the stable release.

      You might be transitioning from one metabox solution to another. I’ve often migrated metadata between ACF, Carbon Fields, and CMB2 depending on the project requirements.

      At it’s core, the metabox plugin’s job is to make metabox creation easier. It is not responsible for the actual metadata storage – that’s a core WordPress feature.

      If I remove an events calendar plugin from my site, I don’t expect it to delete all the events in the database. I expect it to leave the data untouched.

      Yes, you can resolve the “white screen” issue with function_exists(). I’m doing that currently on a site that depends upon ACF on the frontend (we’re using it for building Gutenberg blocks).

      The purpose of this post was to:
      a) Point out a frontend performance issue I found on sites using ACF
      b) Illustrate how to access metadata created by ACF using core WordPress functions
      c) Explain ACF’s naming structure for different data types

      Whether you use ACF across the whole site, only in the backend, or not at all, is a personal decision and one based on the requirements of the project.

  8. Geert van der Heide says

    Like Bill Erickson mentioned above, the performance hit from ACF has gone way down in the last few years. I was curious how much of an influence ACF has on the loading of my current project, a site that uses several ACF fields (including a flexible content field) on most pages. The result of deactivating ACF on the front-end and replacing its function calls with native functions was barely noticeable. Tested with Query Monitor, and both the loading time and the number of queries don’t go down much.

    It’s probably still beneficial to set your image fields to output the attachment ID only, and to then use wp_get_attachment_image_src or similar to retrieve the image URL, width and height. But anything else doesn’t seem worth it. So just use ACF functions or WP functions as desired.

  9. Stephen says

    I know this article is quite old and i used a few years ago. just came back to it after needing to revise an excessive use of ACF on the front end that what causing all sorts of issues. Should of commented when first used but should say this article in Timeless. It well written and extremely informative delivering immediate results in page performance.

    I have a block manager i wrote before guttenberg came out. It enables users to create blocks that are associated with templates. Each block can manage approximately 10 fields some of which are repeaters. In my case, was written to accomodate 4 or 5 blocks. Of course the client is now creating 10-12 blocks on a page. That equates to perhaps 100 or so get_fields. Removing get_field reducing the query considerably especially if you have object cache as the meta fields are stored there.

    I am though messing with an alternative way of handling. I wondered if possibly better to collect all the page meta in one hit with get_post_custom and then reference the array directly. I managed to reduce the querying on the database again quite considerably so when cache has been cleared i don’t have again as many queries from get_post_meta where the value is yet to be stored.

    just for peoples interest, starting to review CMB2