Skip to main content

Performance, Debugging, Migration, and Launch Checklist

ACF release safety depends on repeatable operational controls, not heroic launch-day troubleshooting.

Learning Focus

You will implement performance diagnostics, build migration scripts with dry-run and rollback thinking, and execute a launch checklist that validates runtime schema and data integrity through WP-CLI.

Concept Overview

As ACF projects grow, two risk classes dominate: performance regression and data migration errors. Performance issues often come from repeated field retrieval in nested loops, expensive relational lookups, and unbounded structural field payloads. Migration issues usually come from schema-name changes without deterministic data transfer rules.

A production-ready operations model starts with observability: profile requests, inspect query counts, log fallback paths, and quantify payload sizes. Then apply migration discipline: define source-to-target mappings, run dry-run checks, execute idempotent writes, and verify sample sets immediately.

Launch readiness is a systems process. It includes dependency checks (ACF Pro active/version), schema checks (acf_get_field_groups() counts), migration verification, and rollback preparation. The checklist is not bureaucracy; it is how you compress incident risk before changes reach users.

Core Idea

Treat ACF launches like data operations: profile first, migrate with safeguards, verify with CLI, and keep rollback paths explicit.

Why It Matters

ApproachWhat HappensImpact in Production
Profile and optimize field-heavy templates before launchBottlenecks are reduced before traffic spikeBetter page speed and conversion stability
Run migration scripts with dry-run + sample verificationData transfer issues are caught before full applyLower risk of silent content corruption
Use explicit launch gates and rollback planTeams know go/no-go state objectivelyFaster incident response and less downtime
Track migration results and error counts in options/logsPost-release validation is evidence-basedBetter accountability and auditability
Deploy schema changes without operational checks (wrong pattern)Regression discovered under live trafficEmergency rollbacks, data recovery pressure, reputational risk

Reference Table

Term/APISignature/SyntaxPurposeKey Notes
acf_get_field_groups()acf_get_field_groups(array $filter = []): arrayValidate runtime schema presence/countBaseline smoke check before migrations
get_field()`get_field(string $selector, intstring $post_id = false, bool $format_value = true): mixed`Read source values in migration scripts
update_field()`update_field(string $selector, mixed $value, intstring $post_id = false): intbool`
have_rows()`have_rows(string $selector, intstring $post_id = false): bool`Traverse structural fields in diagnostics
wp eval-filewp eval-file <path>Execute migration or audit scripts in WP contextPrefer committed scripts over ad-hoc eval blocks
SAVEQUERIESdefine('SAVEQUERIES', true)Capture query traces for profilingUse only in staging/debug mode
Migration report optionupdate_option('acf_migration_report', [...], false)Persist migration results for auditInclude counts, errors, timestamps
Launch gate commandwp eval '...' bundleValidate dependencies, schema, and sample dataGate release automation on non-zero exit

Practical Use Cases

Use Case 1 — Diagnose and reduce template bottlenecks in field-heavy pages

A landing page with nested ACF structures becomes slow after adding several sections. You need measurable diagnostics and a remediation pattern that can be repeated before every release.

  1. Enable staging diagnostics for query tracing.
  2. Add loop-level timers around field-heavy sections.
  3. Reduce repeated get_field() calls by caching values locally.
  4. Measure before/after query counts and execution time.
  5. Store performance report for release notes.
wp-content/themes/clinic-pro/inc/acf/performance-audit.php
<?php

declare(strict_types=1);

