Bug description
I am running Statamic with 800 sites and have hit some expected performance bottlenecks. These are three small changes that have helped improve my page load time.
Nav::existsIn() and CoreNav load all trees to check one site.
existsIn($site) calls trees()->has($site), and trees() calls $this->in() for every site, then filters. Same thing happens in CoreNav via $nav->sites()->contains(), since sites() goes through trees() too.
src/Structures/Nav.php
public function existsIn($site)
{
- return $this->trees()->has($site);
+ return $this->in($site) !== null;
}
src/CP/Navigation/CoreNav.php (lines 114 and 118)
-$nav->sites()->contains(Site::selected()->handle())
+$nav->existsIn(Site::selected()->handle())
Sites::authorized() gate overhead for super users. Our super users need everysite but most of our users have access to less than 30 sites.
authorized() runs can('view', $site) on every site. SitePolicy::before() already returns true for super users, but the gate framework still has per-call overhead.
src/Sites/Sites.php
public function authorized()
{
+ if (User::current()->isSuper()) {
+ return $this->sites;
+ }
+
return $this->sites->filter(fn ($site) => User::current()->can('view', $site));
}
Site::resolveAntlersValue() parses plain strings through the Antlers runtime
Every site config value goes through Parse::config() and RuntimeParser, even plain strings like en_US or /about/.
src/Sites/Site.php (after the existing is_array guard)
+ if (! is_string($value) || ! str_contains($value, '{')) {
+ return $value;
+ }
+
$value = Parse::config($value);
How to reproduce
I created a seeder to create a lot of sites. Seed the sites and then navigate the CP.
<?php
namespace Database\Seeders;
use App\Actions\CreateSiteAction;
use Faker\Factory as Faker;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class SiteSeeder extends Seeder
{
public function run(): void
{
$action = new CreateSiteAction;
$faker = Faker::create();
for ($i = 1; $i <= 1500; $i++) {
$name = $faker->unique()->company();
$handle = Str::slug($name);
$action->handle(
handle: $handle,
name: $name,
url: "/{$handle}/",
);
}
}
}
Our CreateSiteAction handles the creation of the site, role, asset container, etc.
Logs
Environment
Environment
Laravel Version: 13.8.0
PHP Version: 8.5.5
Composer Version: 2.9.5
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: UTC
Locale: en
Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: CACHED
Drivers
Broadcasting: log
Cache: file
Database: mariadb
Logs: stack / daily
Mail: log
Queue: database
Session: file
Storage
public/storage: NOT LINKED
Statamic
Addons: 4
License Key: Set
Sites: 1522 (Johnson, Tillman and Cremin, Dach-Weber, Howell, Beier and Ortiz, and 1519 more)
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 6.18.0 PRO
Statamic Addons
ndx/statamic-simple-redirects: 1.1.0
statamic/audit-log: 1.1.0
statamic/collaboration: 2.0.1
statamic/eloquent-driver: 5.7.0
Statamic Eloquent Driver
Addon Settings: file
Asset Containers: eloquent
Assets: eloquent
Blueprints: eloquent
Collection Trees: eloquent
Collections: file
Entries: eloquent
Fieldsets: file
Form Submissions: eloquent
Forms: eloquent
Global Sets: eloquent
Global Variables: eloquent
Navigation Trees: eloquent
Navigations: eloquent
Revisions: eloquent
Sites: eloquent
Taxonomies: eloquent
Terms: eloquent
Tokens: eloquent
Installation
Existing Laravel app
Additional details
I recognize this is an atypical use case and that these changes (particularly #1) may have implications I'm not aware of.
Bug description
I am running Statamic with 800 sites and have hit some expected performance bottlenecks. These are three small changes that have helped improve my page load time.
Nav::existsIn()andCoreNavload all trees to check one site.existsIn($site)callstrees()->has($site), andtrees()calls$this->in()for every site, then filters. Same thing happens inCoreNavvia$nav->sites()->contains(), sincesites()goes throughtrees()too.src/Structures/Nav.phppublic function existsIn($site) { - return $this->trees()->has($site); + return $this->in($site) !== null; }src/CP/Navigation/CoreNav.php(lines 114 and 118)Sites::authorized()gate overhead for super users. Our super users need everysite but most of our users have access to less than 30 sites.authorized()runscan('view', $site)on every site.SitePolicy::before()already returnstruefor super users, but the gate framework still has per-call overhead.src/Sites/Sites.phppublic function authorized() { + if (User::current()->isSuper()) { + return $this->sites; + } + return $this->sites->filter(fn ($site) => User::current()->can('view', $site)); }Site::resolveAntlersValue()parses plain strings through the Antlers runtimeEvery site config value goes through
Parse::config()andRuntimeParser, even plain strings likeen_USor/about/.src/Sites/Site.php(after the existingis_arrayguard)How to reproduce
I created a seeder to create a lot of sites. Seed the sites and then navigate the CP.
Our
CreateSiteActionhandles the creation of the site, role, asset container, etc.Logs
Environment
Installation
Existing Laravel app
Additional details
I recognize this is an atypical use case and that these changes (particularly #1) may have implications I'm not aware of.