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.

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
fileordatabase. - 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.