add_action('template_redirect', function (): void {
if (!is_page_template('template-service.php')) {
return;
}

$start = microtime(true);
$queryCountBefore = isset($GLOBALS['wpdb']) ? count((array) $GLOBALS['wpdb']->queries) : 0;

$hero = get_field('service_hero_group');
$faq = get_field('service_faq_items');
$sections = get_field('landing_sections');

// Simulate render-path access pattern with normalized local variables.
$heroHeadline = is_array($hero) ? (string) ($hero['service_hero_headline'] ?? '') : '';
$faqCount = is_array($faq) ? count($faq) : 0;
$sectionCount = is_array($sections) ? count($sections) : 0;

if ($heroHeadline !== '') {
echo '<h1>' . esc_html($heroHeadline) . '</h1>';
}

echo '<!-- faq_count=' . esc_html((string) $faqCount) . ' section_count=' . esc_html((string) $sectionCount) . ' -->';

$elapsedMs = (microtime(true) - $start) * 1000;
$queryCountAfter = isset($GLOBALS['wpdb']) ? count((array) $GLOBALS['wpdb']->queries) : 0;

update_option('acf_perf_audit_report', [
'template' => 'template-service.php',
'elapsed_ms' => round($elapsedMs, 2),
'query_count_delta' => max(0, $queryCountAfter - $queryCountBefore),
'faq_count' => $faqCount,
'section_count' => $sectionCount,
'captured_at' => gmdate('c'),
], false);

error_log('[acf-perf] elapsed_ms=' . round($elapsedMs, 2) . ' q_delta=' . max(0, $queryCountAfter - $queryCountBefore));
});
terminal: command
wp eval 'print_r(get_option("acf_perf_audit_report"));'
wp eval '$r=get_option("acf_perf_audit_report"); echo "elapsed_ms=" . ($r["elapsed_ms"] ?? "n/a") . PHP_EOL; echo "query_delta=" . ($r["query_count_delta"] ?? "n/a") . PHP_EOL;'
terminal: output
Array
(
[template] => template-service.php
[elapsed_ms] => 42.17
[query_count_delta] => 13
[faq_count] => 2
[section_count] => 3
[captured_at] => 2026-02-23T13:48:22+00:00
)
elapsed_ms=42.17
query_delta=13
note

Profiling data is most useful when captured consistently across the same template and content fixture before/after each change.

Use Case 2 — Safe field rename migration with dry-run and idempotent apply

A redesign renames hero_title to hero_headline across hundreds of pages. You need migration logic that can run multiple times safely and provide an auditable report.

  1. Build migration script with dry-run mode.
  2. Iterate target posts and inspect source/target values.
  3. Apply only when source exists and target is empty.
  4. Record migrated/skipped/error counts.
  5. Persist report and verify sample rows via CLI.
scripts/migrate-hero-title-to-headline.php
<?php

declare(strict_types=1);

$dryRun = in_array('--dry-run', $argv ?? [], true);
$posts = get_posts([
'post_type' => 'page',
'post_status' => ['publish', 'draft', 'pending'],
'numberposts' => -1,
'fields' => 'ids',
]);

$migrated = 0;
$skipped = 0;
$errors = 0;
$samples = [];

foreach ($posts as $postId) {
$source = get_field('hero_title', $postId);
$target = get_field('hero_headline', $postId);

if (empty($source)) {
$skipped++;
continue;
}

if (!empty($target)) {
$skipped++;
continue;
}

if ($dryRun) {
$migrated++;
if (count($samples) < 5) {
$samples[] = ['post_id' => $postId, 'source' => (string) $source, 'mode' => 'dry-run'];
}
continue;
}

$ok = update_field('hero_headline', $source, $postId);
if ($ok === false) {
$errors++;
continue;
}

$migrated++;
if (count($samples) < 5) {
$samples[] = ['post_id' => $postId, 'source' => (string) $source, 'mode' => 'applied'];
}
}

$report = [
'script' => 'migrate-hero-title-to-headline',
'dry_run' => $dryRun,
'migrated' => $migrated,
'skipped' => $skipped,
'errors' => $errors,
'samples' => $samples,
'completed_at' => gmdate('c'),
];

update_option('acf_migration_report_hero_headline', $report, false);
print_r($report);
terminal: command
wp eval-file scripts/migrate-hero-title-to-headline.php -- --dry-run
wp eval 'print_r(get_option("acf_migration_report_hero_headline"));'
wp eval '$id=1360; echo "source=" . (string)get_field("hero_title",$id) . PHP_EOL; echo "target=" . (string)get_field("hero_headline",$id) . PHP_EOL;'
terminal: output
Array
(
[script] => migrate-hero-title-to-headline
[dry_run] => 1
[migrated] => 587
[skipped] => 23
[errors] => 0
[samples] => Array
(
[0] => Array
(
[post_id] => 1204
[source] => Laser Treatment Plans
[mode] => dry-run
)
)
)
source=Laser Treatment Plans
target=
warning

