Skip to main content
Laravel Applications

How We Optimized a Laravel Application for Lightning-Fast Performance on Shared Hosting

How I cut my Laravel blog from 250ms to 98ms on Hostinger shared hosting: query fixes, cache layers, and an HTTP-cache middleware you can copy.

7 min
Read time
1,333
Words
Published
Last revised
Engr Mejba Ahmed

Written by

Engr Mejba Ahmed

Share Article

How We Optimized a Laravel Application for Lightning-Fast Performance on Shared Hosting

This site, mejba.me, runs on Hostinger shared hosting. Not a VPS, not a managed Laravel platform. That is a deliberate choice, and it forced me to learn something most performance guides skip: on shared hosting you do not get to buy your way out of slow code. After a few rounds of optimization, my blog pages went from 250ms to 98ms, the homepage from 323ms to 150ms, the services page from 400ms to 180ms, and per-page query counts dropped from 120+ to somewhere between 15 and 40.

My thesis after doing this on a real production app: shared hosting performance is won in three layers, in a strict order. Queries first, framework caches second, HTTP caching at the edge last. People reach for the edge first because it looks like the biggest win, and then they cache broken pages.

How We Optimized a Laravel Application for Lightning-Fast Performance on Shared Hosting - overview of what you are actually fighting on shared hosting, step 1: measure before touching anything

What you are actually fighting on shared hosting

The constraints are specific, and they change which optimizations matter:

  • 128 to 256 MB PHP memory, so anything that hydrates thousands of models per request will eventually fall over.
  • No root access, so no Redis, no custom PHP extensions, no nginx config. Your cache driver options are file or database.
  • Shared CPU and disk I/O, so file-based sessions and caches compete with hundreds of other tenants for the same disks.
  • You can still control OPcache and HTTP headers, which is where most of the free wins live.

Everything below worked within those limits. Nothing here requires root.

Step 1: Measure before touching anything

I installed Laravel Telescope locally and pointed it at a copy of production data. The homepage was issuing 120+ queries. Most of them were the same three patterns repeated: posts loaded without their author and category relations, duplicate category lookups from a view composer, and unindexed LIKE searches.

If you skip this step you will optimize the wrong thing. My blog index looked like the slow page, but the query log showed the services page was worse per-request because an N+1 pattern was hiding in a Blade partial that rendered a relation inside a loop. I also keep slow-query logging on in production now, because regressions creep back in with every feature.

Step 2: Fix the query layer

Three changes accounted for most of the query-count drop:

Eager load by convention, not by exception. Every controller in this codebase now loads relations explicitly:

$posts = Post::with(['author', 'category'])
    ->latest('published_at')
    ->paginate(10);

That single change took the blog index from 21 queries to 3. I made eager loading a written convention in the project's CLAUDE.md file so it survives future contributors, including AI ones.

Index what you filter and search. blog_posts.title and shop_products.name get hit by search. Adding plain indexes took search queries from roughly 150ms to 40ms. Nothing clever, just indexes on columns that appear in WHERE and LIKE 'term%' clauses.

Cache queries, and invalidate with observers. Cache::remember() is easy. The part everyone gets wrong is invalidation. This site uses model observers that clear exactly the keys a change affects. From the actual PostObserver in this repo:

Cache::forget('sitemap_xml');
Cache::forget('post_content_'.$post->id.'_'.$locale);
Cache::forget('related_posts_'.$post->id.'_'.$locale);
Cache::forget('homepage_latest_posts');

When a post is saved, its cached content, its related-posts block, the homepage latest list, and the sitemap all get busted. No TTL guessing, no "why is the old title still showing" tickets.

Step 3: Framework caches and OPcache

The boring commands matter more on shared hosting than anywhere else, because every millisecond of bootstrap is amplified by slow disks:

php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
composer install --no-dev --optimize-autoloader

That is worth roughly 40ms of bootstrap per request on my host, plus another 15 to 20ms from the optimized autoloader. My GitHub Actions deploy runs these on every push so they can never be forgotten.

One gotcha that has bitten me in production: once config is cached, edits to .env do nothing until you run config:cache again. I lost an afternoon to an integration that was "configured" but silently reading stale values.

For OPcache, shared hosts usually let you control it through .user.ini:

opcache.enable=1
opcache.revalidate_freq=60

And since Redis is off the table, both cache and sessions moved from file to database. On contended shared-host disks, the database driver measured 40 to 60 percent faster for me than file reads, which surprises people until they remember MySQL has its own buffer pool and your files do not.

Step 4: HTTP caching at the edge, with scars

