Skip to content
aviral gupta

Landing Page Speed, Google Ads CPC and Quality Score

Page speed is not a direct Google Ads Quality Score input, but it changes what you pay anyway: landing page experience is rated partly on how fast the page loads, and every visitor who bounces before render is a click you already bought and lost.

Written by Aviral GuptaPublished 9 min read
  • Google Ads
  • Landing Pages
  • Core Web Vitals
  • Conversion Tracking
  • Next.js
Landing Page Speed, Google Ads CPC and Quality ScoreEVERY 100MS OF LCP COSTS A SLICE OF THE NEXT STAGEimpressionclickrender < 2.5sform startlead

Why does a slow landing page cost more per click?

Google bills you at the click. Not at first paint, not at the moment somebody reads your headline — at the click. From then on the money is spent and everything that follows is your half of the transaction. A landing page that takes four seconds to show anything is not a slower version of a good page. It is a page that discards a share of everything you just bought.

Two separate mechanisms connect speed to cost, and they get conflated constantly. The weaker one is Quality Score: Google rates landing page experience, that rating feeds Ad Rank, and Ad Rank affects the price you actually pay. The stronger one is arithmetic: a visitor who leaves before the page renders is a click with a zero attached. The first is worth understanding accurately. The second is where the money actually goes.

This bites harder in the UAE than almost anywhere. UAE average Google Ads cost per click runs 20–40% above global averages (Logicworks, 2026). At those prices, traffic lost before render is not a usability complaint. It is a line item. Most of my landing page work starts with somebody who has been running ads for six months and cannot account for where the budget went.

What does Google Ads Quality Score actually measure?

Quality Score is a 1–10 diagnostic reported at the keyword level. It is assembled from three components, each rated Below average, Average or Above average, and each one is measured relative to other advertisers competing for the same keyword over the same period (Google Ads Help). Precision matters here, because most writing on this topic is imprecise in a direction that flatters whoever is selling something.

The three Quality Score components, and what each one is not.
ComponentWhat Google ratesWhat it is not
Expected click-through rateHow likely your ad is to be clicked when it shows for that keyword, compared with rival advertisersNot your account-wide CTR, and not affected by position
Ad relevanceHow closely the ad text matches the intent behind the keyword that triggered itNot solved by stuffing the keyword into every headline
Landing page experienceRelevant, original content, transparency about your business, ease of navigation, and a page that loads quickly on mobile (Google Ads Help)Not a Core Web Vitals score — your CrUX LCP, INP and CLS figures are not inputs
The reported 1–10 numberA historical diagnostic, aggregated from exact-match query history for that keywordNot the value multiplied into the live auction, which is recomputed per query

The commercial size of the rating is documented. An above-average landing page experience combined with above-average ad relevance correlates with a cost per click around 36% below average (Search Engine Land, 2023). That is correlation, not a lever you pull — advertisers with good landing pages tend to run everything else well too. But the direction is consistent and the spread is wide enough that no media buyer should shrug at it.

A five-stage funnel narrowing from impression to click, to a render under 2.5 seconds, to form start, to lead — with the render stage highlighted as the point where paid traffic is lost.EVERY 100MS OF LCP COSTS A SLICE OF THE NEXT STAGEimpressionclickrender < 2.5sform startlead
Billing happens at stage two. Every stage after it is a place to lose traffic you have already been charged for.

What happens to a paid click on a page that has not painted yet?

This is the part with no ambiguity in it. The click is billed the moment it happens. The visitor then waits. On a mid-range Android phone on a mobile network — which is what a large share of paid traffic in this region actually is — the gap between the tap and the first meaningful pixel is where people leave. Largest Contentful Paint is the metric that describes that gap: the time until the biggest element above the fold has painted, with a documented good threshold of 2.5 seconds at the 75th percentile (web.dev on LCP).

That discrepancy is the single most useful diagnostic available on a paid campaign, and almost nobody checks it. It is also why speed work and tracking work belong in the same conversation. If the measurement layer only starts recording after the point at which you are losing people, every report you read has been filtered through the failure you are trying to find.

How should a paid landing page be structured?

