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:381publicfunctionscan_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
Describe the bug
ACF_Local_JSON::scan_files()performs a fullscandir()plusfile_get_contents()andjson_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:includes/local-json.php:310acf-field-groupincludes/local-json.php:331acf-post-typeincludes/local-json.php:352acf-taxonomypro/post-types/acf-ui-options-page.php:307acf-ui-options-pageEach 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_typesandacf/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:
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) putlocal-json.php:409andlocal-json.php:312among the most frequently caught frames, which is what prompted this investigation.Expected behavior
Memoise the directory scan.
scan_files()already assigns$this->filesbefore returning, so the results are being kept; they are simply rebuilt on every call:Invalidation within a request should not be needed:
update_field_group()and friends write a file and can reset$this->filesexplicitly 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_), soglob()on the prefix would identify candidates without decoding anything.Version Information: