AdvancedCustomFields / AdvancedCustomFields/acf
`ACF_Local_JSON::scan_files()` re-reads and re-decodes the whole `acf-json` directory four times per request
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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