# Performance

Divine's runtime performance behavior is owned by the theme, in a single file: `divine.json` at the theme root. The file declares a `performance` object that the runtime validates and resolves, and the same configuration drives plugin-managed themes and exported standalone themes alike. Defaults are conservative on purpose, so nothing is optimized away until the theme opts in, and because the file lives in the theme, performance changes travel through worktrees, reviews, deploys, and exports like any other theme file. This page is the reference for the performance object, its profiles and sections, and the order in which Divine merges everything into the configuration that actually runs.

## divine.json

`divine.json` is the theme-level source of truth for runtime performance. The runtime reads the `performance` key from it, validates the result strictly, and ignores an invalid configuration with errors recorded rather than applying it partially. Unknown keys, wrong value types, and out-of-range values are all rejected. Start from a profile and override individual sections from there.

```json
{
  "performance": {
    "profile": "speed",
    "media": { "rootMargin": "600px" },
    "staticPrerender": { "enabled": true, "ttl": 86400 }
  }
}
```

Because the configuration is theme-owned, the same file is exported with the standalone theme and continues to govern performance on a site that has no Divine plugin installed.

## Profiles

The `profile` key selects a baseline that individual sections then override. There are three profiles. `compat` is the default and leaves WordPress untouched, which makes it safe to adopt Divine without changing front-end behavior. `speed` turns on the common optimizations, and `strict` extends `speed` with the structured data policy.

| Profile | Behavior |
| --- | --- |
| `compat` | The default. Leaves WordPress behavior untouched; every optimization stays off. |
| `speed` | Enables the WordPress cleanup set, Blockstudio discovery and render caches, intent link preloading, and lazy media. |
| `strict` | Everything in `speed`, plus the structured data policy with postmeta queries disabled. |

Two cross-field rules are enforced during validation, so the resolver can reject a contradictory theme configuration rather than apply half of it. The `strict` profile requires `data.policy` to be `structured`, and a structured policy cannot re-enable `data.postmetaQueries`.

## Configuration sections

Each section is an object with explicitly typed keys; booleans are real booleans, and enumerated values are validated against their allowed set. The defaults below are the `compat` baseline that ships when a key is absent.

| Section | Keys | Defaults | What it controls |
| --- | --- | --- | --- |
| `wordpress` | `headNoise`, `embeds`, `xmlrpc`, `editor`, `frontendAssets`, `media`, `heartbeat` | all `false` | WordPress cleanup toggles: removing default head output, embed machinery, XML-RPC exposure, editor and frontend asset overhead, media extras, and heartbeat throttling. |
| `blockstudio` | `uiApps`, `discoveryCache`, `renderCache` | `uiApps` true, caches `false` | Whether the bundled Blockstudio loads its UI apps and whether block discovery and render caches are enabled. |
| `data` | `policy`, `allowedSources`, `postmetaQueries` | `compat`, `["pages","cpts"]`, `true` | The theme data policy: `compat` or `structured`, the allowed WordPress sources (`pages`, `cpts`), and whether postmeta-driven queries are permitted. |
| `preload` | `links` | `off` | Link preloading: `off` or `intent`, which warms same-origin links on hover, focus, or touch via a small deferred script that respects data-saver connections. |
| `media` | `lazy`, `skeleton`, `metadata`, `rootMargin` | `false`, `false`, `false`, `300px` | The frontend media loader: lazy loading, skeleton placeholders, metadata-driven sizing, and the IntersectionObserver root margin as a pixel or percentage length. |
| `measurement` | `enabled`, `queryMonitor`, `headers`, `timings` | all `false` | Request measurement: master switch, slow-query capture, response headers, and per-request timing output. |
| `staticPrerender` | `enabled`, `ttl`, `invalidate` | `false`, `86400`, `signature` | The anonymous HTML response cache: master switch, lifetime in seconds, and the invalidation mode, currently `signature`. |

### WordPress cleanup

The `wordpress` section is a set of independent toggles over the noisiest parts of a default WordPress page. Each one defaults to `false` so `compat` ships an unmodified site; the `speed` and `strict` profiles flip the whole set to `true`. Turn an individual toggle back off in the theme when one piece of WordPress output is still needed.

### Blockstudio

Divine bundles a scoped Blockstudio build, and the `blockstudio` section controls how it runs at request time. `uiApps` defaults to `true` for the authoring experience and is turned off by the optimizing profiles, while `discoveryCache` and `renderCache` default to `false` and are enabled by `speed` and `strict` so block discovery and render output are cached between requests.

### Data policy

