Skip to content

Benchmarks ​

In short ​

  • Without a page cache, a kitloom site with what a bare WordPress has is faster than WordPress: 10 ms against 13 ms for a post, and 15 ms against 20 ms across thirteen typical pages. It serves 1.2 times as many pages a second.
  • The whole kitloom skeleton, every module and the Filament admin, against WordPress with three common plugins (Yoast SEO, Contact Form 7, WooCommerce): kitloom takes 25 ms where WordPress takes 43 ms, and serves 1.6 times as many pages a second.
  • With the page cache on, a stored page comes back in 0.2 ms when it is sent before Laravel starts, as fast as WordPress with a page-cache plugin. Sent through Laravel, it takes 1.4–7 ms, depending on how many packages the site runs.
  • Redis does not speed up kitloom's pages. It does speed up WordPress's heavy queries, such as search.
  • In the browser, a bundled theme scores 98–100 for performance in Google Lighthouse on a phone and 100 for accessibility, best practices and SEO on every page measured; on a desktop, 99–100 for performance and 100 for the rest. See In the browser: Lighthouse.

The numbers come from one laptop, so read the ratios rather than the milliseconds. How they were measured is at the end of the page.

Two different caches ​

"Laravel caching" can mean two unrelated things. They are worth keeping apart, because only the second one changes how fast a page comes back.

Config and routes: php artisan optimize ​

On every request Laravel reads dozens of config files and registers every route of the site. php artisan optimize does that work once and writes the result into a few PHP files, so the next requests just load them.

It does not remember any page. Every visitor's page is still built from the database again: the posts are read, the template is drawn, the HTML is put together.

It was switched on in every kitloom column on this page, as it should be on a live site: run it on every deploy.

Pages: Settings → Cache ​

This cache keeps the finished HTML of a page. The first visitor's page is built and stored. The next visitors get the stored copy, until something is saved in the admin (a post, a term, an option, a menu) or the time set on Settings → Cache runs out.

It is for visitors who are not signed in, and only for pages that answered normally (200). A "not found" page is built every time.

A stored page can be sent in two ways:

Through LaravelBefore Laravel starts
Switched on bySettings → CacheSettings → Cache and SITE_CACHE_STATIC=true
What happensLaravel starts, finds the stored page and sends itpublic/index.php finds the page in a file and sends it before Composer and Laravel load
What it still costsStarting the framework and the site's packages: about 1.5 ms with a few of them, 7 ms with the whole skeletonReading one file
Where the pages liveAny cache store: files, Redis, the databaseFiles on the server's disk
FitsAny site, also several servers sharing RedisOne server, or several sharing one folder

The skeletons' public/index.php already has the few lines the second way needs. See Configuration.

Results ​

Four sites were measured, two of each:

  • WordPress — WordPress with its default theme and no plugins.
  • WordPress + plugins — the same site with Yoast SEO, Contact Form 7 and WooCommerce, as many WordPress sites run.
  • kitloom, lean — kitloom with what a bare WordPress has: posts and pages, media, menus, widgets, shortcodes, comments, the REST API, a form module and the Blade theme. No admin.
  • kitloom, full — the whole Filament skeleton: about a hundred packages, every module, the themes and the Filament admin.

Each page was requested for 10 seconds over one connection, then over eight at once, in three rounds:

  • ms is the median time of one page over one connection. Lower is better.
  • req/s is pages served per second over eight connections. Higher is better.

Without a page cache ​

Config and routes are cached on the kitloom side. Every page is built from the database.

PageWordPressWordPress + pluginskitloom, leankitloom, full
Home17 ms · 377 req/s36 ms · 193 req/s11 ms · 587 req/s20 ms · 333 req/s
Post13 ms · 496 req/s31 ms · 218 req/s10 ms · 609 req/s20 ms · 363 req/s
Post with 2,167 comments248 ms · 30 req/s303 ms · 24 req/s171 ms · 42 req/s201 ms · 35 req/s
Category16 ms · 371 req/s34 ms · 189 req/s14 ms · 460 req/s23 ms · 305 req/s
Search43 ms · 126 req/s83 ms · 87 req/s54 ms · 131 req/s64 ms · 112 req/s
Not found10 ms · 665 req/s27 ms · 248 req/s6.5 ms · 941 req/s14 ms · 430 req/s
REST: the latest posts18 ms · 376 req/s60 ms · 114 req/s12.5 ms · 238 req/s19 ms · 200 req/s
All 13 pages (geometric mean)19.6 ms · 327 req/s43 ms · 158 req/s14.7 ms · 395 req/s25 ms · 254 req/s

The thirteen pages are the home page and its second page, a post, the post with 2,167 comments, a child page, a category with subcategories and a subcategory, a tag, an author, a year, a search, a page not found and the REST list of posts.

With a page cache ​

Measured on the lean site, and through Laravel on the full one for two pages. The times here are the median over eight connections. WordPress ran Cache Enabler, a page-cache plugin that serves stored pages through PHP.

PageWordPressWordPress + Cache Enablerkitloom lean, through Laravelkitloom full, through Laravelkitloom, before Laravel starts
Home17.7 ms · 382 req/s0.2 ms · 17,675 req/s1.4 ms · 4,798 req/s7.0 ms · 878 req/s0.2 ms · 21,434 req/s
Post12.8 ms · 513 req/s0.2 ms · 18,549 req/s1.3 ms · 4,618 req/s7.1 ms · 769 req/s0.2 ms · 21,874 req/s
Category16.1 ms · 372 req/s0.2 ms · 18,624 req/s1.3 ms · 4,510 req/s–0.2 ms · 22,341 req/s
Search (kitloom: no longer stored, see below)43.3 ms · 127 req/s43.2 ms · 124 req/s, not stored1.4 ms · 4,630 req/s–0.2 ms · 21,597 req/s

Neither side keeps a "not found" page: it is built every time, as in the table above. Cache Enabler does not keep search results. On the code measured here kitloom kept one copy per search; since then it keeps none — every word searched would be another copy in the store — so its search times in this table are history: a search is now built every time, as in the table above.

What the numbers say ​

kitloom builds only what the page shows. A lean kitloom post runs 18 SQL queries, the full one 22, against 25 in WordPress. Menus are built only for the places a page draws them, the way wp_nav_menu() builds the menu of its location. A site moved from WordPress, with a dozen old menus nobody shows, does not pay for them.

What the packages cost. Everything the full skeleton adds to the lean one — about ninety packages and the admin — costs about 10 ms a page. Most of it is starting them: 7 ms against 1 ms, before the page is even looked at. WordPress pays more for its three plugins: they more than double its time, 13 → 31 ms for a post. A kitloom site that leaves out the modules it does not use gets the lean column.

Where WordPress is still ahead. Search: WordPress answers in 43 ms against kitloom's 54–64 ms. Eight connections at once on the REST list: WordPress serves more of them a second, although kitloom answers a single request faster.

Why search is slower. Both sides look for the words with LIKE '%harbor%' in the posts' HTML, which no index can serve: every search reads the content of every post, 23 MB here. WordPress reads it once and asks MySQL how many rows it found (SQL_CALC_FOUND_ROWS, MySQL's own). kitloom reads it twice, once to count the results for the page links and once for the page, because its lists count and fetch in two queries, the way Laravel pages any list on every database it supports. Where an index serves the query — a category, a tag, an archive — the count costs a fraction of a millisecond; on a search it is the whole difference: about 28 ms of SQL in WordPress against 21 + 23 ms in kitloom. The time grows with the text the site holds, so a site with a few thousand posts pays a few milliseconds. The search was measured without kitloom/search: with the module's database engine its index is read the same way, so it ranks better but is no faster. A large site that needs fast search gives the module Meilisearch or Typesense.

What Redis changes. WordPress keeps posts, terms, options and even query results in its object cache. With that cache in Redis, a warm post runs no SQL at all, and search gets three times faster (48 → 17 ms in an earlier run on this stack). kitloom reads only its settings and a few small lists from the cache, a fraction of a percent of a page's time: with its cache and sessions in Redis, kitloom's pages came out the same as with files.

What the page cache changes. Sent before Laravel starts, a stored kitloom page comes back as fast as WordPress's with a page-cache plugin: 0.2 ms, around 20,000 pages a second. Sent through Laravel, it costs the framework's start: 1.4 ms on the lean site, 7 ms on the full skeleton.

For a live site ​

  1. Run php artisan optimize on every deploy.
  2. Switch the page cache on: Settings → Cache.
  3. On one server, add SITE_CACHE_STATIC=true, so cached pages are sent before Laravel starts.
  4. Put the cache and sessions in Redis when several servers serve the site. Set REDIS_PERSISTENT=true as well: without persistent connections every request opens new ones, and at about 500 requests a second the server runs out of local ports ("Cannot assign requested address").
  5. Optionally, preload the site's classes with php artisan site:preload.
  6. Leave out the modules the site does not use: each one starts on every request.

Preload ​

PHP's opcache.preload compiles and links classes once, when PHP-FPM starts, instead of on every request. php artisan site:preload writes the file for it, bootstrap/preload.php, from what the site's pages really load: it draws the home page, a post, a page, a category, a search and a "not found" page as a visitor and lists the files they used — about 1,800–2,200 on the skeletons.

On this stack, with config and routes cached and the page cache off, it took 1.5–3 ms off a full-skeleton page that then took 32–34 ms, and used about 34 MB of OPcache memory. The tables on this page were measured without it.

In php.ini:

ini
opcache.preload=/var/www/site/current/bootstrap/preload.php
opcache.preload_user=www-data
opcache.memory_consumption=256

On every deploy, after php artisan optimize, run php artisan site:preload and restart PHP-FPM: preloaded classes stay as they were until it restarts, whatever the files on disk say. Warnings about an anonymous class (…@anonymous) while it starts are harmless. php artisan site:preload --check only counts the files. The details, and what the other warnings mean, are in the "Preload" section of kitloom/site's README.

In the browser: Lighthouse ​

