K KodePressdocumentation
Laravel 11 · MySQL 8 Specification Built 2026-10-10

13 — Caching and performance#

The performance model is deliberately crude, because crude survives shared hosting: published pages are rendered to HTML files and served from disk. No Redis, no reverse proxy, no edge, no assumptions about the host beyond a writable storage/.

13.1 Cache layers#

Layer Driver Holds Lifetime
Page HTML cache files under storage/app/kodepress-cache/ fully rendered public pages until invalidated
Application cache Laravel file driver block manifest, resolved menus, part resolution, usage indexes, search projections, sitemap dirty flag until invalidated, or a short TTL
Theme CSS a hashed file on the public disk compiled tokens until tokens change
Browser / CDN HTTP headers assets (immutable, 1 year), HTML (no-cache, revalidated) per header
OPcache PHP compiled PHP per deploy (cleared by the deploy script)

HTML is sent with Cache-Control: no-cache so a visitor always revalidates and a published change is visible immediately, plus an ETag so the response is usually a 304. Serving HTML from our own file cache is fast; letting a browser hold a stale page for minutes is not worth the risk of an owner saying "I published it and nothing changed".

13.2 Page cache keys and files#

storage/app/kodepress-cache/
└── site-1/
    ├── bn/
    │   ├── _index.html                 # the homepage
    │   ├── about.html
    │   └── blog/
    │       ├── _index.html
    │       ├── _index--page-2.html
    │       └── my-first-post.html
    └── en/
        └── about.html

The key is site_id / locale / path [/ page-N] [/ year]:

Component Why it is in the key
site_id multi-site isolation (Phase 5), harmless before that
locale content and chrome differ per language
path the page itself
page-N paginated archives are separate entries
year only for pages whose footer uses {{year}}; see 08

Device is not in the key: responsive CSS handles layout, and device-based menu visibility is done with CSS classes precisely so the cache stays single-variant. Query strings are not in the key: a page with a query string that affects output (search, pagination beyond page) is not cached at all.

Each entry is two files: about.html and about.meta.json (rendered-at timestamp, content hash, locale, the page id, and the dependency list from the usage index). The metadata is what makes targeted invalidation and cache reporting possible.

13.3 Invalidation rules#

Change Clears
Publish / unpublish / schedule-fires on a page that page, every locale and pagination variant
Page path change the old path and the new path (the old becomes a 301)
Page deleted or trashed that page
Global block published every page whose usage index lists it
Form edited every page whose usage index lists it
Media replaced every page whose usage index lists it
Category or tag renamed every archive page of that taxonomy, plus pages whose post_list filters on it
New post published the blog index, the author archive, and the archives of its categories and tags (the post_list blocks on them change)
Menu, menu item or mega panel changed all pages
Template part (header/footer/topbar/announcement) published all pages
Theme tokens or custom CSS saved all pages
Site options changed (logo, social, analytics) all pages
Locale added or removed all pages
Deploy all pages, application cache, OPcache

CacheInvalidator is the only place that maps a model event to a clear. Models fire events; the invalidator decides. Nothing else calls HtmlCache::forget*.

Why "all pages" is acceptable. Header, footer, menu and theme changes are rare (a handful per month on a live site) and clearing is cheap — it is a directory delete. The cost is re-rendering on the next visit, roughly 150–400 ms per page on the target host, once per page. The alternative (partial templating with fragment caching) adds a class of "stale header on one page" bugs that a non-technical owner cannot diagnose. See ADR-0007.

13.4 What is never cached#

A page skipped for one of these reasons records the reason in a debug header (X-KodePress-Cache: bypass; reason=auth) when APP_DEBUG is on, so "why is this page slow?" has an answer.

13.5 Warming#

13.6 Public page performance budget#

Measured on the target class of host (shared cPanel, no opcode preloading, spinning or modest SSD):

Metric Budget Notes
TTFB, cache hit < 150 ms server time one stat, one file read, headers
TTFB, cache miss < 600 ms page + part + menu queries, render, write
Total page weight < 500 KB for a typical page WebP images dominate
CSS < 40 KB uncompressed, 1 file theme CSS only
JS < 30 KB uncompressed, 1 file Alpine + the header/menu/accordion behaviours
Requests < 20 fonts (2), css (1), js (1), images
Queries, cache hit 0 nothing touches MySQL
Queries, cache miss < 15 asserted by a test
Largest Contentful Paint < 2.5 s on 4G hero image eager with fetchpriority
Cumulative Layout Shift < 0.05 width/height on every image, no late-injected banners

A test asserts the query count on a cache miss and the asset counts; budgets that are not tested are decoration.

13.7 Front-end delivery#

13.8 Admin performance#

Risk Mitigation
N+1 in page lists eager load author, category, version; asserted with a query-count test
Large media library paginated grid (60 per page), thumbnails only, lazy images
Menu with hundreds of items one query, tree built in PHP, preview rendered on demand
Big trees in the editor tree held once in the Livewire component; Alpine handles per-keystroke state so no round trip per character
Autosave storms debounced and throttled (05)
Audit log growth indexed by subject and user, paginated, pruned nightly
Submissions growth indexed by form + date, paginated, optional retention

13.9 Observability on a host with no tooling#

13.10 Tests for this document#

Test Asserts
cached_page_served_without_queries zero queries on the second request
publish_clears_only_that_page a second cached page survives
part_publish_clears_everything both pages gone
theme_save_clears_everything both pages gone
new_post_clears_blog_index_and_archives index, category and tag archives gone, unrelated page kept
authenticated_request_bypasses_cache and does not poison the cache for guests
pagination_cached_separately ?page=2 has its own entry
locale_cached_separately /about and /en/about
query_string_not_cached ?utm=x is served fresh and writes no entry
cache_miss_query_budget under 15 queries
warm_command_is_chunked at most 10 pages per run, cursor advances
cache_directory_is_not_web_readable a direct HTTP fetch of a cache file 403s or 404s

KodePress documentation · generated from the Markdown sources by tools/build-docs-site.py · internal preview, not indexed.