A landing page for paid traffic is not a website page with a form bolted on. It has one audience, one promise and one action, and everything that does not serve those three is weight.

  • One page, one offer. If the campaign sells two things, build two pages. A page that hedges between offers makes the visitor choose before they have decided to buy anything at all.
  • Message match. The H1 repeats the promise in the ad headline, close to word for word. Same noun, same qualifier, same number.
  • No global navigation. Every link out of the page is an exit you paid for. Keep the logo, drop the menu, put the legal links in the footer where they belong.
  • One primary action, repeated. The same call to action above the fold, after the proof, and at the end. Not three different asks competing for the same tap.
  • Proof adjacent to the ask. The reason to trust you sits next to the button, not on a separate page nobody scrolls to.

Message match deserves dwelling on, because it is the cheapest fix on that list and the most commonly skipped. Somebody clicks an ad promising a 24-hour quote and lands on a page headed "Welcome to our solutions". They now spend a second working out whether they are in the right place — a second charged to a budget that is already down one click. Message match is also half of what Google’s ad relevance and landing page experience ratings are actually looking at, so it is the rare change that improves both the rating and the conversion rate at once.

One warning about how message match usually gets implemented. Swapping the headline with JavaScript after paint, driven by a query parameter, makes your LCP element client-rendered and adds a layout shift at the top of the page — all to avoid building a second static page. Build the second static page. In an App Router project each offer is a directory with its own page.tsx, prerendered at build time and served from the CDN.

What should the above-the-fold LCP element be?

The rule is simple and it is broken on most landing pages I audit: the largest element above the fold must already be in the HTML the server returns. Not fetched, not hydrated, not faded in. Present. That means static text or a static image, and never a hero that a client component assembles after the bundle arrives.

The failure modes in Next.js are specific. A hero reading from a client-side data source cannot paint until the JavaScript has downloaded, parsed and run. A headline wrapped in an entrance animation that begins at opacity: 0 cannot be the LCP element until that animation completes, so a 500ms reveal adds 500ms to the number the field data collects. A carousel mounted on the client shows an empty box for the whole hydration window. Each of those is a design decision that quietly re-prices your media buy.

import Image from 'next/image';
import hero from './hero.jpg';

// No searchParams, no cookies(), no dynamic APIs: this route prerenders
// and is served from the edge, so the ad click hits HTML rather than a
// cold function. Confirm it in `next build` output before launch.
export const dynamic = 'force-static';

export default function Page() {
  return (
    <main className="lp">
      <section className="lp__hero">
        {/* Word-for-word the ad headline. Plain server-rendered text:
            no opacity gate, no stagger, no 'use client' above it. */}
        <h1 className="lp__headline">
          Fleet insurance quoted in 24 hours
        </h1>

        <p className="lp__sub">One page, one offer, one action.</p>

        <a className="lp__cta" href="#quote">
          Get the 24-hour quote
        </a>

        <Image
          src={hero}
          alt=""
          /* priority emits <link rel="preload" fetchpriority="high">, so
             the browser starts this download before it finishes parsing
             the stylesheet. Drop it if the H1 is the LCP element — an
             un-prioritised decorative image should not compete. */
          priority
          /* The width this is actually painted at, per breakpoint. Getting
             sizes wrong is the usual reason a "fast" hero fails on mobile:
             the phone downloads the desktop asset. */
          sizes="(max-width: 768px) 100vw, 560px"
          placeholder="blur"
          className="lp__art"
        />
      </section>
    </main>
  );
}
src/app/offers/24-hour-quote/page.tsx — one offer, one static route, LCP element in the HTML.

Verify which element is the LCP candidate rather than assuming: record a load in the Chrome DevTools Performance panel with CPU throttled 4x and the network on Slow 4G, and read the LCP marker — it names the element. priority and sizes on next/image are documented in the next/image reference, and between them they account for most of the LCP wins available on a landing page.

Fonts belong in the same section. Use next/font so the file is self-hosted and preloaded from your own origin; a webfont fetched from a third-party domain adds a DNS lookup, a TLS handshake and a request to the critical path. Subset it to the characters you actually use. Then check what you are really shipping: this site was preloading a 166KB Arabic font on every English page, because next/font emits the preload from the module declaration rather than from CSS usage. No Lighthouse summary surfaces that. You find it by reading the built HTML and the font manifest in .next/.

Which third-party scripts are eating the click you paid for?

Ask what runs before your hero paints. On most landing pages the answer is a tag manager, a consent banner, a live chat widget, a session recorder and an A/B testing snippet — all injected into the head, because that is what every installation guide tells you to do. Each of them was added for a reason and none of them was ever costed against the media budget.

