From 69 to 92: How I Optimized ShabelleHub's PageSpeed Score
A five-phase performance rebuild, done entirely in Termux, with no laptop in sight.
*A five-phase performance rebuild, done entirely in Termux, with no laptop in sight.*
For months, ShabelleHub's mobile PageSpeed score sat at 69. Not broken, not embarrassing, just... mediocre. The kind of number that doesn't trigger alarms but quietly costs you search rankings and impatient visitors. Fixing it meant a full frontend audit — and doing that audit from a Samsung phone running Termux, with no desktop DevTools to lean on, turned out to be its own project.
Why 69 Wasn't Good Enough
Google's Core Web Vitals directly affect SEO ranking, and a directory site like ShabelleHub lives and dies by organic search traffic. A slow mobile experience wasn't just a vanity metric — it was actively working against the site's main growth channel. I broke the fix into five phases instead of trying to solve everything in one pass, since debugging performance regressions on a small screen is much easier when you can isolate exactly which change caused what.
Phase 1: Animation Overhead
The homepage used framer-motion for scroll-reveal effects — nice in a demo, expensive in production. It was one of the heaviest JavaScript dependencies on the page for a purely cosmetic effect. Removing it entirely and replacing it with a custom useRevealOnScroll hook built on IntersectionObserver and plain CSS transitions cut bundle size immediately.
Phase 2: Font Loading
Google Fonts were loading render-blocking, which meant text was invisible until an external request resolved. Migrating to next/font/google for self-hosting fixed this, though it wasn't clean on the first try — Space Grotesk's variable weight axis needed a specific fix to render correctly once self-hosted.
Phase 3: The LCP Element
The Largest Contentful Paint element on the homepage was, ironically, animated. An animated LCP element delays the moment the browser can mark the page as "loaded" from a Core Web Vitals perspective. Swapping it for a plain CSS-animated element (no JS driving it) fixed this directly.
Phase 4: Re-render Waste
ToolCard and FeaturedTools components were re-rendering more than they needed to. Adding React.memo and useCallback where appropriate cut down on wasted render cycles, particularly on the tools listing page where dozens of cards render at once.
Phase 5: Real Data, Real Trade-offs
The final phase wasn't a pure performance win — it added a real-time trending section backed by a Supabase tool_events table, with a /api/track.js endpoint using navigator.sendBeacon for non-blocking event tracking, and server-side aggregation in lib/trending.js. This added functionality without adding to the critical rendering path, since tracking happens asynchronously and aggregation happens server-side.
The Workflow Problem
None of this was straightforward to ship from a phone. The recurring pattern: package changes as a zip file, copy it into Termux, unzip, verify, commit, push, let Vercel auto-deploy. Along the way: an accidental node_modules commit (fixed with .gitignore and git rm --cached), SSL failures on mobile data that turned out to be a Private DNS setting, and a broken deployment caused by removing framer-motion too early in the sequence — diagnosed only by reading raw Vercel build logs on a 6-inch screen.
Measuring, Not Guessing
One thing that kept this project from turning into five rounds of guesswork was treating PageSpeed's own breakdown as a checklist rather than a single score to stare at. Each phase started with a fresh Lighthouse run, noting which specific metric — Largest Contentful Paint, Total Blocking Time, Cumulative Layout Shift — was flagged, and ending with another run to confirm that exact metric had actually moved. It's tempting, especially when testing is slower and more deliberate on mobile, to make a change that "feels" like it should help and move on without re-measuring. Skipping that verification step even once would have made it impossible to know which of the five phases were actually responsible for the final jump, versus which were neutral or even slightly negative trade-offs hidden behind an overall improving trend.
What Didn't Make the Cut
Not every optimization idea survived contact with testing. An early idea to lazy-load the entire hero section image was dropped after it became clear the hero image *was* the LCP element in some page states — lazy-loading it would have delayed the exact metric the fix was supposed to improve. Another idea, aggressively code-splitting every route, was tested and reverted after it introduced a noticeable delay on first navigation between pages, trading one metric's improvement for a regression in perceived responsiveness. Both of these are the kind of mistake that's easy to avoid on paper and easy to make in practice, and both were only caught because of the re-measure-after-every-change discipline described above.
The Result
69 to 92. Not a perfect 100, but a real, measurable jump — achieved without a single desktop session. The lesson wasn't really about any one optimization technique; it was that performance work is diagnosable and fixable incrementally, even when your "IDE" is a terminal emulator and your test device is the same phone you're coding on.
📬 Get the latest AI tool reviews
Expert picks and comparisons, weekly. No spam.