Never run large migrations first on production. Always dry-run and sample-verify on a staging clone.

Use Case 3 — Edge case: naive migration overwrites corrected content

A prior migration script blindly copies source to target, overwriting pages where editors already refined hero_headline. You compare fragile overwrite behavior with robust idempotent logic.

❌ Fragile Pattern

scripts/migrate-fragile-overwrite.php
<?php

declare(strict_types=1);

$posts = get_posts(['post_type' => 'page', 'numberposts' => -1, 'fields' => 'ids']);
foreach ($posts as $postId) {
$source = get_field('hero_title', $postId);
update_field('hero_headline', $source, $postId);
}

echo "done" . PHP_EOL;

✅ Robust Pattern

scripts/migrate-robust-idempotent.php
<?php

declare(strict_types=1);

$posts = get_posts(['post_type' => 'page', 'numberposts' => -1, 'fields' => 'ids']);
$report = [
'updated' => 0,
'preserved_existing' => 0,
'missing_source' => 0,
'samples' => [],
'completed_at' => gmdate('c'),
];

foreach ($posts as $postId) {
$source = (string) get_field('hero_title', $postId);
$target = (string) get_field('hero_headline', $postId);

if ($source === '') {
$report['missing_source']++;
continue;
}

if ($target !== '') {
$report['preserved_existing']++;
continue;
}

$ok = update_field('hero_headline', $source, $postId);
if ($ok !== false) {
$report['updated']++;
if (count($report['samples']) < 5) {
$report['samples'][] = ['post_id' => $postId, 'value' => $source];
}
}
}

update_option('acf_migration_report_robust', $report, false);
print_r($report);
terminal: command
wp eval-file scripts/migrate-fragile-overwrite.php
wp eval-file scripts/migrate-robust-idempotent.php
wp eval 'print_r(get_option("acf_migration_report_robust"));'
terminal: output
done
Array
(
[updated] => 0
[preserved_existing] => 587
[missing_source] => 23
[samples] => Array
(
)
[completed_at] => 2026-02-23T13:56:02+00:00
)
Array
(
[updated] => 0
[preserved_existing] => 587
[missing_source] => 23
)
warning

Idempotency is not optional in migration scripts. Non-idempotent scripts can silently destroy corrected content on reruns.

Common Mistakes

MistakeRoot CauseWhat Breaks in ProductionCorrect Pattern
Launching without field-heavy performance baselineNo before/after metricsSlow pages discovered post-launchCapture and compare perf report option values
Running migration scripts without dry-run modeNo early visibility into impactLarge-scale data errors under pressureAdd --dry-run path and sample output
Overwriting target fields blindlyNo idempotency ruleCorrected content lost after rerunsUpdate only when target is empty
Skipping sample verification after migrationAssumes aggregate counts are enoughHidden value-format errors surviveValidate representative post IDs with wp eval
Missing rollback ownership and trigger criteriaOperational responsibility unclearDelayed recovery during incidentDocument rollback command + owner + threshold
No post-release log watch for fallback spikesObservability gapContent quality issues persist silentlyMonitor fallback and migration logs for 24h
Deep Dive: Why Migration Scripts Fail Even When They "Succeed"

Migration scripts can print success while still producing bad data. Typical causes are shape mismatch (array vs scalar), context mismatch (option vs post IDs), and non-idempotent writes that overwrite corrected content. Aggregate counts (migrated=500) can hide these issues unless you inspect representative samples. Always include dry-run mode, sample snapshots, and post-run verification commands. Think of migration as a repeatable operation with observability, not a one-time fire-and-forget command.

wp eval '$r=get_option("acf_migration_report_hero_headline"); print_r($r["samples"] ?? []);'