What each common third party costs above the fold, and the cheaper arrangement.
ScriptWhat it costs before paintWhat to do instead
Tag manager containerThe loader plus every tag inside it, executing on the main thread while the hero waitsLoad it after the LCP element has painted, or move the container server-side
Live chat widgetOften 100KB+ of JavaScript and its own webfont, for a bubble nobody opens in the first ten secondsRender a static button; swap in the real widget on first click or after a delay
Consent bannerBlocks paint, then shifts layout when it injects above the contentServer-render the shell with reserved height; load the CMP logic afterwards
Session recorder or heatmapContinuous main-thread work plus a beacon on scroll and mutationSample a fraction of sessions, and switch it off during a paid before/after test
A/B testing snippetAn anti-flicker rule that deliberately hides the page until the script decidesSplit at the route or the edge, so each variant is its own static document
Third-party webfontDNS, TLS and a fetch on the critical path before any text can renderSelf-host with next/font, subset it, keep font-display: swap

The verification method is blunt and it works. Open DevTools, use request blocking to block each third-party origin one at a time, and re-measure LCP on a throttled profile. Anything that moves the number by more than 200ms has to justify itself in front of the media budget. Most cannot. A chat widget producing two conversations a week does not deserve to sit in front of a page you are paying AED 40 a click to reach.

The tag manager is the awkward case, because you genuinely need it — a landing page with no measurement is worse than a slow one. The answer is not removal but scheduling: fire it after the LCP element has painted. Getting that ordering right without breaking the tags is most of what analytics and tracking implementation consists of. The tags have to fire, and they have to fire late.

How do you build the form and the trust signals?

The form is where the page converts or does not, and most of what goes wrong in it is mechanical rather than persuasive. These are the fixes that repay the effort, in order.

  • Ask for the minimum that lets you qualify the lead. Every field is a decision point and a chance to leave. If a salesperson will ask on the call anyway, do not ask on the page.
  • Set the right input type on every field. type="email", type="tel", inputmode="numeric" and the correct autocomplete token change which keyboard opens on a phone and whether the browser can autofill. On mobile this is worth more than any copy change you will make.
  • Validate on blur, not on every keystroke. Telling somebody their email is invalid while they are still typing the domain trains them to ignore your error messages.
  • Reserve space for error messages. An error that appears and pushes the submit button down is a layout shift at the moment of highest intent — and the reason a tap lands on the wrong element.
  • Show submit state and block double submission. Disable the button, keep a visible pending state, and make the success state a real change on the page rather than a toast that has already faded.
  • Never clear the form on a failed submission. Re-render the values server-side. Losing somebody’s typed input after a network error loses the lead outright.

Trust signals have to be real. A licence or registration number, a named person with a photograph, a phone number that gets answered, the terms of the offer written out where they can be read before the form. Invented reviews and borrowed client logos are worse than useless: buyers in this market check, and a claim that collapses under inspection costs more than the space it occupied. If the honest list is short, publish the short list. A single specific true fact outperforms five vague ones.

How do you make sure the conversion is actually recorded?

Everything above is unfalsifiable if the conversion is not recorded reliably. This is the part that speed-improved-conversions case studies quietly skip: the before number and the after number both came out of a tracking setup that was dropping events, at rates nobody established were the same in the two periods.

Two things eat conversions. Consent banners, which stop the browser tag firing at all when a visitor declines or ignores them. And ad blockers plus browser tracking prevention, which block the request to the ad platform’s domain outright. The fix for both is the same shape: send the event from your own server, from your own origin, carrying the same identifier as the browser event so the pair is collapsed into one rather than counted twice.

import {createHash, randomUUID} from 'node:crypto';
import {cookies, headers} from 'next/headers';

export const dynamic = 'force-dynamic';

const PIXEL_ID = process.env.META_PIXEL_ID!;
const CAPI_TOKEN = process.env.META_CAPI_TOKEN!;
const API_VERSION = 'v21.0';

/** Meta expects SHA-256 of the normalised value, as lower-case hex. */
const sha256 = (value: string) =>
  createHash('sha256').update(value).digest('hex');

/** Trim and lower-case before hashing, or the match rate collapses. */
const normaliseEmail = (email: string) => email.trim().toLowerCase();

/** Digits only, country code included, no '+' and no leading zeros. */
const normalisePhone = (phone: string) => phone.replace(/\D/g, '');

