Skip to content

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

Description

@jpmckinney

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions