Why Your Site Feels Slow (Even When Your Server Is Fast)
Lighthouse isn’t magic, it’s a very opinionated flashlight
People often treat Lighthouse like a pass/fail badge: run it, glance at the score, maybe tweak an image or two, and move on. That’s a waste.
Used properly, Lighthouse is more like a guided interrogation of your page: What’s blocking rendering? Why is JavaScript doing so much work? Which assets are being downloaded for no good reason? The real value is in those details.
Let’s walk through three practical performance debugging stories:
- A single-page app crushed by JavaScript
- A marketing site drowning in media
- A “fast” app ruined by main-thread work and third-party scripts
You’ll see where to look in Lighthouse, how to interpret the weird jargon, and — most importantly — what to actually change in your codebase.
Story 1 – The JavaScript-heavy dashboard that felt sticky
Picture a typical admin dashboard: charts, tables, filters, client-side routing. On a decent laptop it feels okay. On a mid-range phone, though? Every navigation stutters.
The Lighthouse moment
Run Lighthouse (Performance only, mobile mode, throttling on). The score isn’t terrible, but a few numbers stand out:
- Time to Interactive (TTI): ~9.5 s
- Total Blocking Time (TBT): ~1200 ms
- Largest Contentful Paint (LCP): 3.1 s (acceptable-ish)
So the main content appears reasonably quickly, but the app isn’t truly usable for several seconds. That’s your hint: this isn’t a network problem; it’s a JavaScript problem.
Scroll down to Diagnostics and Opportunities and you’ll often see:
- “Reduce JavaScript execution time”
- “Remove unused JavaScript”
- “Avoid an excessive DOM size” (sometimes)
Open the View Treemap link next to “Remove unused JavaScript.” Now you see the real villain: a 900 KB vendor bundle plus a massive charting library that’s loaded on every route — even the ones that don’t show charts.
Herkenbaar? You think, “We only show charts on two pages, why is this everywhere?”
Debugging steps that actually move the needle
Here’s a simple, realistic workflow:
Identify heavy bundles
In the Treemap, look for big boxes representing JS bundles. Hover to see the file and library names. You might see things like:charting-lib.js– 350 KBdate-fns-all.js– 180 KBvendor.js– 400 KB
Check Main-thread work
In Lighthouse, expand “Minimize main-thread work.” It breaks down where time is spent:- Script evaluation
- Script parsing/compilation
- Style & layout
If script evaluation dominates, that’s your bottleneck.
Lazy-load what users don’t need immediately
Instead of importing everything at the top of your main bundle, split your app:// Before import HeavyChart from './HeavyChart'; function Dashboard() { return <HeavyChart />; } // After: lazy-load const HeavyChart = React.lazy(() => import('./HeavyChart')); function Dashboard() { return ( <React.Suspense fallback={<Spinner />}> <HeavyChart /> </React.Suspense> ); }Same idea works in Vue (
defineAsyncComponent) or Angular (lazy routes).Trim dependencies
In many audits, Lighthouse flags “Reduce JavaScript bundle size with code splitting” and “Remove unused JavaScript.” That’s usually a kind way of saying: stop importing the entire library for one function.Example: replace
momentor huge date libraries with smaller utilities, or import only what you use:// Bad import _ from 'lodash'; const result = _.debounce(fn, 200); // Better import debounce from 'lodash/debounce'; const result = debounce(fn, 200);Re-run Lighthouse and compare
After lazy-loading the charts and trimming a couple of libraries, you might see something like:- TTI: 5.2 s → 3.1 s
- TBT: 1200 ms → 450 ms
- Performance score: 68 → 88 (rough example, but you get the trajectory)
Now the dashboard still looks the same, but interactions start much sooner and the app feels lighter, especially on phones. Klinkt logisch, toch?
Story 2 – The gorgeous marketing site that quietly kills mobile users
Next scenario: a brand’s marketing homepage. Giant hero video, autoplay background, full-bleed images, fancy animations. On a MacBook Pro over fiber, it feels fine. On a budget Android over 4G? The page is basically a slideshow.
Lighthouse doesn’t care that it “looks great”
Run Lighthouse (mobile, throttled). The report slaps you with:
- LCP: 6.8 s
- First Contentful Paint (FCP): 3.5 s
- Cumulative Layout Shift (CLS): 0.45 (well above the recommended 0.1)
Up top you’ll probably see Opportunities like:
- “Serve images in next-gen formats”
- “Properly size images”
- “Defer offscreen images”
- “Video formats and autoplay issues” (sometimes via additional audits)
Scroll to the LCP element section. Lighthouse tells you exactly what counts as your Largest Contentful Paint — maybe a <video> background or a huge hero image.
Fixing LCP and CLS without killing the design
The goal isn’t to strip all visual flair. It’s to get the main content visible fast and stop the layout from jumping around.
Step 1: Tame the hero media
If LCP is a hero image:
- Use an appropriate size (
srcsetandsizes) instead of shipping a 4000 px wide JPEG to a 375 px wide phone. - Convert to WebP or AVIF for supported browsers.
Preload the LCP image with:
<link rel="preload" as="image" href="/hero.webp" imagesrcset="/hero-800.webp 800w, /hero-1600.webp 1600w" imagesizes="100vw">
If LCP is a video background, be honest with yourself: do you really need full autoplay motion on mobile? Often a static poster image for small screens loads faster and still looks good.
Step 2: Kill layout shifts at the root
CLS is usually death by a thousand cuts: images without dimensions, ads or embeds loading late, fonts swapping.
Common Lighthouse hints:
- “Images do not have explicit width and height”
- “Avoid large layout shifts”
Concrete actions:
- Always specify
widthandheight(or aspect-ratio) for images. - Reserve space for embeds (YouTube, iframes) with a wrapper that has a fixed ratio.
- Use
font-display: swapor similar strategies to avoid huge shifts when web fonts load.
Example for responsive images:
<img
src="/hero-800.jpg"
srcset="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1600.jpg 1600w"
sizes="(max-width: 768px) 100vw, 50vw"
width="1600"
height="900"
alt="Product in use"
/>
Step 3: Lazy-load the non-critical eye candy
Lighthouse’s “Defer offscreen images” audit will highlight images below the fold. Add loading="lazy" where appropriate, or use an intersection observer–based lazy loader if you need more control.
<img src="/gallery-1.webp" loading="lazy" alt="Customer story" />
Re-run Lighthouse and you might see something like:
- LCP: 6.8 s → 2.9 s
- CLS: 0.45 → 0.03
- Perceived performance on a mid-range phone: night and day
The brand doesn’t lose its “wow” factor, but users actually see that wow before they bounce.
Story 3 – The “modern” SPA destroyed by its own main thread
Third case: a JavaScript framework SPA with client-side routing, animated transitions, and a forest of analytics and marketing tags. Navigation feels janky despite good LCP.
This is where Lighthouse’s Total Blocking Time and Main-thread work audits earn their keep.
When TBT screams louder than LCP
Lighthouse run (again: mobile, throttled) shows:
- LCP: 2.4 s (nice)
- Speed Index: 3.0 s
- TBT: 1500 ms (yikes)
The page looks ready quickly, but interactions lag. Scrolling is sticky, button clicks feel delayed.
In Opportunities and Diagnostics, you likely see:
- “Reduce the impact of third-party code”
- “Reduce JavaScript execution time”
- “Avoid long main-thread tasks”
Click into “Avoid long main-thread tasks”. Lighthouse lists tasks over 50 ms. You might see one or two monsters:
- 400 ms: analytics bundle + tag manager
- 350 ms: hydration of the SPA framework
- 300 ms: a custom animation library initializing everything at once
Strategy: make the browser breathe
Main-thread work isn’t evil. The problem is doing all of it at once. The fix is usually a mix of deferring, splitting, and sometimes just saying “no” to unnecessary scripts.
1. Audit and triage third-party scripts
Start with what Lighthouse highlights under “Reduce the impact of third-party code.” You’ll see domains and timings.
Typical low-hanging fruit:
- Multiple analytics tools all doing similar things
- Heatmap/recording tools loaded on every page, including login
- Tag managers that inject a small army of additional scripts
Ask the uncomfortable question: do we really need all of these on every route? Often the answer is “no, but marketing added them and we never pushed back.”
Implement changes like:
- Loading some tools only on marketing pages, not the app shell
- Delaying non-critical analytics until
loador after first user interaction
2. Break up long tasks
If your own code is causing long tasks (e.g., processing a massive JSON config, building a huge list, or complex calculations), consider:
- Chunking work with
requestIdleCallbackor smallsetTimeoutbatches - Moving heavy computation to Web Workers
- Rendering lists incrementally (windowing/virtualization)
Example: virtualizing a big list in React using something like react-window instead of dumping 5,000 DOM nodes at once.
3. Defer non-critical hydration and effects
Many frameworks now offer ways to hydrate components lazily or partially. If a widget is below the fold or rarely interacted with, it doesn’t need to fully wire up on first paint.
Combine this with code splitting so each route loads only what it truly needs. Lighthouse won’t tell you how to hydrate, but it will happily confirm when TBT drops after you do.
Watching the metrics improve
After removing one analytics tool, limiting another to marketing routes, and splitting a big initialization task, your Lighthouse run might show:
- TBT: 1500 ms → 500 ms
- Performance score: 55 → 85
More importantly: scrolling and clicking actually feel responsive. The UI isn’t just “loaded,” it’s comfortable to use.
How to read Lighthouse like a performance engineer
Lighthouse throws a lot of jargon at you. Here’s a quick way to decide what to tackle first, based on what the report is screaming about.
When LCP is bad but TBT is fine
This often means:
- Slow network
- Heavy images or video
- Render-blocking CSS or fonts
Focus on:
- Image optimization (format, size, lazy-loading)
- Preloading critical assets (fonts, hero image)
- Reducing render-blocking CSS (critical CSS, code splitting)
When TBT is bad but LCP is okay
Users see something quickly but can’t interact smoothly. That’s usually:
- Too much JavaScript
- Long tasks on the main thread
- Third-party scripts doing too much work
Focus on:
- Code splitting and lazy-loading
- Trimming dependencies
- Deferring non-critical third-party scripts
When CLS is ugly but everything else looks decent
Your layout is jumping around like crazy. Look for:
- Images without dimensions
- Late-loading ads or embeds
- Font swaps or aggressive layout changes
Fix by:
- Reserving space for media and embeds
- Using more stable font-loading strategies
- Avoiding layout shifts in animations and interactions
If you want a neutral reference on performance basics, the MDN Web Docs performance guide is a solid, vendor-agnostic resource.
Using Lighthouse alongside real-user data
One more opinion: never rely solely on synthetic tests.
Lighthouse simulates a mid-range device on a throttled connection. That’s helpful, but it doesn’t know your actual users. Pair it with real-user monitoring (RUM) if you can.
Why it matters:
- Lighthouse might report an LCP of 3 s, but users in rural areas or on low-end phones might see 6–7 s.
- Third-party scripts sometimes behave differently in production than in test environments.
The Web Performance Working Group and related resources at W3C are worth a look if you want more on metrics and standards.
Quick FAQ: the questions dev teams actually ask
Do I need to chase a 100 Lighthouse score?
No. Chasing 100 can push you into weird trade-offs that don’t make sense for your product. Aim for a score that reflects a genuinely good user experience — fast LCP, low TBT, stable layout — and prioritize changes that help real users, not just the metric.
How often should I run Lighthouse on a project?
At least on every major release, and ideally as part of your CI pipeline on key pages. Run it locally when you’re working on performance-heavy features, then compare before/after results to confirm the change actually helped instead of guessing.
Is Lighthouse enough, or do I need other tools?
Lighthouse is a strong starting point, but it’s not the whole story. Pair it with browser DevTools (Performance tab, Network tab) and, if possible, real-user monitoring. Lighthouse tells you what might be wrong; RUM tells you what is slow for actual users.
Why does my score change between runs?
Performance is noisy: network conditions, CPU load, background processes — they all affect results. Use multiple runs and look at trends, not single numbers. Running in a stable environment (CI, headless Chrome) helps reduce that variability.
Should designers care about Lighthouse, or just developers?
Absolutely, designers should care. Choices about imagery, animation, fonts, and layout all affect LCP, CLS, and overall performance. When design and engineering review Lighthouse reports together, you get a site that both looks good and feels fast.
If you want to go deeper into performance fundamentals and browser behavior, Mozilla’s MDN Web Docs and the W3C’s performance resources are solid places to explore. For a broader view on user experience and cognitive load, the usability research at NIDCD (NIH) can be surprisingly relevant when you think about how people perceive “slowness.”
Related Topics
Why Your Site Feels Slow (Even When Your Server Is Fast)
Examples of .NET Debugger Usage in Visual Studio
Angular CLI Debugging Examples
Fiddler HTTP Debugging Examples
Jest Debugging Examples for JavaScript Tests
Debugging React Applications with Developer Tools
Explore More Debugging Frameworks and Tools
Discover more examples and insights in this category.
View All Debugging Frameworks and Tools