DEV Community

Usman Sheikh
Usman Sheikh

Posted on • Originally published at brokstack.hashnode.dev

How we hit 100/100 Lighthouse on a real estate site (and why most are stuck at 30-50)

Most Dubai real estate websites I audit score 30-50 on mobile Lighthouse. The category average is bad — heavy image carousels, three different chat widgets, a CRM tracker, an analytics tag, a heatmap script, four font weights from two foundries, and a 6MB hero video that autoplays. Buyers tap, wait, tap back to Property Finder.

We just shipped a brokerage site that scores 100/100/100/100 on mobile, and it wasn't because of any single magic decision. It was a stack of small ones, each costing something I had to be willing to give up.

Live: demo.brokstack.com

This post is the breakdown of the actual decisions, not the "use Next.js, lazy-load images" generic advice you've read 200 times.

The stack, briefly

  • Next.js 15 (App Router, RSC by default)
  • React 19
  • Tailwind CSS v4 (with @theme instead of tailwind.config.js)
  • Vercel for hosting
  • Sanity as the headless CMS (but the front-end never talks to it — see below)
  • Framer Motion for animations (carefully)

Nothing exotic. The discipline is what's exotic.

Decision 1: Static-first, even where it's hard

The default impulse for a real estate site is "agents update listings, so we need SSR or live data." That impulse is wrong for 95% of the page.

Listing data updates roughly hourly. Home page hero, location pages, agent bios, brand content, FAQ — these update weekly at most.

We use Next.js's generateStaticParams + ISR with a 60-minute revalidate for everything except the live property detail pages. Even those have a 5-minute ISR window because nobody is making buying decisions on whether a property listed 2 minutes ago.

What this means concretely: the HTML for every page is pre-rendered and served from Vercel's edge cache. TTFB on the first hit from a cold cache is ~80ms. From a warm cache, ~12ms.

Lighthouse rewards this. So does the user.

Decision 2: Zero client-side JavaScript on the marketing pages

React Server Components let you render an entire page server-side with literally zero JS shipped to the client unless you opt in. I opt in for exactly four components on the home page: the audit form, the testimonial carousel, the WhatsApp button, and a small accordion in the FAQ.

Everything else — hero, location grid, listings preview, footer — is a server component. The JS bundle for the marketing pages is 8KB compressed. The Brokstack-built demo's home page has a total transfer of 47KB compressed including the hero image.

For comparison, the average Dubai brokerage site I audit ships 800KB-1.2MB of JS on initial load. WordPress themes, jQuery, four plugins each loading their own React bundle, a chat widget that pulls in another 200KB.

The trick: write the page in JSX, mark only the genuinely interactive bits with 'use client'. Everything else stays on the server. The cost is that you have to actually think about which interactions need to be on the client vs. which can be a form post.

Decision 3: One font, two weights

I see sites loading Inter Regular, Inter Medium, Inter SemiBold, Inter Bold, plus a display font in Italic and Italic Bold. That's six font file requests, each 80-120KB, blocking text render until they load.

Our site uses Geist (loaded via next/font/google, which inlines the CSS and self-hosts the WOFF2 files) in exactly two weights: 400 and 600. One font family, two files, total ~45KB. next/font handles font-display: swap and preloading automatically.

The visual hierarchy is created with size and color, not weight variation. This is a stylistic constraint, not just a performance one. Most sites with five font weights look worse than sites with two, because the designer is reaching for weight instead of thinking about composition.

Decision 4: Images are not negotiable

Every image on the site is:

  • Served from Vercel's image optimizer
  • Delivered in AVIF (fallback WebP, fallback JPEG)
  • Sized for the exact viewport via sizes attribute
  • Lazy-loaded below the fold via loading="lazy" (this is the default in Next 15)
  • The hero image specifically is preloaded with <link rel="preload"> so it doesn't block LCP

The hero image on demo.brokstack.com is 38KB in AVIF at 1200px wide. The original PNG was 2.4MB.

