diff --git a/app/Models/Comment.php b/app/Models/Comment.php
index 99071d7e..1b639cab 100644
--- a/app/Models/Comment.php
+++ b/app/Models/Comment.php
@@ -77,10 +77,12 @@ protected function hierarchy(): Attribute
CommentHierarchy
SQL;
- return new Attribute(
+ $attribute = new Attribute(
get: fn ($value) => Arr::first(
DB::select($query, ['id' => $this->id])
)
- )->shouldCache();
+ );
+
+ return $attribute->shouldCache();
}
}
diff --git a/app/Models/User.php b/app/Models/User.php
index d3c5b7bc..2b5d7d8b 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -81,9 +81,11 @@ public function notifyNewComment(NewComment $instance): void
protected function gravatarUrl(): Attribute
{
- return new Attribute(
+ $attribute = new Attribute(
get: fn ($value) => get_gravatar(email: $this->email, size: 512)
- )->shouldCache();
+ );
+
+ return $attribute->shouldCache();
}
public function passkeys(): MorphMany
diff --git "a/resources/views/components/layouts/\342\232\241header.blade.php" "b/resources/views/components/layouts/\342\232\241header.blade.php"
index 8284cc54..f0d68283 100644
--- "a/resources/views/components/layouts/\342\232\241header.blade.php"
+++ "b/resources/views/components/layouts/\342\232\241header.blade.php"
@@ -55,6 +55,10 @@ public function logout(Logout $logout): void
@endscript
+@php
+ $hasUnreadNotifications = auth()->check() && auth()->user()->unreadNotifications()->exists();
+@endphp
+
- @if (auth()->user()->unreadNotifications->count() > 0)
+ @if ($hasUnreadNotifications)
@@ -315,7 +319,7 @@ class="rounded-full text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-50 hov
- @if (auth()->user()->unreadNotifications->count() > 0)
+ @if ($hasUnreadNotifications)
diff --git a/tests/Feature/HeaderPerformanceTest.php b/tests/Feature/HeaderPerformanceTest.php
new file mode 100644
index 00000000..e204fa4a
--- /dev/null
+++ b/tests/Feature/HeaderPerformanceTest.php
@@ -0,0 +1,58 @@
+create();
+
+ // Create 50 unread notifications
+ for ($i = 0; $i < 50; $i++) {
+ DatabaseNotification::create([
+ 'id' => Str::uuid()->toString(),
+ 'type' => 'App\Notifications\TestNotification',
+ 'notifiable_type' => User::class,
+ 'notifiable_id' => $user->id,
+ 'data' => ['message' => 'test'],
+ 'read_at' => null,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+ }
+
+ $this->actingAs($user);
+
+ DB::enableQueryLog();
+ DB::flushQueryLog();
+
+ $start = microtime(true);
+
+ Livewire::test('layouts.header');
+
+ $end = microtime(true);
+ $duration = $end - $start;
+
+ $queries = DB::getQueryLog();
+ $queryCount = count($queries);
+
+ $hasFullSelect = collect($queries)->contains(function ($query) {
+ // SQLite uses double quotes, MySQL uses backticks.
+ // We just check for "select * from" and "notifications"
+ return str_contains(strtolower($query['query']), 'select * from')
+ && str_contains(strtolower($query['query']), 'notifications');
+ });
+
+ $hasExists = collect($queries)->contains(function ($query) {
+ return str_contains(strtolower($query['query']), 'exists');
+ });
+
+ // Verify results
+ expect($queryCount)->toBeLessThanOrEqual(3);
+ expect($hasExists)->toBeTrue();
+ // We can't strict check "Has Full Select" because exists() contains the string.
+ // But we can check that we used the optimized path.
+});