From 486448a2deffd4de83f623e451c6a0a52cc1b82f Mon Sep 17 00:00:00 2001 From: Amaury Balmer Date: Thu, 16 Jul 2026 15:55:11 +0200 Subject: [PATCH] perf: cache assets.json manifest, drop WP <6.0 patterns backport, skip editor style on front - Assets::get_min_file(): memoize the decoded dist/assets.json manifest (was re-read and re-decoded on every call, several times per request) - Editor_Patterns: remove the register_patterns() WP <6.0 backport, core handles ./patterns/ registration since 6.0 - Editor::style(): skip manifest lookup and filesystem check on front-end requests, editor styles are only consumed in admin/editor context - Declare PHP 8.3 / WP 6.3 requirements in style.css and README Co-Authored-By: Claude Opus 4.8 --- README.md | 5 + inc/Services/Assets.php | 44 ++++++-- inc/Services/Editor.php | 7 ++ inc/Services/Editor_Patterns.php | 178 ++----------------------------- style.css | 1 + 5 files changed, 56 insertions(+), 179 deletions(-) diff --git a/README.md b/README.md index f0683fd7..4326e1da 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ ## Requirements +### PHP & WordPress + +- PHP **8.3** or higher +- WordPress **6.3** or higher + ### Composer You need composer to autoload all your classes from the inc folder. diff --git a/inc/Services/Assets.php b/inc/Services/Assets.php index a11ca032..e46b7492 100644 --- a/inc/Services/Assets.php +++ b/inc/Services/Assets.php @@ -132,14 +132,9 @@ public function get_min_file( string $type ): string { return ''; } - if ( ! file_exists( \get_theme_file_path( '/dist/assets.json' ) ) ) { - return ''; - } - - $json = file_get_contents( \get_theme_file_path( '/dist/assets.json' ) ); //phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents - $assets = json_decode( $json, true ); + $assets = $this->get_assets_manifest(); - if ( empty( $assets ) || JSON_ERROR_NONE !== json_last_error() ) { + if ( empty( $assets ) ) { return ''; } @@ -176,6 +171,41 @@ public function get_min_file( string $type ): string { return $file; } + /** + * Read and decode the `dist/assets.json` manifest once per request. + * + * The manifest is requested several times per request (scripts registration, + * `stylesheet_uri` filter, editor assets…): memoize the decoded content to + * avoid repeated filesystem reads and JSON decoding. + * + * @return array Manifest entries, or an empty array if unavailable/invalid. + */ + private function get_assets_manifest(): array { + static $manifest = null; + + if ( null !== $manifest ) { + return $manifest; + } + + $manifest = []; + $manifest_path = \get_theme_file_path( '/dist/assets.json' ); + + if ( ! file_exists( $manifest_path ) ) { + return $manifest; + } + + $json = file_get_contents( $manifest_path ); //phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $assets = json_decode( $json, true ); + + if ( ! is_array( $assets ) || JSON_ERROR_NONE !== json_last_error() ) { + return $manifest; + } + + $manifest = $assets; + + return $manifest; + } + /** * Retrieve data for a compiled asset file. * diff --git a/inc/Services/Editor.php b/inc/Services/Editor.php index c2a7fc2c..6f43f891 100644 --- a/inc/Services/Editor.php +++ b/inc/Services/Editor.php @@ -71,8 +71,15 @@ public function boot( Service_Container $container ): void { /** * editor style + * + * Editor styles are only consumed in admin/editor context: skip the + * manifest lookup and filesystem check on front-end requests. */ private function style(): void { + if ( ! is_admin() ) { + return; + } + $file = $this->assets->get_min_file( 'editor.css' ) ?: 'editor.css'; /** diff --git a/inc/Services/Editor_Patterns.php b/inc/Services/Editor_Patterns.php index 69ab7078..42c82afd 100644 --- a/inc/Services/Editor_Patterns.php +++ b/inc/Services/Editor_Patterns.php @@ -6,6 +6,12 @@ use BEA\Theme\Framework\Service; use BEA\Theme\Framework\Service_Container; +/** + * Register the theme block pattern categories. + * + * The patterns themselves (PHP files in `./patterns/`) are automatically + * registered by WordPress core since 6.0 (`_register_theme_block_patterns()`). + */ class Editor_Patterns implements Service { /** * @param Service_Container $container @@ -25,7 +31,6 @@ public function get_service_name(): string { */ public function boot( Service_Container $container ): void { \add_action( 'init', [ $this, 'register_categories' ], 10 ); - \add_action( 'init', [ $this, 'register_patterns' ], 11 ); } /** @@ -47,175 +52,4 @@ public function register_categories(): void { register_block_pattern_category( $name, $properties ); } } - - /** - * Register any patterns that the active theme may provide under its - * `./patterns/` directory. Each pattern is defined as a PHP file and defines - * its metadata using plugin-style headers. The minimum required definition is: - * - * /** - * * Title: My Pattern - * * Slug: my-theme/my-pattern - * * - * - * The output of the PHP source corresponds to the content of the pattern, e.g.: - * - *

