📖 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

  1. The Page Builder Problem
  2. Why We Chose ACF + Custom Blocks
  3. How We Structure Our ACF Blocks
  4. The Real Performance Impact
  5. Transitioning Clients From Page Builders
  6. Cost and Resource Comparison
  7. 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-names

Real 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:

TaskPage BuilderACF + Custom Blocks
Change heading font sizeFind the section → open Advanced → scroll to typography → pick from dropdownUse the WordPress Global Styles panel — one click
Add a repeating team member cardInstall addon → configure dynamic content → manually duplicate sectionsAdd the Team Members block → click “Add Member” → done
Update site-wide button stylesEdit every button individually or install another pluginUpdate one CSS custom property — all buttons change
Add structured data (Schema)Install yet another pluginBuilt into the block’s render callback
Export content to another CMSExport HTML riddled with shortcodes and inline stylesClean 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 sync on 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 sync

4. 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:

MetricPage Builder (Elementor/Divi)ACF + Custom BlocksImprovement
Average page weight2.8 MB620 KB78% reduction
HTTP requests per page85-12035-5555% reduction
Mobile PageSpeed (avg)48/10091/100+43 points
Desktop PageSpeed (avg)72/10097/100+25 points
Core Web Vitals pass rate22%94%+72% pass rate
CSS shipped per page250-450 KB (inline)15-35 KB (scoped)90% reduction
JS shipped per page200-350 KB20-60 KB80% reduction
Time to first paint3.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:

  1. Audit all layouts — map every page builder template to a custom block type (typically 5-12 block types cover 90% of layouts)
  2. Build blocks in parallel — develop blocks while the old site is still live
  3. Migrate page by page — deactivate the page builder on each page after rebuilding it with blocks
  4. Redirect shortcodes — page builder shortcodes in content will break; use the_content filters to gracefully degrade
  5. 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 FactorPage BuilderACF + 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 debuggingCommon (page builders + other plugins = trouble)Rare (standard WP architecture)
Site migration to another hostCan 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:

  1. ACF Blocks Documentation — Official ACF guide to registering custom Gutenberg blocks. The starting point for every block we build.
  2. 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+).
  3. 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.
  4. 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.
  5. 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.