export async function POST(request: Request) {
  const {email, phone} = (await request.json()) as {
    email: string;
    phone?: string;
  };

  // Generated ONCE, here, and shared by both transports. It must be a
  // string: a number on one side and a string on the other will not match.
  const eventId = randomUUID();

  const cookieStore = await cookies();
  const headerList = await headers();

  const payload = {
    data: [
      {
        event_name: 'Lead', // identical casing on the browser side
        event_time: Math.floor(Date.now() / 1000),
        event_id: eventId,
        event_source_url: headerList.get('referer') ?? undefined,
        action_source: 'website',
        user_data: {
          em: [sha256(normaliseEmail(email))],
          ...(phone ? {ph: [sha256(normalisePhone(phone))]} : {}),
          // Click and browser ids, so a server event can still be attributed.
          fbc: cookieStore.get('_fbc')?.value,
          fbp: cookieStore.get('_fbp')?.value,
          client_user_agent: headerList.get('user-agent') ?? undefined,
          client_ip_address: headerList
            .get('x-forwarded-for')
            ?.split(',')[0]
            ?.trim()
        }
      }
    ]
  };

  const endpoint =
    `https://graph.facebook.com/${API_VERSION}/${PIXEL_ID}/events` +
    `?access_token=${CAPI_TOKEN}`;

  // Never let a marketing endpoint hold the user's submission open.
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(3000)
  });

  if (!response.ok) {
    console.error('CAPI rejected the event', await response.text());
  }

  // Returned even when CAPI failed: a blocked pixel plus a failed server
  // call must still leave you with a lead in your own database.
  return Response.json({eventId});
}
src/app/api/lead/route.ts — one id generated server-side, sent to the Conversions API, returned to the browser.

The identifier is the entire mechanism. Meta deduplicates on the pair of event_name and event_id: when the Pixel and the Conversions API both report Lead with the same id inside the deduplication window, one is kept and the other discarded. So generate it once on the server and hand it back to the browser rather than letting each side invent its own.

'use client';

declare global {
  interface Window {
    fbq?: (...args: unknown[]) => void;
    gtag?: (...args: unknown[]) => void;
  }
}

export async function submitLead(form: {email: string; phone?: string}) {
  // Server first. This request is same-origin, so it survives the ad
  // blocker that will silently drop the call to the pixel below.
  const response = await fetch('/api/lead', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify(form)
  });

  const {eventId} = (await response.json()) as {eventId: string};

  // Same event name, same string id -> Meta keeps one of the pair.
  window.fbq?.('track', 'Lead', {}, {eventID: eventId});

  // GA4 carries the id too, so the two systems can be reconciled by hand
  // when the numbers disagree — and they will disagree.
  window.gtag?.('event', 'generate_lead', {event_id: eventId});
}
The browser fires second, reusing the id it was given.

On the Google side the equivalent is enhanced conversions: you send hashed first-party data — email, phone, name — alongside the conversion, so Google Ads can match conversions it would otherwise lose to a blocked cookie. The normalisation rules mirror Meta’s: trim, lower-case, SHA-256, and phone numbers in E.164 with the country code before hashing. Consent Mode v2 sits underneath both, and it has to be implemented with the real gtag('consent', ...) API. Pushing a custom event named something like consent_default into the data layer does nothing whatsoever — Google’s tags ignore it, and you get neither the consent signal nor the modelling. I have found that exact mistake in production more than once, including once in my own code.

What should you verify before the campaign goes live?

Run this before the first invoice, not after it. Every step is something you can check yourself in under ten minutes, and each one has caught a live problem for me at least once.

  1. 1

    Confirm the route is static

    Run next build and check the route is marked as prerendered in the output. A landing page that renders per request pays a cold start on the click you already bought.

  2. 2

    Identify the LCP element

    Record a load in the Chrome DevTools Performance panel with CPU throttled 4x and the network on Slow 4G. Read the LCP marker. If it names a skeleton, a spinner or an empty container, nothing else on this list matters yet.

  3. 3

    Prove the headline is in the HTML

    Run curl -s https://your-page | grep -i "<h1". If your headline is not in the raw response, it is client-rendered and your LCP is gated on a JavaScript bundle.

  4. 4

    Read the ad and the H1 side by side

    Same promise, same qualifier, same number. If a stranger cannot tell within one second that the page answers the ad, the message match has failed regardless of how it scores.

  5. 5

    Block third parties one at a time

    Use DevTools request blocking per origin and re-measure LCP after each. Anything costing more than 200ms goes on a list to be deferred, replaced or removed.

  6. 6

    Fill in the form on a real phone

    Check the keyboard that opens for each field, that autofill works, that an error message does not shift the layout, and that a double tap on submit produces one lead rather than two.

  7. 7

    Watch the conversion arrive in both systems

    GA4 DebugView for the browser event, Meta Events Manager Test Events for the server event. Confirm they appear as one deduplicated conversion with two sources, not as two conversions.

  8. 8

    Repeat with an ad blocker on and consent denied

    The browser event will vanish. The server event must still arrive, and the lead must still be in your own database. If it is not, your reporting will silently understate every campaign you run.

  9. 9

    Check the conversion action inside Google Ads

    Confirm it is set as Primary, that the counting setting matches the business meaning, that enhanced conversions is switched on and reporting as recording, and that the campaign is optimising towards this action rather than a legacy one.

  10. 10

    Take a baseline before you change anything

    Note clicks, sessions, LCP at the 75th percentile, form starts and conversions for a full week. Without that, the after number proves nothing and the argument about whether the work paid for itself is unwinnable.