The `data` section selects how Divine resolves theme content. The `compat` policy preserves normal WordPress query behavior, and the `structured` policy restricts content to declared sources. `allowedSources` lists which WordPress sources participate, and `postmetaQueries` decides whether postmeta-driven queries run. The structured policy is what the `strict` profile requires, and it is the one configuration the validator will not let you partially defeat: a structured policy with `postmetaQueries` set back to `true` is rejected.

### Preload

The `preload` section has a single key, `links`. With `intent`, Divine enqueues a small deferred script that warms same-origin links when the user signals intent through hover, focus, or touch, and it backs off on data-saver connections. The `off` default enqueues nothing.

## How the effective config is resolved

Several layers merge into one effective configuration, each able to refine the previous one. Divine resolves them in a fixed order so the source of any value is predictable, and validation guards every step: a layer that fails validation is dropped back to a safe value and its errors are recorded, rather than corrupting the merge.

| Step | Source | Role |
| --- | --- | --- |
| 1 | Built-in defaults | The `compat` baseline every install starts from. |
| 2 | `divine/performance/default_config` filter | Lets platform code restate the defaults before anything merges. |
| 3 | `DIVINE_PERFORMANCE_PROFILE` constant | Pins a profile above the defaults when defined. |
| 4 | Theme profile | The profile named in `divine.json` expands into its baseline. |
| 5 | Theme config | The theme's explicit `performance` values, read through the `raw_theme_config` and `theme_config` filters with validation between them, override the profile baseline. |
| 6 | Policy overlay | A host policy file named by `DIVINE_PERFORMANCE_POLICY` merges after the theme, including its own profile when it names one, so platform operators can enforce site-wide decisions. |
| 7 | `divine/performance/effective_config` filter | Filters the merged result before the final validation and per-theme cache. |

The practical reading order is simple: defaults, then the profile, then the theme's own values, then host policy, then filters, with validation guarding every step. All five performance filters are listed with their exact arguments in the generated [hook reference](hooks). The result is validated once more and cached per theme root, so the merge runs once per theme rather than once per request.

The performance runtime is also lazy in a second sense: each capability only attaches its hooks when its section is enabled in the resolved configuration. A `compat` theme registers nothing on render, so the cost of Divine's performance layer on an unconfigured site is effectively zero, and an exported standalone theme behaves the same way.

## Static prerendering

When `staticPrerender.enabled` is true, Divine captures fully rendered HTML responses for anonymous GET requests and serves them from disk on subsequent hits, marked with an `X-Divine-Static-Prerender: HIT` header. The cache is deliberately narrow: requests with query strings, logged-in users, admin, REST, AJAX, cron, login, feeds, and search are always bypassed, and only complete HTML documents are stored.

Cached files live under `wp-content/uploads/divine-static-prerender/` unless the `DIVINE_STATIC_PRERENDER_CACHE_DIR` constant points elsewhere. Entries expire after `ttl` seconds, and the cache key embeds a signature over the effective performance config and the theme's key files, `divine.json`, `style.css`, `functions.php`, `blockstudio.json`, and the `pages/`, `blocks/`, `templates/`, and `assets/` directories, so changing the theme invalidates stale HTML structurally. The cache is also purged outright when posts are saved or deleted, the theme switches, or a worktree deploys or is destroyed.

## Media loading

With `media.lazy` enabled, Divine enqueues its media script and stylesheet, which lazy-load images with an IntersectionObserver using the configured `rootMargin`, optionally render skeleton placeholders, and use stored metadata for stable sizing. Themes render compatible markup with the `divine_media_image()` template helper, which accepts the image arguments and produces the markup the loader expects, falling back to eager markup when lazy loading is off.

Stable sizing comes from `<theme-root>/assets/media.json`. Regenerate it after adding or replacing theme images with the runtime builder:

```php
( new \Divine\Runtime\Media\MediaMetadataBuilder() )->write( get_stylesheet_directory(), true );
```

The builder records raster dimensions for theme assets under `assets/` and, when requested, attachment metadata from the WordPress media library. SVG files are ignored for raster sizing instead of treated as errors. `MediaMetadata::reset()` clears the in-request reader cache after a rebuild, and `write()` calls it automatically.

## Measurement

Measurement is for local profiling and stays off unless `measurement.enabled` is true. When enabled, Divine records a per-request snapshot, profile, config hash, request URI, elapsed time, peak memory, and query count, in a short-lived transient for inspection, and `measurement.queryMonitor` adds the slow queries observed during the request to that snapshot. With `measurement.headers` enabled, responses carry `X-Divine-Performance-Profile` and `X-Divine-Performance-Config` headers, plus `X-Divine-Performance-Time` when `timings` is also on. The `divine/performance/measurement_enabled` action fires once measurement is active, so profiling integrations can attach to exactly the requests being measured.
</content>
</invoke>
