The earlier articles covered the architecture and the content management system. This one is about speed and preparing the blog to scale. A nice interface does not help if pages load slowly.
Asynchronous processing with jobs
One of the key decisions was moving heavy operations into background tasks. View tracking is a good example:
class PostViewTracker implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected string $ipAddress;
protected Post $post;
protected Content $content;
public function __construct(string $ipAddress, Post $post, Content $content)
{
$this->ipAddress = $ipAddress;
$this->post = $post;
$this->content = $content;
}
public function handle()
{
// Check if there was already a view from this IP in the last 24 hours
$recentView = View::where('post_id', $this->post->id)
->where('ip_address', $this->ipAddress)
->where('created_at', '>', now()->subDay())
->exists();
if (!$recentView) {
View::create([
'post_id' => $this->post->id,
'content_id' => $this->content->id,
'ip_address' => $this->ipAddress,
]);
}
}
}
In the controller, we only dispatch the job to the queue:
public function show(string $language, string $slug)
{
$content = Content::where('slug', $slug)
->where('language', $language)
->firstOrFail();
$post = $content->post()
->with(['author', 'categories', 'tags'])
->first();
// Asynchronous view registration
PostViewTracker::dispatch(
request()->ip(),
$post,
$content
);
return view('blog.show', compact('post', 'content'));
}
View counting
Instead of incrementing a counter directly, we use an event-driven model:
class View extends Model
{
protected $table = 'admin_blog_posts_views';
protected $fillable = [
'uuid',
'post_id',
'content_id',
'ip_address',
];
public static function boot()
{
parent::boot();
static::created(function (View $model) {
// Update counter only after successful record creation
$model->post->increment('initial_view_count');
// Invalidate cache
Cache::tags(['posts', "post-{$model->post_id}"])->flush();
});
}
}
Publication scheduler
Scheduled posts are published automatically by Laravel commands:
class PublishScheduledPosts extends Command
{
protected $signature = 'blog:publish-scheduled {--dry-run}';
public function handle(): int
{
$isDryRun = $this->option('dry-run');
$now = now();
$scheduledPosts = Post::whereNotNull('scheduled_at')
->whereNull('published_at')
->where('scheduled_at', '<=', $now)
->lockForUpdate() // Prevent race conditions
->get();
if ($scheduledPosts->isEmpty()) {
$this->info('No posts scheduled for publishing.');
return self::SUCCESS;
}
$this->info("Found {$scheduledPosts->count()} post(s) ready for publishing:");
foreach ($scheduledPosts as $post) {
try {
if (!$isDryRun) {
$post->update([
'published_at' => $now,
'scheduled_at' => null,
]);
// Send notifications to subscribers
PublishPostNotification::dispatch($post);
Log::info("Published scheduled post", [
'post_id' => $post->id,
'scheduled_at' => $post->scheduled_at,
]);
}
$this->info("✓ Published post ID: {$post->id}");
} catch (Exception $e) {
$this->error("✗ Failed to publish post {$post->id}: {$e->getMessage()}");
Log::error("Failed to publish scheduled post", [
'post_id' => $post->id,
'error' => $e->getMessage(),
]);
}
}
return self::SUCCESS;
}
}
Registering the schedule:
// app/Providers/AppServiceProvider.php
$schedule->command(PublishScheduledPosts::class)
->everyMinute()
->withoutOverlapping()
->runInBackground();
Database query optimization
N+1 queries were one of our main performance problems. We fixed them with eager loading and optimized scopes:
// Bad - N+1 problem
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // Additional query for each post
}
// Good - eager loading
$posts = Post::with([
'author',
'categories',
'tags',
'contents' => function ($query) {
$query->where('language', app()->getLocale());
}
])->get();
Scopes for the more complex queries:
public function scopePublished(Builder $query): Builder
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', now())
->whereNull('archived_at');
}
public function scopeWithLocalContent(Builder $query, string $language = null): Builder
{
$language = $language ?? app()->getLocale();
return $query->with(['contents' => function ($q) use ($language) {
$q->where('language', $language);
}]);
}
// Usage
$posts = Post::published()
->withLocalContent()
->orderBy('published_at', 'desc')
->paginate(10);
Multi-level caching
Caching works at three levels:
1. Query caching
class PostRepository
{
public function getPublishedPosts(int $page = 1, int $perPage = 10)
{
$cacheKey = "posts.published.{$page}.{$perPage}." . app()->getLocale();
return Cache::tags(['posts', 'published'])
->remember($cacheKey, now()->addMinutes(10), function () use ($page, $perPage) {
return Post::published()
->withLocalContent()
->with(['author', 'categories', 'tags'])
->orderBy('published_at', 'desc')
->paginate($perPage, ['*'], 'page', $page);
});
}
public function clearCache(): void
{
Cache::tags(['posts'])->flush();
}
}
2. HTTP-level caching
// Middleware for HTTP response caching
class CacheResponse
{
public function handle($request, Closure $next)
{
// Cache only GET requests for guests
if ($request->isMethod('GET') && !auth()->check()) {
$key = 'response.' . md5($request->fullUrl());
return Cache::remember($key, now()->addMinutes(5), function () use ($request, $next) {
return $next($request);
});
}
return $next($request);
}
}
3. RSS feed caching
public static function getFeedItems()
{
return Cache::remember('feed_items', now()->addMinutes(10), function () {
return self::published()
->where(function(Builder $query) {
$query->whereNull('is_generated')
->orWhere('is_generated', false);
})
->with(['author'])
->latest('published_at')
->take(10)
->get();
});
}
Frontend optimization
1. Lazy component loading
// Instead of direct import
import PostEditor from '@/components/PostEditor.vue';
// Use dynamic import
const PostEditor = () => import('@/components/PostEditor.vue');
2. Virtualizing long lists
<template>
<VirtualList
:items="posts"
:item-height="120"
:buffer="5"
v-slot="{ item }"
>
<PostCard :post="item" />
</VirtualList>
</template>
<script setup>
import { VirtualList } from '@tanstack/vue-virtual';
// Component renders only visible elements
// saving memory and speeding up rendering
</script>
3. Search debouncing
const debouncedSearch = debounce(() => {
performSearch();
}, 500);
watch(() => form.search, (newSearch) => {
if (newSearch.length >= 3 || newSearch.length === 0) {
debouncedSearch();
}
});
Performance monitoring
We watch a few key metrics:
class PerformanceMonitor
{
public static function trackPageLoad(string $page, float $loadTime)
{
DB::table('performance_metrics')->insert([
'page' => $page,
'load_time' => $loadTime,
'memory_usage' => memory_get_peak_usage(true),
'created_at' => now(),
]);
// Alert on slow loading
if ($loadTime > 2.0) {
Log::warning("Slow page load detected", [
'page' => $page,
'load_time' => $loadTime,
]);
}
}
}
Database optimization
Proper indexes matter a great deal for performance:
Schema::table('admin_blog_posts', function (Blueprint $table) {
// Composite index for frequent queries
$table->index(['published_at', 'archived_at']);
// Index for sorting
$table->index('created_at');
});
Schema::table('admin_blog_posts_contents', function (Blueprint $table) {
// Unique composite index
$table->unique(['post_id', 'language']);
// Index for slug search
$table->index('slug');
});
Results
After all these changes, we measured:
Main page load time: under 200 ms
Post load time: under 150 ms
Throughput: over 10,000 requests per second
Memory usage: 60% lower thanks to caching
Analysis tools
For monitoring we use a few tools:
# Laravel Debugbar for development
composer require barryvdh/laravel-debugbar --dev
# Telescope for production monitoring
composer require laravel/telescope
# Query analysis
DB::enableQueryLog();
// ... code ...
dd(DB::getQueryLog());
Takeaways
There was no single big optimization. Speed came from many small improvements:
Heavy operations run asynchronously
Database indexes are used properly
Caching works at several levels
The frontend is optimized
Monitoring never stops
The next article is about user experience: the interface, the filtering system and the preview mode for authors.
To be continued.



