na.
← Back to Blog

The 1.8-Second Rule: Budget Breakdown for Local Service Sites

Where every millisecond goes in a fast-loading local service site—fonts, images, scripts, and what to cut first when you're over budget.

By Nabeel Ahmed11 min read
#Performance#Web Vitals#Budget#Optimization

Your client's HVAC site takes 4.2 seconds to load. You know it needs to be under 2 seconds. But when you open DevTools and stare at the waterfall, you see 47 requests, 2.3MB transferred, and no obvious place to start.

Where do you cut?

Most performance guides dump a checklist at you: optimize images, defer scripts, preload fonts, enable compression. All true. None of it tells you how much time each thing is costing you or which fix buys you the most speed.

Here's the truth: a fast local service site has a performance budget, and every asset you add spends from that budget.

This post breaks down where every millisecond goes in a real 1.8-second site, so you know exactly what to cut when you're over budget.

The 1.8-Second Target

Why 1.8 seconds?

  • Google's LCP threshold is 2.5 seconds. You want headroom for slower devices and networks.
  • Bounce rate climbs sharply after 2 seconds. At 1.8 seconds, you're still in the safe zone.
  • It's achievable without heroics. You don't need edge functions or exotic optimizations. Just discipline.

Here's how a well-built local service site spends its 1.8-second budget:

| Phase | Time Budget | What Happens | |-------|-------------|--------------| | DNS + Connection | 200ms | Browser resolves domain, opens TCP, does TLS handshake | | Server Response (TTFB) | 300ms | Server processes request, returns HTML | | HTML Parse | 100ms | Browser builds DOM from HTML | | Critical CSS | 150ms | Inline or preloaded styles render above-fold content | | Fonts | 200ms | Display font loads and renders (or swap happens) | | Hero Image | 600ms | LCP element (usually hero image) loads and paints | | Interactivity (FCP→TTI) | 250ms | JS hydrates, page becomes interactive | | Total | 1.8s | Largest Contentful Paint complete |

That's your budget. Every asset you add either fits within one of these buckets or pushes you over.

Breaking Down Each Budget Line

Let's go deeper into what each phase costs and how to keep it under budget.

DNS + Connection: 200ms

What it is: The time it takes for the browser to look up your domain's IP address, open a TCP connection, and complete the TLS handshake.

What controls it:

  • Your DNS provider's speed (Cloudflare DNS is fast, GoDaddy's default is not)
  • CDN vs origin server (CDN edge nodes are geographically closer)
  • HTTP/2 or HTTP/3 (reduces connection overhead)

How to stay under budget:

  • Use a fast DNS provider (Cloudflare, Route 53, Vercel DNS)
  • Serve from a CDN (Vercel, Cloudflare, Netlify)
  • Enable HTTP/3 if your host supports it

Red flags:

  • Hosting on a $5/month shared host in a single data center
  • Not using a CDN for static assets
  • Multiple domains in the critical path (fonts from Google, images from WordPress, scripts from Facebook)

Server Response (TTFB): 300ms

What it is: Time to First Byte—how long until the server sends back the first byte of HTML.

What controls it:

  • Static generation vs server-side rendering vs client-side rendering
  • Database queries and API calls
  • Server location and compute power

How to stay under budget:

  • Static generation wins. Pre-render at build time (Next.js generateStaticParams, Astro, Eleventy). TTFB drops to 50-100ms.
  • If you must use SSR, cache aggressively and keep it near your users (edge functions, CDN cache).
  • Avoid client-side rendering for local service sites. There's no reason a plumber's homepage should wait for a React bundle before showing content.

Red flags:

  • WordPress with no caching plugin
  • Fetching data from a slow CMS on every request
  • Running SSR on a single-region server far from your users

HTML Parse: 100ms

What it is: The browser reads the HTML and builds the DOM.

What controls it:

  • HTML size (bigger = slower)
  • Inline scripts that block parsing
  • Render-blocking stylesheets and scripts in <head>

How to stay under budget:

  • Keep HTML under 50KB (compressed)
  • Inline critical CSS, defer everything else
  • Move non-critical scripts to the bottom or mark them async/defer

Red flags:

  • 200KB of HTML because you inlined the entire design system
  • Render-blocking analytics scripts in <head>
  • Huge inline JSON blobs for client-side hydration

Critical CSS: 150ms

What it is: The CSS needed to render above-the-fold content. Everything else can load later.

