← Back to blog

ACF Blocks vs Native Gutenberg Blocks: When to Use Which

· 8 min read · WordPress

I’ve registered more custom blocks than I care to count. When ACF introduced its blocks feature back in ACF 5.8, it felt like a gift — finally, a way to build Gutenberg blocks without React. But the longer I work on complex projects, the more nuanced my opinion has become. Neither approach is universally superior. Choosing the wrong one costs your team real time and real money, so let me walk through exactly how each one works, where each one shines, and what a real-world migration actually looks like.

What ACF Blocks Actually Are

ACF Blocks are custom Gutenberg blocks registered through ACF’s PHP API using acf_register_block_type(). They use ACF field groups for the block’s data model and render the output through a standard PHP template — exactly the same way you’d write any other WordPress template file. No webpack, no JSX, no build step required.

Under the hood, ACF blocks are still proper Gutenberg blocks. They get registered with register_block_type() internally, they store their data in the post content as block comments, and they show up in the block inserter like any other block. The difference is that ACF handles the entire React editor UI for you, generating fields from your ACF field group and wrapping everything in a block preview that re-renders on change. It’s a layer of abstraction on top of the native block API.

Here’s what registration looks like in PHP:

<?php
add_action( 'acf/init', 'register_testimonial_block' );

function register_testimonial_block() {
    if ( ! function_exists( 'acf_register_block_type' ) ) {
        return;
    }

    acf_register_block_type( [
        'name'              => 'testimonial',
        'title'             => __( 'Testimonial' ),
        'description'       => __( 'A single customer testimonial.' ),
        'render_template'   => get_template_directory() . '/blocks/testimonial/testimonial.php',
        'category'          => 'theme',
        'icon'              => 'format-quote',
        'keywords'          => [ 'testimonial', 'quote', 'review' ],
        'supports'          => [
            'align' => false,
            'jsx'   => true,
        ],
    ] );
}

And the render template, blocks/testimonial/testimonial.php, is plain PHP:

<?php
$quote  = get_field( 'quote' );
$author = get_field( 'author' );
$role   = get_field( 'author_role' );
$avatar = get_field( 'avatar' );
$class  = 'testimonial-block' . ( ! empty( $block['className'] ) ? ' ' . $block['className'] : '' );
?>

<figure class="<?php echo esc_attr( $class ); ?>">
    <blockquote>
        <p><?php echo wp_kses_post( $quote ); ?></p>
    </blockquote>
    <figcaption>
        <?php if ( $avatar ) : ?>
            <?php echo wp_get_attachment_image( $avatar['ID'], 'thumbnail' ); ?>
        <?php endif; ?>
        <cite>
            <strong><?php echo esc_html( $author ); ?></strong>
            <?php if ( $role ) : ?>
                <span><?php echo esc_html( $role ); ?></span>
            <?php endif; ?>
        </cite>
    </figcaption>
</figure>

Add your ACF field group, target the block, and you’re done.

How Native Gutenberg Blocks Work

Native blocks are built in JavaScript using React (or Preact-compatible JSX). The official toolchain is @wordpress/scripts, which wraps webpack under a zero-config interface. Each block lives in its own directory with a block.json manifest that declares the block’s metadata, attributes, and asset file paths.

A native block has two distinct rendering surfaces: the edit function (what appears in the editor, written in React) and the save function (the static HTML serialized into post content, or null for dynamic/server-rendered blocks). This separation is one of the most important things to understand — if your save output ever changes in a breaking way, Gutenberg will flag existing blocks as invalid.

Developer Experience Side by Side

The ACF approach wins on onboarding speed by a wide margin. A PHP developer who has never touched React can have a working block in under an hour. The field registration is handled through ACF’s familiar UI or PHP API, the template is just a PHP file, and debugging is straightforward — you’re reading PHP error logs, not chasing hydration mismatches or stale closure bugs in a webpack bundle.

Native blocks have a steeper ramp, but once you’re comfortable, the editor experience you can build is dramatically richer. Inline editing with RichText, complex sidebar controls, block transforms, block variations, nested blocks with constrained templates — all of this is either easier or only possible in a native block. ACF’s preview mode (where the block re-renders by making an AJAX call to the server) introduces a noticeable lag and a loading spinner that feels jarring compared to the instant, in-place editing native blocks provide.

Build Tooling Differences

ACF blocks have zero build requirements. You can add a new field to an ACF block, push it to production, and it works. No CI pipeline change, no npm install, no webpack config to maintain.

Native blocks require @wordpress/scripts at minimum — which is excellent for what it is, but it brings real operational overhead. You need Node installed on every developer machine, a build artifact that needs committing or a CI step that generates it, and a process for keeping @wordpress/scripts updated as the WordPress block API evolves. On a small agency team shipping a dozen sites a year, this friction compounds fast.

