I used ACF Pro for a long time. Defining custom post types in the ACF UI, building block-like structures with flexible content, all fast. But over the last 18 months of theme and plugin work I’ve set ACF aside and leaned on native PHP: register_post_type + Gutenberg block registration. Here’s why I switched and how it plays out on real projects.
ACF’s convenience and hidden cost
I liked ACF because it was easy to explain to a client: “You can add new fields here, your content editor sees them right away.” Designing fields in the UI is genuinely fast.
But after 4 or 5 projects the costs I was no longer willing to pay became visible:
An extra plugin dependency. You pay for ACF Pro, a separate license per client (using one license on multiple sites may violate the ToS). It’s a yearly spend, not a one-off.
Field groups live in the database. Even if you define ACF field groups in PHP, the meta structure still lands in the DB, which causes friction when syncing staging to production.
Meta query weight. get_field() hits the DB on every call. Caching needs extra code. Calling get_field() in a loop on a post with 50 custom fields tanks page speed.
Developer experience in code review. If ACF fields are defined in the UI, they don’t show up in the git repo, code review is impossible. Exporting to PHP produces long arrays that are painful to diff.
Version coupling. A major ACF upgrade means regression-testing the theme. A plugin-dependent theme is hard to maintain.
Native CPT: register_post_type
Defining a custom post type in PHP is 30 lines. Inside the theme:
add_action('init', function() {
register_post_type('project', [
'label' => 'Projects',
'public' => true,
'has_archive' => 'projects',
'rewrite' => ['slug' => 'project'],
'supports' => ['title', 'editor', 'thumbnail', 'custom-fields'],
'show_in_rest' => true,
]);
});Use register_taxonomy for taxonomies, register_post_meta for meta fields, and you get native REST API integration for free. The Gutenberg editor recognises the structure automatically.
Advantages over ACF:
- It’s in git, code review works
- Activation/deactivation lifecycle is clear
- No plugin dependency
- Performance: no extra table writes
Gutenberg block: register_block_type
Writing a native Gutenberg block instead of an ACF Block has a steeper learning curve, but once you’ve done one you come around.
Minimal native block structure:
- A
block.jsonfor block metadata - A JS file with edit + save functions (or a server-side render callback for dynamic blocks)
- A CSS file for styles
{
"apiVersion": 3,
"name": "alicinaroglu/stat-card",
"title": "Stat Card",
"category": "design",
"attributes": {
"value": {"type": "string"},
"label": {"type": "string"}
},
"supports": {"html": false}
}On the PHP side:
register_block_type(__DIR__ . '/blocks/stat-card');The JS edit component shows the fields to the user, the save function emits HTML, or a render callback does server-side rendering.
Dynamic block vs static block
Static block: the JS save function writes HTML into post_content permanently. No extra work at render time.
Dynamic block: save returns null, and a server-side PHP callback renders the block. Every page render gets fresh content. For blocks that pull a post list, show latest content, or read from external sources, dynamic is the right call.
I lean dynamic by default. If the theme updates and the markup changes, old posts pick up the new rendering automatically.
InnerBlocks for nested structure
The cleanest native replacement for flexible content is InnerBlocks. You define a container block and declare which blocks can go inside:
<InnerBlocks
allowedBlocks={['core/heading', 'core/paragraph', 'alicinaroglu/cta-button']}
template={[['core/heading', {level: 2}]]}
/>The user builds the page block by block in the UI. You get the full flexibility of ACF flexible content, natively.
Meta fields: CMB2 or native
You can define custom sidebar panels in the Gutenberg editor to edit meta fields. For power users that’s a JS-plugin approach. When I want a shortcut I use CMB2 (free, MIT), 4 to 5x lighter than ACF.
Learning cost is real, the payoff is worth it
Native block development is harder than ACF. You need React, a build pipeline (webpack or wp-scripts), and an understanding of block attributes plus the edit/save split.
But that investment is one-off. By the third native block the curve flattens and you move fast. ACF means clicking through the UI on every new project, forever.
The budget angle
Across three mid-sized projects, moving from ACF Pro to native saved:
- Roughly 600 USD a year in license fees
- 100 to 150ms of TTFB p50 (plugin overhead gone)
- Theme deploy times cut in half (no plugin dependency checks)
My recommendation
It’s been 18 months since I last used ACF on a new project and I don’t regret it. Migrating existing ACF installs is easier than I expected: the fields already live in post_meta, so once you remove ACF, get_post_meta works directly. You only rewrite the validation and admin UI.
Staying closer to WordPress native buys you easier long-term maintenance. Giving up the plugin ecosystem’s comfort is hard at first, but the dividends come in later.