AdvancedCustomFields / AdvancedCustomFields/acf

`ACF_Local_JSON::scan_files()` re-reads and re-decodes the whole `acf-json` directory four times per request

Open Beginner friendly
#1,028 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
945
Forks
197
PR merge metrics
No merged PRs in 30d

Description

Describe the bug

ACF_Local_JSON::scan_files() performs a full scandir() plus file_get_contents() and json_decode() of every file in every load path, and caches nothing between calls. It is called four times on every request, once per internal post type:

Caller Post type
includes/local-json.php:310 acf-field-group
includes/local-json.php:331 acf-post-type
includes/local-json.php:352 acf-taxonomy
pro/post-types/acf-ui-options-page.php:307 acf-ui-options-page

Each call reads and decodes the entire directory before get_files() filters the result down to the requested post type. On a site that stores only field groups, 3 of the 4 passes read every file and match nothing.

This runs on acf/include_fields, acf/include_post_types and acf/include_taxonomies, so it applies to front-end requests as well as admin.

To Reproduce

Make a request.

On my site with 69 field-group JSON files, 0.38 MB total, on PHP 8.1 with OPcache warm:

files: 69, 0.38 MB per pass
4 passes (current behaviour): 18.9 – 33.8 ms
1 pass  (sufficient):          3.3 –  7.1 ms
wasted per request:           15.6 – 26.7 ms

That's 276 file_get_contents() + json_decode() calls per request, of which 207 are discarded.

My sampled PHP stack traces (PHP-FPM slowlog, requests over 1 s) put local-json.php:409 and local-json.php:312 among the most frequently caught frames, which is what prompted this investigation.

Expected behavior

Memoise the directory scan.

scan_files() already assigns $this->files before returning, so the results are being kept; they are simply rebuilt on every call:

// includes/local-json.php:381
public function scan_files( $post_type = 'acf-field-group' ) {
    if ( is_array( $this->files ) ) {
        return $this->get_files( $post_type );
    }
    // ... existing scan ...
}

Invalidation within a request should not be needed: update_field_group() and friends write a file and can reset $this->files explicitly if required.

An alternative that avoids reading unrelated files at all: JSON files are named {key}.json, and each internal post type has a distinct $post_key_prefix (group_, post_type_, taxonomy_, ui_options_page_), so glob() on the prefix would identify candidates without decoding anything.

Version Information:

  • WordPress Version 7.1
  • PHP Version 8.1
  • ACF Version PRO 6.8.8
  • Browser Safari 26
  • MySQL 8.0

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in includes/local-json.php at ACF_Local_JSON::scan_files() and review the four callers listed at lines 310, 331, 352, and pro/post-types/acf-ui-options-page.php:307. Confirm that repeated calls reuse the stored scan results while get_files() still filters by post type, then verify that one request no longer re-reads and re-decodes the directory for each caller.

Written by the indexing model from the issue text.

Assessment

Tech stack
php, wordpress
Domain
backend, performance
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.