Performance, Debugging, Migration, and Launch Checklist
ACF release safety depends on repeatable operational controls, not heroic launch-day troubleshooting.
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.
Treat ACF launches like data operations: profile first, migrate with safeguards, verify with CLI, and keep rollback paths explicit.
Why It Matters
| Approach | What Happens | Impact in Production |
|---|---|---|
| Profile and optimize field-heavy templates before launch | Bottlenecks are reduced before traffic spike | Better page speed and conversion stability |
| Run migration scripts with dry-run + sample verification | Data transfer issues are caught before full apply | Lower risk of silent content corruption |
| Use explicit launch gates and rollback plan | Teams know go/no-go state objectively | Faster incident response and less downtime |
| Track migration results and error counts in options/logs | Post-release validation is evidence-based | Better accountability and auditability |
| Deploy schema changes without operational checks (wrong pattern) | Regression discovered under live traffic | Emergency rollbacks, data recovery pressure, reputational risk |
Reference Table
| Term/API | Signature/Syntax | Purpose | Key Notes |
|---|---|---|---|
acf_get_field_groups() | acf_get_field_groups(array $filter = []): array | Validate runtime schema presence/count | Baseline smoke check before migrations |
get_field() | `get_field(string $selector, int | string $post_id = false, bool $format_value = true): mixed` | Read source values in migration scripts |
update_field() | `update_field(string $selector, mixed $value, int | string $post_id = false): int | bool` |
have_rows() | `have_rows(string $selector, int | string $post_id = false): bool` | Traverse structural fields in diagnostics |
wp eval-file | wp eval-file <path> | Execute migration or audit scripts in WP context | Prefer committed scripts over ad-hoc eval blocks |
SAVEQUERIES | define('SAVEQUERIES', true) | Capture query traces for profiling | Use only in staging/debug mode |
| Migration report option | update_option('acf_migration_report', [...], false) | Persist migration results for audit | Include counts, errors, timestamps |
| Launch gate command | wp eval '...' bundle | Validate dependencies, schema, and sample data | Gate 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.
- Enable staging diagnostics for query tracing.
- Add loop-level timers around field-heavy sections.
- Reduce repeated
get_field()calls by caching values locally. - Measure before/after query counts and execution time.
- Store performance report for release notes.
<?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));
});
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;'
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
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.
- Build migration script with dry-run mode.
- Iterate target posts and inspect source/target values.
- Apply only when source exists and target is empty.
- Record migrated/skipped/error counts.
- Persist report and verify sample rows via CLI.
<?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);
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;'
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=
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
<?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
<?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);
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"));'
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
)
Idempotency is not optional in migration scripts. Non-idempotent scripts can silently destroy corrected content on reruns.
Common Mistakes
| Mistake | Root Cause | What Breaks in Production | Correct Pattern |
|---|---|---|---|
| Launching without field-heavy performance baseline | No before/after metrics | Slow pages discovered post-launch | Capture and compare perf report option values |
| Running migration scripts without dry-run mode | No early visibility into impact | Large-scale data errors under pressure | Add --dry-run path and sample output |
| Overwriting target fields blindly | No idempotency rule | Corrected content lost after reruns | Update only when target is empty |
| Skipping sample verification after migration | Assumes aggregate counts are enough | Hidden value-format errors survive | Validate representative post IDs with wp eval |
| Missing rollback ownership and trigger criteria | Operational responsibility unclear | Delayed recovery during incident | Document rollback command + owner + threshold |
| No post-release log watch for fallback spikes | Observability gap | Content quality issues persist silently | Monitor 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
- Profile high-traffic field-heavy templates before and after every schema release.
- Build migrations with dry-run mode and idempotent apply conditions.
- Persist migration/performance reports in options for audit and rollback decisions.
- Verify representative sample records, not just aggregate counts.
- Block launch when dependency or schema checks fail (
acf_get_field_groupsbaseline mismatch). - Define rollback trigger criteria in advance (error count, performance regression threshold).
- 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
| Command | Purpose | Real Example Output |
|---|---|---|
wp eval 'print_r(get_option("acf_perf_audit_report"));' | Inspect latest performance audit payload | Array ( [elapsed_ms] => 42.17 ... ) |
wp eval-file scripts/migrate-hero-title-to-headline.php -- --dry-run | Simulate migration impact without writes | dry_run => 1, migrated => ... |
wp eval-file scripts/migrate-robust-idempotent.php | Apply idempotent migration writes | updated => X, preserved_existing => Y |
wp eval 'print_r(get_option("acf_migration_report_hero_headline"));' | Inspect detailed migration report | Array ( [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 baseline | 16 |
wp plugin get advanced-custom-fields-pro --field=version | Verify dependency version before launch | 6.3.1 |
wp post meta get 1360 hero_headline | Read raw migrated target value | Laser Treatment Plans |
Shell Script Pattern
Use this launch-gate script in 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
- Continue to Understanding ACF Hook Lifecycle and Priorities.
- Return to Module 5 Overview for the full delivery workflow.
- Related lesson: Reading Field Values in Templates.
- Related lesson: Migration, Versioning, and Rollback Playbooks.
Revisit this lesson before every major release that includes schema changes, field renames, or high-traffic template updates.