None of this guarantees a cheaper click, and I will not pretend otherwise. What it does is remove the two failures that make paid media unmeasurable — traffic lost before render, and conversions lost in transit — and leave you in a position where a before-and-after actually means something, because the measurement was trustworthy in both periods.

If you are running Google Ads against a page you did not build and cannot see inside, send me the URL and the campaign name. I will tell you what the page is doing to your cost per lead before you spend another month on it. Every project starts with an introductory project call, then a fixed scope and a fixed price. Most of the work I take is for businesses in Dubai and the wider UAE, where the click prices above make the arithmetic obvious; the Core Web Vitals side and the measurement side are the same engagement more often than not. Get in touch.

What should you know about how I work on this?

I build landing pages as engineering work with conversion structure baked in: one page per offer, message match to the ad, a server-rendered element above the fold, a form that behaves correctly on a phone, and conversion tracking verified before launch. I am not a brand studio and I do not sell visual concepts on their own. If you already have a design, I will build and instrument it. If you do not, I will build something plain, fast and legible, which in paid traffic usually outperforms something elaborate.

Conversion rate and bounce behaviour move immediately, because they depend only on what the visitor experiences — you can read them within days at reasonable traffic volume. The Quality Score components move slower, because they are computed from accumulated query history for each keyword, so give them a few weeks of steady spend before drawing conclusions. Do not judge either in the first 48 hours. Take a full week of baseline data before the change and compare like-for-week, holding budget, bids and creative constant, or the comparison proves nothing.

By removing the mechanical failures first, because they are the largest and the cheapest to fix. That means the largest element above the fold present in the server HTML, third-party scripts deferred until after it paints, the H1 matching the ad promise, a form with correct input types and no layout shift on validation, and one call to action instead of three. Only once those are done does copy or design testing tell you anything reliable, because until then you are measuring how many people saw the page rather than how many were persuaded by it.

Yes — specified, implemented and validated, rather than pasted in and hoped for. That includes the event and parameter schema written down before anything is built, Consent Mode v2 implemented with the real gtag consent API rather than a decorative data-layer event, the Meta Pixel paired with the Conversions API and deduplicated on a shared event id, and enhanced conversions with correctly normalised hashed inputs. The deliverable is an event-by-event QA record you can audit yourself, so you are not taking my word for whether the numbers are real.

No. I do not write ads, choose keywords, set bids or manage budgets — that is a different discipline and you should hire someone who does it full time. What I do is everything the click lands on and everything that measures it: the landing page, its speed, its structure, and the tracking layer that decides whether your reporting is trustworthy. I work alongside whoever runs your account. In practice that division is efficient, because most cost-per-lead problems turn out to sit on my side of the line rather than theirs.

You own the ad accounts, the pixel, the GA4 property, the domain and the code — always, and I will not take a project where that is not the arrangement. I work inside your accounts with the access level the task requires and hand it back untouched. The landing pages are mine to build and yours to keep: the repository is yours, deployed to your hosting, with no proprietary layer that stops another developer taking over. Owning your own measurement is the difference between switching agencies and starting again.

Yes, and it is usually necessary rather than optional, because tracking breaks quietly. Platforms change API versions, consent tools update, a new tag gets added by somebody in marketing, and a redesign moves the element a trigger was bound to. What I hand over is documentation of every event and its trigger so your team can maintain it, plus a re-verification pass whenever the site changes materially. A tracking setup nobody re-checks after launch is a tracking setup that is wrong within a few months, and no alarm sounds.

// OPEN TO WORK

Hiring, or building something that needs an engineer?