The tables above time the server. What a visitor's browser does with the page is the other half: how soon the text and the largest picture appear, whether the page jumps while it loads, whether a screen reader can use it, whether a search engine finds what it needs. Google's Lighthouse scores that in four categories, from 0 to 100.

This is Keystone, the estate agency theme, on a public demo sandbox, measured by Lighthouse 12.8 in a clean Chrome 155 with no extensions, on 21 October 2026, every page in the same sandbox. On a phone means Lighthouse's own: a mid-range Android phone on a slow 4G connection, with a processor four times slower than the computer's. The phone figures are the median of three runs; each page was measured on a desktop once.

PagePhoneDesktopFirst text / largest picture, phone
Front page: search form, homes, places, agents, posts99 · 100 · 100 · 100100 · 100 · 100 · 1001.0 s / 1.9 s
A home: gallery, details, map98 · 100 · 100 · 100100 · 100 · 100 · 1001.0 s / 2.5 s
All homes: list and map100 · 100 · 100 · 10099 · 100 · 100 · 1001.0 s / 1.8 s
A place (taxonomy archive)100 · 100 · 100 · 100100 · 100 · 100 · 1001.0 s / 1.8 s
A post98 · 100 · 100 · 100100 · 100 · 100 · 1001.0 s / 2.5 s
A page98 · 100 · 100 · 100100 · 100 · 100 · 1001.0 s / 2.4 s

The four numbers are performance · accessibility · best practices · SEO. On every page the blocking time was 30 ms or less and nothing shifted (layout shift 0).

An archive needs a description of its own. Lighthouse's SEO check wants a <meta name="description"> on every page. A post or a page lends its excerpt or its own SEO description; a list has only what the owner writes for it: a post type's archive in SEO → Titles & Meta, a category or a place in the term's description (or, for every term of a taxonomy at once, a template there with %term%). Without one an archive scores 92 for SEO — Keystone's demo writes them, as an owner would.

What gets a theme there is mostly done for it:

  • The head: the canonical address, the title and description, robots, Open Graph and schema.org come from the core and the SEO module, the same for every theme.
  • The largest picture first. Images go out at the address the files are served from, not WordPress's /wp-content/uploads, which would cost a redirect; the front page's large picture is preloaded with fetchpriority="high", so the browser asks for it before the page's script has run.
  • Nothing heavy before it is seen. The map library loads when its map comes near the screen: a home's map at the foot of its page waits for a scroll.
  • Accessible by default: colours with a 4.5:1 contrast, a name for every link and button (an icon-only one keeps its label for screen readers), headings in order, labels on every form field.

A minute of it, with the reports:

Keystone in Lighthouse: performance, accessibility, best practices and SEO

To measure it yourself, open a sandbox from the Keystone demo, then open it in a private window — so neither your extensions nor the demo's sign-in are in the numbers — and run Lighthouse from Chrome's developer tools. Scores move a point or two from run to run; run it a few times.

How it was measured ​

The content ​

A fresh WordPress filled with the Theme Unit Test data and generated content:

  • 10,000 posts in 118 categories and with 564 tags;
  • 50 more pages in a tree;
  • 50,000 comments, a third of them replies;
  • 20 authors.

It was exported as a WXR file and imported into an empty kitloom with the Import & Export module: 10,239 posts and pages, 697 terms and 48,517 comments. Both sites read the same records from the same database server. The lean kitloom has no importer: it got the full one's content tables, row for row.

The themes are WordPress's Twenty Twenty-One and kitloom's Blade theme. Twenty Twenty-One's footer widgets were removed, so that both pages show the same things: the content, the menu and the comments. The WordPress with plugins has twenty WooCommerce products.

The environment ​

All four sites ran on the same machine, in identical containers.

MachineApple M4 Pro, 14 cores, 24 GB of memory, macOS 15.7
ContainersDocker Desktop 29.2, a Linux VM with 14 CPUs and 12 GB
PHP8.4.26 under PHP-FPM, 8 workers each, OPcache on without timestamp checks: the same image for every site
Web servernginx 1.27.5
DatabaseMySQL 8.0.46, one server for all
WordPress7.1.3; with plugins: Yoast SEO 28.7, Contact Form 7 6.2.2, WooCommerce 11.2.1
kitloomthe main branch of 17 October 2026 on Laravel 13.37, installed with composer install --no-dev; lean: 13 kitloom packages, full: the Filament skeleton
Loadoha 1.16.0 inside the Docker network, so port forwarding is not in the numbers

Before each page was measured, it was requested for 3 seconds at four connections, so the caches and OPcache were warm. The four sites were measured in three rounds, in a different order each round, each round waiting for a quiet machine; every figure is the median of the three. The tables with a page cache come from one run on the same stack and day.

What to keep in mind ​

  • It is one laptop. Docker Desktop and the other programs on it add noise: the three rounds were 3–9 % apart. The ratios held across runs; the milliseconds will differ on a server.
  • The two themes are different markup. Twenty Twenty-One inlines its CSS, so its pages are larger.
  • Without a page cache, WordPress gets nothing extra. It ran no caching plugin and no object cache in the first table.

kitloom — modules and themes for Laravel.
Pricing · Services · License · Refunds · Privacy · Community · [email protected]