Best Practices

  1. Profile high-traffic field-heavy templates before and after every schema release.
  2. Build migrations with dry-run mode and idempotent apply conditions.
  3. Persist migration/performance reports in options for audit and rollback decisions.
  4. Verify representative sample records, not just aggregate counts.
  5. Block launch when dependency or schema checks fail (acf_get_field_groups baseline mismatch).
  6. Define rollback trigger criteria in advance (error count, performance regression threshold).
  7. Monitor fallback/log anomalies for at least 24 hours post-launch.

Hands-On Practice

Exercise 1: Generate a baseline performance report

Create wp-content/themes/clinic-pro/inc/acf/performance-audit.php and run:

wp eval 'print_r(get_option("acf_perf_audit_report"));'

After completing this exercise, output should include:

[elapsed_ms] =>
[query_count_delta] =>

Exercise 2: Run dry-run migration for hero_title rename

Run:

wp eval-file scripts/migrate-hero-title-to-headline.php -- --dry-run
wp eval 'print_r(get_option("acf_migration_report_hero_headline"));'

After completing this exercise, output should include:

[dry_run] => 1
[migrated] =>
[samples] => Array

Exercise 3: Apply migration in idempotent mode

Run:

wp eval-file scripts/migrate-robust-idempotent.php
wp eval 'print_r(get_option("acf_migration_report_robust"));'

After completing this exercise, output should include:

[updated] =>
[preserved_existing] =>

Exercise 4: Validate sample source/target pairs

Run:

wp eval '$id=1360; echo "source=" . (string)get_field("hero_title",$id) . PHP_EOL; echo "target=" . (string)get_field("hero_headline",$id) . PHP_EOL;'

After completing this exercise, expected output pattern:

source=
target=

Exercise 5: Execute launch gate command bundle

Run:

wp plugin get advanced-custom-fields-pro --fields=name,status,version
wp eval 'echo "groups=" . count(acf_get_field_groups()) . PHP_EOL;'
wp eval 'print_r(get_option("acf_migration_report_robust"));'
wp eval 'print_r(get_option("acf_perf_audit_report"));'

After completing this exercise, output should show active plugin, expected group count, and both report arrays.

CLI Reference

CommandPurposeReal Example Output
wp eval 'print_r(get_option("acf_perf_audit_report"));'Inspect latest performance audit payloadArray ( [elapsed_ms] => 42.17 ... )
wp eval-file scripts/migrate-hero-title-to-headline.php -- --dry-runSimulate migration impact without writesdry_run => 1, migrated => ...
wp eval-file scripts/migrate-robust-idempotent.phpApply idempotent migration writesupdated => X, preserved_existing => Y
wp eval 'print_r(get_option("acf_migration_report_hero_headline"));'Inspect detailed migration reportArray ( [samples] => ... )
`wp eval '$id=1360; echo get_field("hero_title",$id) . "" . get_field("hero_headline",$id) . PHP_EOL;'`Compare source/target values on sample post
wp eval 'echo count(acf_get_field_groups()) . PHP_EOL;'Validate runtime schema baseline16
wp plugin get advanced-custom-fields-pro --field=versionVerify dependency version before launch6.3.1
wp post meta get 1360 hero_headlineRead raw migrated target valueLaser Treatment Plans

Shell Script Pattern

Use this launch-gate script in scripts/acf-release-gate.sh:

scripts/acf-release-gate.sh
#!/usr/bin/env bash
set -euo pipefail

echo "[1/5] Checking ACF Pro activation"
wp plugin get advanced-custom-fields-pro --fields=name,status,version

echo "[2/5] Checking runtime field group baseline"
wp eval 'echo "groups=" . count(acf_get_field_groups()) . PHP_EOL;'

echo "[3/5] Checking latest migration report"
wp eval 'print_r(get_option("acf_migration_report_robust"));'

echo "[4/5] Checking latest performance report"
wp eval 'print_r(get_option("acf_perf_audit_report"));'

echo "[5/5] Sampling critical fields"
wp eval '$id=1360; echo "hero_headline=" . (string)get_field("hero_headline",$id) . PHP_EOL;'

echo "ACF release gate completed"

What's Next

tip

Revisit this lesson before every major release that includes schema changes, field renames, or high-traffic template updates.