That said, for a product plugin — like the kind of work I do day-to-day at SeedProd — the build pipeline is already a first-class concern. TypeScript, unit tests, Playwright e2e tests, automated releases. In that context, adding a block’s build output to the pipeline is trivial.

Performance Implications

This one’s often misunderstood. ACF blocks are server-rendered by default: every page load runs the PHP render callback. This means your block output is always dynamic — great for content that depends on current query data or user state, but it does mean the block’s HTML is generated at request time rather than stored statically.

Native blocks with a non-null save function serialize their HTML directly into post_content. That HTML is stored in the database and served as-is, with no PHP execution required beyond the initial page query. For a static testimonial block, this is a meaningful performance win, especially on cached pages where the full WordPress bootstrap might be skipped.

For either approach, if you’re using full-page caching (I lean heavily on Cloudflare’s cache rules for this), the difference becomes negligible. But for high-traffic pages that can’t be cached — membership content, personalized views — the static save approach of native blocks is worth considering.

When ACF Blocks Are the Right Call

Three situations where ACF blocks are the obvious choice:

  • Agency work with PHP-first teams. When the team building and maintaining the site knows PHP deeply but JavaScript only casually, ACF blocks are the pragmatic choice. You get Gutenberg compatibility without a React tax on every future maintenance task.
  • Rapid prototyping and client projects. When a client needs a new block for a campaign landing page by end of week, ACF blocks let me move fast. I can scaffold a block, wire up fields, and hand it to a content editor the same day.
  • Blocks that display dynamic, query-dependent data. Things like “recent posts by this author” or “related products from the same category” — anything that needs live PHP data on every render — are actually a better conceptual fit for server-rendered ACF blocks than for a native block with a render_callback tacked on.

When Native Blocks Are the Right Call

Native blocks make more sense when:

  • You need rich, inline editing UX. If the block experience matters — if the content editor needs to type directly into the block and see real formatting without a loading spinner — native blocks are the only real option.
  • You’re building for distribution. If the block is going into a public plugin or theme, you cannot require ACF Pro as a dependency. ACF Blocks require ACF Pro, full stop. Native blocks have zero third-party dependencies.
  • You need complex InnerBlocks templates. Native blocks give you fine-grained control over allowed blocks, block templates, and block locking in a way that’s much harder to replicate with ACF.
  • The block has significant client-side interactivity. The WordPress Interactivity API is built around native blocks. If your block needs reactive state, native blocks integrate with @wordpress/interactivity far more cleanly.

Migrating from ACF to Native Blocks

I’ve done this migration twice on real production sites, and it’s not as painful as it sounds, but it does require a plan. The key steps I follow:

  1. Audit your field groups. Map every ACF field to a native block attribute.
  2. Register the native block under the same name. If your ACF block was named acf/testimonial, register your native block as acf/testimonial initially. This means existing post content won’t break.
  3. Write a deprecation entry. The block deprecation API lets you define the old save structure so Gutenberg can recover existing blocks.
  4. Use a render_callback first, then go static. Start with "render": "file:./render.php" in your block.json to keep the PHP render during the transition.

The Future Direction of Both Approaches

ACF was acquired by WP Engine in 2023, and development has continued steadily. ACF Blocks are unlikely to disappear — there are too many production sites depending on them. But I do think the gap between ACF blocks and native blocks is narrowing, and not in the direction ACF developers might hope. The WordPress core team is investing heavily in the Interactivity API, block bindings, and the block editor’s design system. These are all native-block-first features.

The introduction of "render": "file:./render.php" in block.json was a significant move by core — it essentially gives native blocks the PHP server rendering that was ACF blocks’ main selling point, without needing ACF at all. For new projects where the team is comfortable with a minimal build step, I’ve started leaning toward this pattern: block.json with a PHP render file, no React in the save function, and just enough JS for the editor preview.

For new projects where the team is comfortable with a minimal build step, this hybrid pattern is increasingly where I land.

My Bottom Line

If you’re building a bespoke theme for a client and your team lives in PHP, use ACF blocks. If you’re building a product plugin, a block library intended for the WordPress.org repository, or anything that needs native editor integration, use native blocks. If you’re somewhere in between — a growing agency starting to invest more in the block ecosystem — consider the block.json + PHP render hybrid as your on-ramp to native without abandoning PHP entirely.

Neither approach is wrong. ACF blocks are faster to build and easier to maintain for PHP teams. Native blocks offer a richer editor experience and zero third-party dependencies. Pick based on your team’s skills and the project’s maintenance story, not based on what’s trending on WordPress Twitter.