What controls it:

  • How much CSS you inline vs load externally
  • Whether the external CSS is render-blocking

How to stay under budget:

  • Inline critical CSS (usually 8-15KB) in <head>
  • Load non-critical CSS asynchronously
  • Use a tool like critical (npm package) or Next.js automatic CSS splitting

Red flags:

  • Loading a 200KB Tailwind bundle before any content renders
  • Using @import in CSS (blocks rendering until imported sheet loads)
  • External CSS file with no preload

Fonts: 200ms

What it is: The time it takes to load and render custom web fonts.

What controls it:

  • Font file size (WOFF2 is smallest)
  • Font loading strategy (font-display: swap vs block)
  • Whether fonts are preloaded

How to stay under budget:

  • Preload the primary font file (<link rel="preload" href="/fonts/display.woff2" as="font" type="font/woff2" crossorigin />)
  • Self-host fonts instead of loading from Google Fonts (saves a DNS lookup and connection)
  • Subset fonts (remove unused glyphs—you don't need Cyrillic for a Texas plumber)
  • Use font-display: swap so text renders immediately with the fallback, then swaps when the custom font loads

Alternative: skip custom fonts entirely. System fonts (-apple-system, BlinkMacSystemFont, Segoe UI) are instant and look native. Zero CLS, zero load time.

Red flags:

  • Loading 6 font weights when you only use 2
  • Using Google Fonts without preconnect
  • Not preloading the font used in the hero headline

Hero Image: 600ms

What it is: The time it takes to load the Largest Contentful Paint (LCP) element, which is usually the hero image.

What controls it:

  • Image file size
  • Image format (WebP is smaller than JPEG)
  • Whether the image is prioritized (fetchpriority="high")
  • CDN vs origin server

How to stay under budget:

  • Optimize the hero image aggressively. Target 100-200KB for a full-width hero.
  • Use WebP with a JPEG fallback (or AVIF if you're feeling fancy)
  • Size it correctly. Don't serve a 3000px-wide image when the viewport is 1920px.
  • Prioritize it. Use Next.js priority prop or fetchpriority="high" in plain HTML.
  • Serve it from a CDN with proper caching headers.

Red flags:

  • 3MB JPEG uploaded directly from the client's phone
  • Image hosted on WordPress media library with no optimization
  • No width/height attributes (causes CLS)

Interactivity (FCP → TTI): 250ms

What it is: The time from First Contentful Paint (when something first renders) to Time to Interactive (when the page can respond to user input).

What controls it:

  • JavaScript bundle size
  • How much JS runs during hydration
  • Third-party scripts (analytics, chat widgets, ads)

How to stay under budget:

  • Ship less JavaScript. Use server components where possible.
  • Defer non-critical scripts. Analytics and chat widgets can load after the page is interactive.
  • Use code splitting. Don't load the contact form JS until the user scrolls to the contact section.
  • Avoid blocking the main thread. Break up long tasks, use Web Workers for heavy computation.

Red flags:

  • 400KB React + GSAP + Three.js bundle on a plumber's homepage
  • Loading Facebook Pixel, Google Analytics, Hotjar, and a live chat widget all in <head>
  • No code splitting—every page loads the entire app bundle

Real Example: Anatomy of a 1.8-Second HVAC Site

Here's a real breakdown from one of the sites in my portfolio:

Ridge Air Co. (San Antonio HVAC)
Target: 1.8s LCP
Actual: 1.7s (median, real-world mobile users)

| Asset | Size | Time | Notes | |-------|------|------|-------| | HTML | 22KB | 280ms TTFB | Static generation via Next.js | | Critical CSS (inline) | 11KB | +40ms | Above-fold styles only | | Display font (Fraunces WOFF2, subset) | 48KB | +160ms | Preloaded, font-display: swap | | Hero image (WebP) | 187KB | +580ms | Optimized, prioritized, CDN-served | | Body font (Geist Sans WOFF2) | 32KB | +90ms | Loads async, non-blocking | | Remaining CSS | 18KB | +50ms | Deferred, loaded async | | JS bundle (hydration) | 68KB | +180ms | Code-split, minimal client JS | | Third-party scripts | 42KB | +240ms | Deferred (analytics, schema) | | Total LCP | 428KB | 1.7s | Under budget ✓ |

Key decisions:

  • Static generation dropped TTFB from 600ms to 280ms
  • Preloading the display font saved 300ms
  • Hero image optimization (3.2MB JPEG → 187KB WebP) saved 2.1 seconds
  • Deferring analytics saved 180ms on TTI

What to Cut When You're Over Budget

You've built the site. You run Lighthouse. LCP is 3.4 seconds. Now what?

Here's the triage order—cut from the top until you're under budget:

1. Hero Image (Biggest Win)

If your hero image is over 300KB, fix it first. This is almost always the LCP element, so optimizing it has the biggest impact.

  • Run it through an image optimizer (Squoosh, ImageOptim, SharpJS)
  • Convert JPEG → WebP
  • Serve responsive sizes (don't send a 3000px image to a 375px phone)

Expected savings: 500ms - 2s

2. Third-Party Scripts (Easiest Win)

Every third-party script is a tax on performance. Audit them ruthlessly.

  • Analytics: Defer loading until after LCP
  • Chat widgets: Load on user interaction (click "Chat" button)
  • Social pixels: Ask yourself if you actually need them

Expected savings: 300ms - 800ms

3. Fonts (Quick Win)

Custom fonts are expensive. Either optimize them or remove them.

  • Preload the primary font
  • Subset fonts (remove unused characters)
  • Consider system fonts for body text

Expected savings: 200ms - 500ms

4. JavaScript Bundle (Hard Win)

Smaller JS bundle = faster interactivity.

  • Remove unused libraries (do you really need Lodash?)
  • Code-split by route and component
  • Use server components instead of client components

Expected savings: 200ms - 600ms

5. Hosting and TTFB (Infrastructure Win)

If TTFB is over 600ms, your hosting is the problem.

  • Switch to static generation if possible
  • Use a CDN
  • Upgrade from shared hosting to a modern platform (Vercel, Netlify, Cloudflare Pages)

Expected savings: 300ms - 1s

Measuring Your Budget Spend

Use these tools to see where your budget is going:

WebPageTest

Best for: Detailed waterfall view showing exactly what loaded when.

Run a test on a throttled mobile connection (3G or slow 4G). Look at:

  • TTFB (should be under 300ms)
  • LCP element (should be under 2.5s)
  • Total blocking time (should be under 300ms)

Chrome DevTools Performance Tab

Best for: Understanding main thread activity and JavaScript execution time.

Record a page load. Look at:

  • Long tasks (anything over 50ms blocks interactivity)
  • Layout shifts (anything that moves the page while loading)
  • Paint timing (when content first appears)

Lighthouse

Best for: Quick pass/fail on Core Web Vitals.

Run it in Chrome DevTools. Focus on:

  • LCP (should be green, under 2.5s)
  • TBT (Total Blocking Time, should be under 300ms)
  • CLS (should be under 0.1)

Real User Monitoring (Optional)

Tools like Vercel Analytics, Cloudflare Web Analytics, or Google Search Console show real-world performance from actual users, not lab tests.

Use this to catch regressions after launch.

How to Enforce a Performance Budget

Measuring is pointless if you don't enforce it. Here's how to keep future changes from blowing the budget:

1. Set Thresholds in CI/CD

Use Lighthouse CI to fail the build if performance drops below your threshold.

# .github/workflows/lighthouse.yml
- name: Run Lighthouse CI
  run: |
    npm install -g @lhci/cli
    lhci autorun --collect.numberOfRuns=3 --assert.assertions.lcp=2500

2. Track Bundle Size

Use bundlesize or size-limit to fail the build if the JavaScript bundle grows too large.

// package.json
"bundlesize": [
  {
    "path": "./out/_next/static/chunks/*.js",
    "maxSize": "80kb"
  }
]

3. Audit Third-Party Scripts Quarterly

Every few months, review what's still loading on your site. Remove anything that's not actively used.

Final Thought

Performance isn't a feature you add at the end. It's a budget you manage from the start.

Every asset you add—every font, every script, every image—spends from that budget. If you don't track it, you'll go over budget without noticing. Then your client's competitor gets the call instead.

The 1.8-second rule is simple:

  1. Know your budget (1.8 seconds = ~400KB of critical assets)
  2. Measure what you ship (Lighthouse, WebPageTest, DevTools)
  3. Cut the biggest offenders first (hero image, third-party scripts, fonts)
  4. Enforce it in CI/CD (so it doesn't regress)

Your client doesn't need a redesign. They need a site that loads fast enough to keep visitors from bouncing.

Fix the budget. Watch the phone ring.