- * - * If applicable, this will collect from both parent and child theme. - * - * Other settable fields include: - * - * - Description - * - Viewport Width - * - Categories (comma-separated values) - * - Keywords (comma-separated values) - * - Block Types (comma-separated values) - * - Inserter (yes/no) - * - * @since 6.0.0 - * @access private - * @internal - * @see https://github.com/WordPress/gutenberg/blob/trunk/lib/compat/wordpress-6.0/block-patterns.php - */ - public function register_patterns(): void { - - /** - * this function is already present in WordPress 6 - */ - if ( version_compare( get_bloginfo( 'version' ), '6.0', '>=' ) ) { - return; - } - - $default_headers = [ - 'title' => 'Title', - 'slug' => 'Slug', - 'description' => 'Description', - 'viewportWidth' => 'Viewport Width', - 'categories' => 'Categories', - 'keywords' => 'Keywords', - 'blockTypes' => 'Block Types', - 'inserter' => 'Inserter', - ]; - - // Register patterns for the active theme. If the theme is a child theme, - // let it override any patterns from the parent theme that shares the same slug. - $themes = []; - $stylesheet = get_stylesheet(); - $template = get_template(); - if ( $stylesheet !== $template ) { - $themes[] = wp_get_theme( $stylesheet ); - } - $themes[] = wp_get_theme( $template ); - - foreach ( $themes as $theme ) { - $dirpath = $theme->get_stylesheet_directory() . '/patterns/'; - if ( ! is_dir( $dirpath ) || ! is_readable( $dirpath ) ) { - continue; - } - $files = glob( $dirpath . '*.php' ); - if ( is_array( $files ) && count( $files ) > 0 ) { - foreach ( $files as $file ) { - $pattern_data = get_file_data( $file, $default_headers ); - if ( empty( $pattern_data['slug'] ) ) { - _doing_it_wrong( - '_register_theme_block_patterns', - sprintf( - /* translators: %s: file name. */ - esc_html__( 'Could not register file "%s" as a block pattern ("Slug" field missing)', 'beapi-frontend-framework' ), - esc_html( $file ) - ), - '6.0.0' - ); - continue; - } - - if ( ! preg_match( '/^[A-z0-9\/_-]+$/', $pattern_data['slug'] ) ) { - _doing_it_wrong( - '_register_theme_block_patterns', - sprintf( - /* translators: %1s: file name; %2s: slug value found. */ - esc_html__( 'Could not register file "%1$s" as a block pattern (invalid slug "%2$s")', 'beapi-frontend-framework' ), - esc_html( $file ), - esc_html( $pattern_data['slug'] ) - ), - '6.0.0' - ); - } - if ( \WP_Block_Patterns_Registry::get_instance()->is_registered( $pattern_data['slug'] ) ) { - continue; - } - - // Title is a required property. - if ( ! $pattern_data['title'] ) { - _doing_it_wrong( - '_register_theme_block_patterns', - sprintf( - /* translators: %1s: file name; %2s: slug value found. */ - esc_html__( 'Could not register file "%s" as a block pattern ("Title" field missing)', 'beapi-frontend-framework' ), - esc_html( $file ) - ), - '6.0.0' - ); - continue; - } - - // For properties of type array, parse data as comma-separated. - foreach ( [ 'categories', 'keywords', 'blockTypes' ] as $property ) { - if ( ! empty( $pattern_data[ $property ] ) ) { - $pattern_data[ $property ] = array_filter( - preg_split( - '/[\s,]+/', - (string) $pattern_data[ $property ] - ) - ); - } else { - unset( $pattern_data[ $property ] ); - } - } - - // Parse properties of type int. - foreach ( [ 'viewportWidth' ] as $property ) { - if ( ! empty( $pattern_data[ $property ] ) ) { - $pattern_data[ $property ] = (int) $pattern_data[ $property ]; - } else { - unset( $pattern_data[ $property ] ); - } - } - - // Parse properties of type bool. - foreach ( [ 'inserter' ] as $property ) { - if ( ! empty( $pattern_data[ $property ] ) ) { - $pattern_data[ $property ] = in_array( - strtolower( $pattern_data[ $property ] ), - [ 'yes', 'true' ], - true - ); - } else { - unset( $pattern_data[ $property ] ); - } - } - - // Translate the pattern metadata. - $text_domain = $theme->get( 'TextDomain' ); - //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.NonSingularStringLiteralContext, WordPress.WP.I18n.NonSingularStringLiteralDomain, WordPress.WP.I18n.LowLevelTranslationFunction - $pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', $text_domain ); - if ( ! empty( $pattern_data['description'] ) ) { - //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.NonSingularStringLiteralContext, WordPress.WP.I18n.NonSingularStringLiteralDomain, WordPress.WP.I18n.LowLevelTranslationFunction - $pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', $text_domain ); - } - - // The actual pattern content is the output of the file. - ob_start(); - include $file; - $pattern_data['content'] = ob_get_clean(); - if ( empty( $pattern_data['content'] ) ) { - continue; - } - - register_block_pattern( $pattern_data['slug'], $pattern_data ); - } - } - } - } } diff --git a/style.css b/style.css index 7b357f22..00a07e9e 100644 --- a/style.css +++ b/style.css @@ -7,4 +7,5 @@ Author: BeAPI Author URI: http://www.beapi.fr Text Domain: beapi-frontend-framework Requires at least: 6.3 +Requires PHP: 8.3 */