📖 The Turning Point
In 2020, we delivered a 40-page Elementor site for a real estate client in Hanoi. The design was stunning. The client approved it on the first presentation. Everyone was happy — until three months later.
The client wanted to add a custom property comparison table. In Elementor, this meant installing a third-party addon plugin, configuring 15 different settings, and fighting with inline CSS overrides. Total time: 6 hours for what should have been a 30-minute task. Six months later, the site had 8 premium addon plugins installed, weighed 23 MB per page with inline CSS, and scored 38 on mobile PageSpeed.
That project was the wake-up call that pushed us to abandon page builders entirely. Today, every project we build at CODE TOT uses ACF (Advanced Custom Fields) with custom Gutenberg blocks. Here’s why — and how you can make the switch too.
📖 Table of Contents
- The Page Builder Problem
- Why We Chose ACF + Custom Blocks
- How We Structure Our ACF Blocks
- The Real Performance Impact
- Transitioning Clients From Page Builders
- Cost and Resource Comparison
- External References and Further Reading
1. The Page Builder Problem
Page builders like Elementor, Divi, WPBakery, and Beaver Builder were a revolution in their time. They made WordPress design accessible to non-developers. But as a professional agency serving clients who need performance, maintainability, and scalability, page builders create three fundamental problems.
1.1 Performance Bloat
Page builders inject massive amounts of inline CSS, JavaScript, and HTML comments into every page — even pages that don’t use them. Elementor alone can add 200-400 KB of CSS and 150-300 KB of JavaScript per page load.
# Check how much bloat a page builder adds to your site
# Run this against your WordPress database
# Elementor inline CSS styles stored in post meta
wp post meta get POST_ID _elementor_css
# Total Elementor post meta size across all pages
wp db query "
SELECT SUM(LENGTH(meta_value)) AS total_bytes
FROM wp_postmeta
WHERE meta_key LIKE '_elementor_%'
" --skip-column-names
# Count Elementor meta entries
wp db query "
SELECT COUNT(*) AS elementor_meta_entries
FROM wp_postmeta
WHERE meta_key LIKE '_elementor_%'
" --skip-column-namesReal data from our projects: After migrating a 35-page Divi site to ACF custom blocks, total page weight dropped from 4.2 MB per page to 780 KB. Mobile PageSpeed went from 42 to 94. The difference? No unused CSS. No inline JavaScript. Clean, semantic HTML.
1.2 Vendor Lock-In
When you build with a page builder, you’re renting your design system from a third party. If Elementor changes its rendering engine (which it has, multiple times), your layouts can break. If Divi updates its module API (which it did in 2024), your custom modules need rewrites. If the plugin’s license expires, your client can’t even edit the site.
A client’s website should be built on WordPress core, not on top of a page builder’s proprietary layer. Custom blocks register natively with WordPress’s block editor — they work on any WordPress 5.0+ installation with zero third-party dependencies.
1.3 Editor Experience Degradation
Paradoxically, page builders that promise “easy editing” often create the worst editing experience:
| Task | Page Builder | ACF + Custom Blocks |
|---|---|---|
| Change heading font size | Find the section → open Advanced → scroll to typography → pick from dropdown | Use the WordPress Global Styles panel — one click |
| Add a repeating team member card | Install addon → configure dynamic content → manually duplicate sections | Add the Team Members block → click “Add Member” → done |
| Update site-wide button styles | Edit every button individually or install another plugin | Update one CSS custom property — all buttons change |
| Add structured data (Schema) | Install yet another plugin | Built into the block’s render callback |
| Export content to another CMS | Export HTML riddled with shortcodes and inline styles | Clean HTML output — any CMS can ingest it |
2. Why We Chose ACF + Custom Blocks
We evaluated several approaches before settling on ACF + custom blocks:
- Pure Gutenberg core blocks — Too limited for complex layouts, no custom field support without workarounds
- Advanced Gutenberg / CoBlocks — Better than core, but still constrained and introduce dependency risks
- Laravel + Nova — Too heavy for content sites, loses the WP admin ecosystem
- ACF Flexible Content — Great, but limited to the classic editor experience and templating
- ACF Custom Blocks — The sweet spot: full control, native Gutenberg integration, clean output
2.1 What ACF Custom Blocks Give You
ACF Pro’s block registration API allows you to register custom Gutenberg blocks that use ACF field groups as the data source. The block renders via a PHP template in your theme — no React knowledge required.
// Step 1: Register the block in your theme's functions.php or a plugin
add_action('acf/init', function () {
acf_register_block_type([
'name' => 'testimonial-grid',
'title' => __('Testimonial Grid'),
'description' => __('A grid of client testimonials with ratings.'),
'render_template' => 'blocks/testimonial-grid.php',
'category' => 'custom-blocks',
'icon' => 'testimonial',
'keywords' => ['testimonial', 'review', 'social proof'],
'mode' => 'edit',
'supports' => [
'align' => ['wide', 'full'],
'jsx' => true,
],
]);
});
// Step 2: Create blocks/testimonial-grid.php in your theme
// This is the render template — pure PHP, clean output
// Step 3: Create an ACF Field Group with Location Rule:
// Block: Testimonial Grid
// Step 4: Add fields like:
// - heading (text)
// - testimonials (repeater)
// - name (text)
// - company (text)
// - avatar (image)
// - content (textarea)
// - rating (number, min:1 max:5)2.2 block.json Registration (Native Approach)
WordPress 6.0+ introduced the block.json standard, which works even better with ACF. This is our current preferred method:
// blocks/hero-banner/block.json
{
"name": "codetot/hero-banner",
"title": "Hero Banner",
"description": "Full-width hero banner with heading, text, CTA, and background.",
"category": "codetot-blocks",
"icon": "welcome-widgets-menus",
"keywords": ["hero", "banner", "cta", "header"],
"acf": {
"mode": "auto",
"renderTemplate": "blocks/hero-banner/render.php"
},
"supports": {
"align": ["full"],
"anchor": true,
"color": {
"background": true,
"text": true
}
},
"styles": [
{ "name": "default", "label": "Default", "isDefault": true },
{ "name": "overlay", "label": "Dark Overlay" }
]
}// blocks/hero-banner/render.php
// PHP render template — automatically loaded by WordPress
if (!defined('ABSPATH')) exit;
$heading = get_field('heading');
$text = get_field('text');
$cta = get_field('cta_button');
$background = get_field('background_image');
$block_classes = 'codetot-hero';
if (get_field('style') === 'overlay') {
$block_classes .= ' codetot-hero--overlay';
}
?>
<section class="<?= esc_attr($block_classes) ?>"
<?php if ($background): ?>
style="background-image: url('<?= esc_url($background['url']) ?>')"
<?php endif; ?>>
<div class="codetot-hero__content">
<?php if ($heading): ?>
<h1 class="codetot-hero__heading">
<?= esc_html($heading) ?>
</h1>
<?php endif; ?>
<?php if ($text): ?>
<p class="codetot-hero__text">
<?= wp_kses_post($text) ?>
</p>
<?php endif; ?>
<?php if ($cta && $cta['url']): ?>
<a href="<?= esc_url($cta['url']) ?>"
class="codetot-hero__cta button button--primary"
<?= $cta['target'] ? 'target="_blank"' : '' ?>>
<?= esc_html($cta['title']) ?>
</a>
<?php endif; ?>
</div>
</section>3. How We Structure Our ACF Blocks
After 40+ projects using this approach, here’s the structure that has proven most maintainable:
3.1 File Organization
themes/codetot-theme/
├── blocks/
│ ├── hero-banner/
│ │ ├── block.json
│ │ ├── render.php
│ │ ├── style.css # Block-specific styles (5-10KB max)
│ │ └── preview.png # Block preview in editor
│ ├── testimonial-grid/
│ │ ├── block.json
│ │ ├── render.php
│ │ └── style.css
│ ├── pricing-table/
│ │ ├── block.json
│ │ ├── render.php
│ │ └── style.css
│ └── ...
├── inc/
│ └── block-registration.php # Bulk register all blocks
└── acf-json/ # ACF Local JSON — version controlled
├── group_xxx-hero.json
├── group_xxx-testimonials.json
└── ...3.2 Bulk Block Registration
// inc/block-registration.php
add_action('acf/init', function () {
$blocks_dir = get_template_directory() . '/blocks/';
foreach (glob($blocks_dir . '*', GLOB_ONLYDIR) as $block_dir) {
$block_json = $block_dir . '/block.json';
if (file_exists($block_json)) {
$block_data = wp_json_file_decode($block_json, ['associative' => true]);
if (!empty($block_data)) {
// Merge theme defaults with block.json settings
$block_data['render_template'] = $block_dir . '/render.php';
acf_register_block_type($block_data);
}
}
}
});This approach means adding a new block is literally creating a folder with block.json and render.php. Everything else is automatic. Junior developers on our team can ship a new block in under an hour.
3.3 ACF Local JSON
ACF field groups are stored in the database by default. We override this to store them as JSON files in our theme’s acf-json/ directory. This means all field configurations are:
- Version controlled — Changes show up in Git diffs
- Deployable — Sync fields with
wp acf syncon staging/production - Reviewable — Code reviews can catch field configuration mistakes
// In functions.php — enable ACF Local JSON
add_filter('acf/settings/save_json', function () {
return get_template_directory() . '/acf-json';
});
add_filter('acf/settings/load_json', function ($paths) {
$paths[] = get_template_directory() . '/acf-json';
return $paths;
});
# Sync ACF fields from JSON files (run after deploy)
wp acf sync4. The Real Performance Impact
We track performance metrics across every project. Here’s what the data shows across 175+ projects that we’ve built or migrated:
| Metric | Page Builder (Elementor/Divi) | ACF + Custom Blocks | Improvement |
|---|---|---|---|
| Average page weight | 2.8 MB | 620 KB | 78% reduction |
| HTTP requests per page | 85-120 | 35-55 | 55% reduction |
| Mobile PageSpeed (avg) | 48/100 | 91/100 | +43 points |
| Desktop PageSpeed (avg) | 72/100 | 97/100 | +25 points |
| Core Web Vitals pass rate | 22% | 94% | +72% pass rate |
| CSS shipped per page | 250-450 KB (inline) | 15-35 KB (scoped) | 90% reduction |
| JS shipped per page | 200-350 KB | 20-60 KB | 80% reduction |
| Time to first paint | 3.2s (mobile 3G) | 1.1s (mobile 3G) | 66% faster |
“The fastest request is the one never made. Page builders make hundreds of requests your site doesn’t need. ACF blocks ship only what you actually use.” — Our speed optimization case study
These numbers come from real Lighthouse audits on production sites, not synthetic benchmarks. The improvement holds across every hosting environment we’ve tested — from shared hosting on RunCloud to dedicated VPS setups.
5. Transitioning Clients From Page Builders
The hardest part isn’t the technology — it’s convincing clients that a simpler editor is actually better for them. Here’s our playbook:
5.1 The “Dashboard Tour” Demo
Instead of explaining the technology, we sit clients down and compare side-by-side:
- Page builder: Sidebar panels, tabs, accordions, 50+ settings per element, inline styles, hidden by 15 plugin interfaces
- ACF custom blocks: Clean WordPress editor, one block = one purpose, fields labeled in plain language, familiar native UI
Every single time, clients choose the cleaner interface. They don’t want more options — they want the right options at the right time.
5.2 Migration Strategy
When migrating existing page builder sites:
- Audit all layouts — map every page builder template to a custom block type (typically 5-12 block types cover 90% of layouts)
- Build blocks in parallel — develop blocks while the old site is still live
- Migrate page by page — deactivate the page builder on each page after rebuilding it with blocks
- Redirect shortcodes — page builder shortcodes in content will break; use
the_contentfilters to gracefully degrade - Test for regression — compare screenshots of old vs new pages visually
5.3 Communicating the Value to Clients
# Script for client conversations:
#
# "With the page builder, you hire me every time you need a new layout style.
# With custom blocks, you hire me once to build the block, then you use it
# as many times as you want — like adding an app to your phone instead of
# calling someone to build each screen from scratch."6. Cost and Resource Comparison
Let’s be honest: custom blocks have a higher upfront cost. A page builder lets you design visually for free (or cheap). Custom blocks require developer time to register, build, and style. But the long-term economics favor custom blocks significantly.
| Cost Factor | Page Builder | ACF + Custom Blocks |
|---|---|---|
| Initial theme/setup cost | $0 (free builder) or $59-$199/year (premium) | $800-$2,500 (block development) |
| Adding a new layout type | $50-$200 (visual design time) | $300-$600 (developer to build block) |
| Adding a new variant of existing layout | $30-$100 (copy + modify) | $0 (uses existing block with different content) |
| Monthly page builder license | $59-$299/year per site | $0 |
| Performance optimization retainer | $200-$500/month (fighting bloat) | $0-$100/month (minimal adjustments) |
| Plugin conflict debugging | Common (page builders + other plugins = trouble) | Rare (standard WP architecture) |
| Site migration to another host | Can break layouts (caching, CSS loading) | Seamless (standard WP content) |
| Year 1 total (typical site) | $1,200-$4,700 | $1,100-$3,700 |
| Year 3 total (cumulative) | $2,800-$10,500 | $1,400-$4,900 |
Key insight: By year 2-3, custom blocks are almost always the cheaper option — and your site is faster, more secure, and easier to maintain. As we wrote in our article on the real cost of cheap websites, the cheapest option upfront is rarely the most affordable in the long run.
7. External References and Further Reading
Here are the resources that informed our approach and that we recommend to any agency considering the switch:
- ACF Blocks Documentation — Official ACF guide to registering custom Gutenberg blocks. The starting point for every block we build.
- WordPress block.json Metadata Reference — WordPress.org’s definitive reference for the block.json registration standard (introduced in WP 5.8, enhanced in 6.0+).
- Introducing the Block Supports API — WordPress core team post explaining how block supports work, enabling features like color, typography, and spacing controls inherited from the theme.json system.
- State of the Word 2024 — Matt Mullenweg’s annual address covering WordPress’s direction, including the continued investment in the Block Editor, performance improvements, and the ecosystem shift toward block-based theming.
- Website Speed Optimization: Grade E to Grade A — Our own case study showing how moving away from bloat-heavy plugins and builders contributed to a 48-hour turnaround from a failing Lighthouse score to 94+ on mobile.
Conclusion
Page builders served an important purpose — they democratized web design for a generation of builders. But as a professional agency serving clients who care about performance, maintainability, and long-term total cost of ownership, the page builder era is ending for serious projects.
ACF + custom Gutenberg blocks give us:
- 90% less CSS bloat — ship only what the page actually uses
- Zero vendor lock-in — blocks are native WordPress, no third-party plugin required
- Better client experience — editors use clean, purpose-built interfaces
- Full design control — every pixel is intentional, not inherited from a page builder’s default styles
- Better economics — higher upfront cost, but dramatically lower lifetime cost
If you’re an agency still using page builders for client projects, start the transition today. Pick one layout pattern — a testimonial grid, a pricing table, a feature showcase — and build it as a custom block. Once you see the difference in code quality, rendering speed, and client satisfaction, you won’t go back.
At CODE TOT, we’ve been building custom-block WordPress sites for over 5 years. If you need help migrating your site from a page builder or building a new fast, custom-block WordPress site, get in touch.