If you're using next/image and not seeing dramatic size reduction, your sizes attribute is wrong. The sizes attribute tells the browser which image variant to download — if you leave it as the default 100vw, the browser downloads the full-width image even for a thumbnail.

Concrete example: a 200px-wide agent avatar should have sizes="200px", not sizes="100vw". This single attribute change has saved us 80% on image weight for the agent grid.

Decision 5: No third-party scripts on the critical path

This is the one most teams refuse to do, because every department wants their pixel on the home page.

The Brokstack demo loads zero third-party scripts on the marketing pages. No Google Analytics, no Hotjar, no chat widget on initial render, no Facebook pixel, no CRM tag, no Intercom.

Analytics: Vercel Analytics (first-party, no external script).

Chat: WhatsApp button is a single anchor tag pointing to wa.me/<number>. No widget, no JS. Zero requests until tapped.

Conversion tracking: GA4 loaded only after first user interaction (focus, scroll, or click) — same trick we use for Cloudflare Turnstile on the audit form. By the time GA4 loads, Lighthouse has already finished measuring.

This is uncomfortable for the marketing team. "But we need to track funnel events." Fine. Track them after interaction. Lighthouse measures TBT (Total Blocking Time) in the first 5 seconds, before any user has interacted. Move your tracking out of those 5 seconds and the score jumps 20-30 points.

Decision 6: Build vs. runtime CSS

Tailwind CSS v4 has a JIT compiler that generates the exact CSS classes used in the source code at build time. The output for our entire site is 8.2KB of CSS gzipped.

For comparison, a Bootstrap-based site typically ships 150-200KB of CSS. A site with a UI library like Material UI ships 80-120KB of CSS plus the runtime JS.

We never use a single utility class we don't need. There's no PurgeCSS plugin, no manual cleanup. Tailwind v4 scans the source at build time and emits only what's referenced.

The @theme directive in Tailwind v4 also moves the design tokens (colors, spacing, type scale) into CSS variables, which means dark mode and brand variants are just CSS variable swaps — no extra build output.

What I'm NOT doing that you might expect

Not using a CDN for assets — Vercel's edge network already does this.

Not using service workers — they add complexity and modern browsers cache aggressively without them.

Not using HTTP/3 — Vercel does this automatically. I don't touch it.

Not micro-optimizing CSS specificity — Tailwind handles ordering.

Not preloading every route — Next 15 prefetches links in viewport automatically. Custom prefetch logic often hurts more than it helps.

The 100 score is not from doing more. It's from doing less, on purpose, even when stakeholders push back.

The result, measured

  • LCP: 0.8s (target: <2.5s)
  • TBT: 0ms (target: <200ms)
  • CLS: 0 (target: <0.1)
  • FCP: 0.4s
  • Mobile Lighthouse: 100
  • Desktop Lighthouse: 100
  • Accessibility: 100
  • Best Practices: 100
  • SEO: 100

Real-world impact for a brokerage: a Property Finder visitor who taps a Google search result lands on the homepage in under a second on a mid-range Android. They don't bounce. The bounce rate on the demo is 18% vs. industry average ~62% for real estate.

That's the difference between paying AED 8K/month for portal leads and pulling them direct.

If you build for a similar vertical

This isn't real-estate-specific — the same stack and discipline works for any content-heavy site where the primary user action is "find information, then contact someone." Local services, legal practices, dental clinics, B2B SaaS marketing sites.

The hard part is not the technical work. The hard part is convincing the stakeholders to remove the chat widget, kill the heatmap script, and accept that GA4 fires after first interaction. The technical work is two weeks. The political work is months.


I'm Usman Sheikh, founder of Brokstack. We build lead-generation websites for Dubai real estate brokerages — the kind that pull inquiries direct to you instead of feeding the portals. Live demo: demo.brokstack.com. Free audit of your current site: brokstack.com.

Top comments (0)