Case Study: Raising a Vue App's Lighthouse Score to 93
A Vue and Vite case study: how route splitting, lazy components, deferred analytics and leaner fonts raised a Lighthouse performance score from 60 to 93.
I raised our landing page's Lighthouse performance score from about 60 to 93 by shrinking what the first request loads, not by removing features. The changes were route-level code splitting, lazy-loaded heavy components, analytics started after the page is idle, and fewer fonts up front. The app is a Vue single-page app built with Vite.
Our landing page was visually strong, but the frontend was shipping too much too early.
In this guide:
- The problem: a 1.8 MB main bundle
- What I changed, in five steps
- Benchmarks before and after
- Why this worked
- Frequently asked questions
The biggest symptoms were:
A Google performance score around 60 on the landing page
A very large production bundle
Heavy CSS and font payloads
Too much non-critical work happening during initial load
This was the warning I was getting from Vite while running the build command.
vite v6.4.1 building for production...
✓ 2527 modules transformed.
dist/index.html
(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 9.64s
Raising build.chunkSizeWarningLimit only hides that warning. The fix is to load less on the first page, which is what the rest of this post does. The option is described in Vite's build options reference.
This write-up focuses only on the frontend side. API performance was intentionally left out.
The Problem
From the original production build:
Main JS bundle: 1,841.83 kB minified
Main CSS bundle: 269.42 kB
Gzipped JS: 590.54 kB
Gzipped CSS: 61.27 kB
That meant the browser had to parse, download, and evaluate far more than the landing page actually needed.
Strategy
I kept the plan simple:
Reduce the critical path
Load only what the current page needs
Push non-essential work after first paint
Keep regressions low by changing loading strategy first, not rewriting features
What I Changed
1. Route-level code splitting
The landing page should not pay the cost of planner screens, task editing flows, integrations, and dashboard code.
I changed the app so major routes load on demand instead of being pulled into the first page load.
const routes = [
{ path: "/", component: () => import("...HomePage...") },
{ path: "/app", component: () => import("...WorkspaceView...") },
{ path: "/settings", component: () => import("...SettingsView...") },
];
Vue Router turns each dynamic import() into its own chunk, fetched the first time the route is visited (lazy loading routes in the Vue Router docs).
2. Lazy-loading expensive UI inside authenticated screens
Even inside the app, some heavy UI only appears after user interaction.
Examples:
task edit modal
integration panels
filter sidebar
device-specific overlays
So instead of bundling them into the initial workspace render, I loaded them only when needed.
const HeavyModal = defineAsyncComponent(() => import("...TaskEditor..."));
You can read more about defineAsyncComponent in the official Vue docs on async components.
3. Deferring analytics and monitoring
Analytics, session tracking, and error monitoring are useful, but they do not need to compete with first paint.
I moved them behind a delayed, idle-time startup.
runAfterLoadAndIdle(() => {
setTimeout(() => {
initAnalytics();
initMonitoring();
loadThirdPartyScript();
}, 4000);
});
This was one of the highest-leverage changes for Lighthouse.
4. Font loading discipline
The previous build was shipping a wide set of font families and language subsets up front.
I changed that strategy to:
load only base fonts first
load landing-page accent fonts only on the landing page
load theme-specific fonts only when that theme is active
use narrower subsets instead of pulling everything by default
loadBaseFonts();
loadLandingFonts();
loadThemeFonts(currentTheme);
5. Reducing work on the landing page itself
The landing page also had runtime work that looked nice but did not need to happen immediately.
I optimized that by:
delaying visual effects until idle time
keeping below-the-fold sections out of the initial rendering cost
adding image dimensions to reduce layout work
preserving design while trimming early execution
Benchmarks
Build Footprint
| Metric | Before | After |
|---|---|---|
| Main landing-path JS | 1,841.83 kB | about 237 kB critical JS |
| Main landing-path CSS | 269.42 kB | about 24 kB critical CSS |
Lighthouse Snapshot
| Metric | Before | After |
|---|---|---|
| Landing page performance score | ~60 | 93 |
Notes:
The new JS number reflects the critical landing path, not the entire app footprint.
Large authenticated features still exist, but they are no longer loaded up front on the landing page.
The score is from a local Lighthouse run. Lighthouse scores move between runs and devices, so compare runs made on the same machine and settings (how Lighthouse scores performance).
Why This Worked
The biggest lesson was straightforward:
I did not need less product. I needed less product in the first request.
Most performance issues were not caused by one huge mistake. They came from many reasonable frontend decisions stacking together:
eager route imports
global loading of rare UI
early analytics boot
broad font loading
decorative runtime work too early
Once I treated the landing page like a focused entry experience instead of the whole app, performance improved quickly.
Key takeaways
Start with the critical path, not micro-optimizations
Route splitting gives outsized wins when a landing page and app shell live together
Third-party scripts should earn their place in the first few seconds
Font strategy matters more than many teams think
Good performance work is often about loading order, not feature removal
Frequently asked questions
What is a good Lighthouse performance score?
Lighthouse rates a performance score of 90 to 100 as good, 50 to 89 as needing improvement, and below 50 as poor. This landing page started around 60, in the middle band, and reached 93 after the changes above. Scores vary between runs, so compare runs made on the same machine.
How do I fix the Vite warning that chunks are larger than 500 kB?
Split the code instead of raising the limit. Load each route with a dynamic import, load heavy components with defineAsyncComponent, and keep rarely used features out of the first page. Raising build.chunkSizeWarningLimit only hides the warning, and manualChunks helps caching more than it shrinks the first load.
Should I lazy-load every Vue component?
No. Lazy-load what the first screen does not need: other routes, and heavy UI that only appears after a click, like editors, modals and integration panels. Components visible on first paint should stay in the main chunk, or the page waits on extra requests before it can show anything useful.
Did the speedup mean removing features?
No. Every feature stayed in the product. The work changed when code loads, not what the app does: the landing page stopped downloading planner screens, task editors, integrations and analytics it never used. In this case study, loading order mattered more than cutting features, which kept the risk of regressions low.
Final Summary
I improved page speed by shrinking the initial frontend payload, deferring non-essential work, and keeping heavy product features out of the landing-page critical path.
The result was a much lighter first load, better rendering behavior, and a local Lighthouse score that moved from the low-performance range into the 90+ range without redesigning the product.
If your Vue or Vite app is a static single-page app, the guide to deploying a static site or Vite SPA with ox shows how it is served by Caddy on your own server, with no app process. For a Vue frontend with a Django API, see the Django and Vue.js VPS deployment guide, and for why I build tools this way, the story behind ox.
Before you go:
If you're someone struggling with managing time, then check out this lightweight but powerful web app Lazy Planner.
I made this for myself first because I couldn't find one that better suits my needs, but I believe if it's useful for me, it could be useful for more people too.
Updated : added a short summary, a FAQ, notes on the Vite warning and Lighthouse scoring, and links to the official docs.
First published on blog.sorv.dev.
New to running apps on your own server? How ox self-hosts your apps shows what ox sets up on your server.