The biggest single win was a middleware I wrote called CacheGuestHtml. It marks guest pages as publicly cacheable so Hostinger's edge and any CDN can serve them without touching PHP:

private const CACHEABLE_TTL =
    'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400';

Logged-out visitors get a 5-minute browser cache, the CDN holds pages for an hour, and stale pages can be revalidated in the background for a day. The middleware also strips the session, XSRF, and locale cookies from cacheable responses, because a Set-Cookie header makes most CDNs refuse to cache at all.

Here is the part I earned the hard way. Stripping the XSRF cookie from a cached page breaks every form on it. My blog posts carry a Livewire comment form, and after I shipped this middleware, every comment submission started returning 419 Page Expired, because visitors were receiving a cached page whose CSRF token belonged to nobody. Contact-form pages in the non-default locales had the same failure, and it was invisible in testing because /contact was excluded by path while /de/contact was not.

The fix was to exclude by route name instead of path, since this site registers every page six times with locale prefixes:

private const NON_CACHEABLE_ROUTES = [
    'blog.show', '*.blog.show',
    'contact', '*.contact',
];

One wildcard covers all locales. Route names also let me cache /blog/category/{slug} while excluding /blog/{slug}, which a path glob cannot distinguish.

A second non-obvious detail: the middleware sends Vary: Accept-Encoding only, never Vary: Accept-Language. Locale is in my URLs (/de/..., /es/...), so varying on the header would only shatter the CDN cache. Googlebot rotates its language preferences between crawls, and every rotation would have been a cache miss.

The trade-off to accept: content changes can look stale to logged-out visitors for up to an hour. My observers clear the application cache instantly, but the CDN copy self-heals on its own s-maxage schedule. For a content site, that is a fine trade. For a dashboard, it is not, which is why the middleware refuses to cache authenticated responses at all.

The results, honestly framed

Page Before After
Blog post 250ms 98ms
Homepage 323ms 150ms
Services page 400ms 180ms
Queries per page 120+ 15–40

Those "after" numbers are PHP render times on cache-miss requests. Edge-cached hits do not touch PHP at all, so the typical logged-out visitor sees better than that. Payload size also dropped from about 2.5 MB to 0.85 MB after a Vite production build plus gzip and long-lived asset headers in .htaccess.

What I would skip

Two things I tried that were not worth it on shared hosting: micro-tuning Blade with @once and partial extraction (single-digit millisecond wins), and moving image processing into the request cycle with on-the-fly resizing (it fights the CPU throttle; pre-generate sizes at upload instead). If you are planning an upgrade path off shared hosting, spend that energy on understanding what changed in Laravel 13 first, because the framework's own caching primitives keep improving faster than hosting tiers do.

Want this done to your Laravel app?

Everything in this post came from optimizing a live production site, and the same three-layer pass (queries, framework caches, edge caching) transfers to any Laravel app on constrained hosting. If yours is slow and you would rather have someone who has done this walk your codebase, my Laravel optimization and development services cover exactly this kind of audit, with before-and-after numbers like the table above as the deliverable.

Advertisement
Coffee cup

Enjoyed this article?

Your support helps me create more in-depth technical content, open-source tools, and free resources for the developer community.

Related Topics

Engr Mejba Ahmed

Engr Mejba Ahmed

Engr. Mejba Ahmed builds AI-powered applications and secure cloud systems for businesses worldwide. With 8+ years shipping production software in Laravel, Python, and AWS, he's helped companies automate workflows, reduce infrastructure costs, and scale without security headaches. He writes about practical AI integration, cloud architecture, and developer productivity.

Related Articles

Browse All

Comments

Leave a Comment

Comments are moderated before appearing.

Learning Resources

Expand Your Knowledge

Accelerate your growth with structured courses, verified certificates, interactive flashcards, and production-ready AI agent skills.

Sample Certificate of Completion

Sample certificate — complete any course to earn yours

Engr Mejba Ahmed

Engr Mejba Ahmed

AI assistant · trained on my work

👋

Hey there!

Quick Actions

WhatsApp Direct line to me

Chat on WhatsApp

+880 1723 741224 · Replies within the hour on working days

Popular Questions

Engr Mejba Ahmed is connected
Engr Mejba Ahmed is typing...
Engr Mejba Ahmed avatar

✉ Want me to follow up? Drop your email

Engr Mejba Ahmed avatar

📞 Connect Directly

Choose how you'd like to reach me

WhatsApp

+880 1723 741224

Email

mejba.13@gmail.com

✓ Details sent! I'll get back to you shortly.

Powered by OpenAI

335+

Blog Posts

25

AI Courses

63

Projects

Services & Expertise

Pricing & Process

Learning & Resources

Connect & Support