# bw-ai-schema-pro: performance + code-quality cleanup

Audit done against version 2.1.4 in `/srv/apps/bw-plugins/wp-content/plugins/bw-ai-schema-pro/`.

## The good news

Most of what looked alarming in surface-level analysis is actually fine:

- 16 `wp_remote_*` calls → 15 are in the vendored `plugin-update-checker` library (only runs during update checks), only 1 in plugin code itself
- Admin classes are properly gated behind `is_admin()` (don't load on frontend)
- Author box has an early-exit on `!is_singular()`
- `BW_Schema_Core::disable_conflicting_schema()` uses the proper documented filter hooks for Yoast/AIOSEO/SEOPress/RankMath — that's the right way to deconflict

## Issue 1 — CRITICAL: remove `remove_remaining_schemas`

**File**: `includes/class-bw-schema-team-member.php` around line 1255  
**Hook**: `wp_head` priority 0 (fires on **every frontend page**)

```php
public static function remove_remaining_schemas() {
    global $wp_filter;
    if ( isset( $wp_filter['wp_head'] ) ) {
        foreach ( $wp_filter['wp_head'] as $priority => $hooks ) {
            foreach ( $hooks as $hook_key => $hook ) {
                // ... fragile string matching on 'schema', 'structured_data', 'json_ld'
                remove_action( 'wp_head', $hook_key, $priority );
            }
        }
    }
}
```

**Why this is bad**:
- Iterates over **every wp_head hook on every page load** — small but constant cost
- Fragile string matching on hook keys means it could remove unrelated legitimate hooks (anything with "schema" in the name from another plugin)
- It's **redundant with `disable_conflicting_schema`** in `class-bw-schema-core.php`, which already disables Yoast/AIOSEO/SEOPress/RankMath schema using their documented filters — the right way

**Action**: Delete the `remove_remaining_schemas` method **and** its `add_action('wp_head', ...)` registration. The `disable_conflicting_schema` function (already in place) handles all known cases correctly. If a new SEO plugin shows up that generates schema we want to suppress, add it to `disable_conflicting_schema` explicitly with the plugin's documented filter.

## Issue 2 — HIGH: schema cache doesn't survive without persistent object cache

**File**: `includes/class-bw-schema-cache.php`

```php
public static function get( $key ) {
    if ( ! self::is_enabled() ) return false;
    return wp_cache_get( $key, self::CACHE_GROUP );
}

public static function set( $key, $data ) {
    if ( ! self::is_enabled() ) return false;
    return wp_cache_set( $key, $data, self::CACHE_GROUP, self::CACHE_EXPIRATION );
}
```

**Why this is bad**: `wp_cache_*` is in-memory and **only persists for the duration of one HTTP request** unless a persistent object cache (Redis/Memcached) is installed. Many shared/managed hosts (including Flywheel's lower plans) don't have one. On those sites the schema is regenerated on every page load — the cache is a no-op.

**Action**: add a transient fallback when no persistent cache is available:

```php
public static function get( $key ) {
    if ( ! self::is_enabled() ) return false;
    if ( wp_using_ext_object_cache() ) {
        return wp_cache_get( $key, self::CACHE_GROUP );
    }
    return get_transient( 'bw_schema_' . $key );
}

public static function set( $key, $data ) {
    if ( ! self::is_enabled() ) return false;
    if ( wp_using_ext_object_cache() ) {
        return wp_cache_set( $key, $data, self::CACHE_GROUP, self::CACHE_EXPIRATION );
    }
    return set_transient( 'bw_schema_' . $key, $data, self::CACHE_EXPIRATION );
}

public static function delete( $key ) {
    if ( wp_using_ext_object_cache() ) {
        return wp_cache_delete( $key, self::CACHE_GROUP );
    }
    return delete_transient( 'bw_schema_' . $key );
}
```

Same pattern for `clear_post_cache` and `clear_all` — also delete the corresponding transients. For `clear_all`, since transient names use a prefix, a one-shot `$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_bw_schema_%' OR option_name LIKE '_transient_timeout_bw_schema_%'")` works.

## Issue 3 — LOW: consolidate 10 separate `init` hook registrations

**File**: `bw-ai-schema-pro.php` lines 112-136 (and one each in `class-bw-schema-blocks.php`, vendor)

Currently each subsystem registers its own `init` hook:

```php
add_action( 'init', array( $this, 'load_textdomain' ) );
add_action( 'init', array( 'BW_Schema_Core', 'maybe_migrate_options' ), 1 );
add_action( 'init', array( 'BW_Schema_Core', 'disable_conflicting_schema' ), 5 );
add_action( 'init', array( 'BW_Schema_Cache', 'init' ) );
add_action( 'init', array( 'BW_Schema_Hooks', 'init' ) );
add_action( 'init', array( 'BW_Schema_Security', 'init' ) );
add_action( 'init', array( 'BW_Schema_Author_Override', 'init' ) );
add_action( 'init', array( 'BW_Schema_Team_Member', 'init' ) );
// + 1 from class-bw-schema-blocks.php, 1 from vendor (untouchable)
```

**Action (optional)**: minor cleanup — consolidate the plugin's own 8 into one wrapper. Most of these need to keep specific priorities (1 and 5 above), so it's not strictly "1 hook = 1 priority", but priority 10 ones can collapse:

```php
add_action( 'init', array( $this, 'init_subsystems' ) );

public function init_subsystems() {
    $this->load_textdomain();
    BW_Schema_Cache::init();
    BW_Schema_Hooks::init();
    BW_Schema_Security::init();
    BW_Schema_Author_Override::init();
    BW_Schema_Team_Member::init();
}

// Leave these separate since they need specific priorities:
add_action( 'init', array( 'BW_Schema_Core', 'maybe_migrate_options' ), 1 );
add_action( 'init', array( 'BW_Schema_Core', 'disable_conflicting_schema' ), 5 );
```

Marginal perf win (a few microseconds per request), but cleaner code.

## Issue 4 — LOW: file size on the renderer hot path

`output_schema_markup` instantiates `new BW_Schema_Renderer()` on every wp_head call. The renderer is 27 KB and its `render()` does the cache check. Two micro-optimizations possible:

1. **Singleton the renderer** so we don't `new` it on every page
2. **Early-exit before instantiation** for post types where schema is disabled (currently the renderer instantiates first, then checks)

Both are very minor — fixing Issues 1 and 2 above will deliver almost all the win.

## Summary of priorities

| # | Severity | Effort | Win |
|---|---|---|---|
| 1 | **Critical** — risky | 5 min | Remove `remove_remaining_schemas` entirely |
| 2 | **High** | 30 min | Add transient fallback to BW_Schema_Cache |
| 3 | Low | 15 min | Consolidate init hooks |
| 4 | Low | 30 min | Renderer micro-optimization |

After 1 and 2, expect noticeably lower TTFB on sites without persistent object cache (most clients). Version bump to 2.1.5 or 2.2.0 depending on whether you treat the cache fix as a breaking change to cache behavior.
