Build a free website speed test tool with Google PageSpeed

Website speed is a practical business issue across Australia. A customer checking a menu on a train in Sydney, a shop owner loading product pages on 4G in regional Queensland, and a team browsing over an NBN connection in Melbourne all experience delay differently. A public page speed test turns that technical issue into a simple score and useful measurements for owners who may not understand server response times.

This guide explains how to combine a lightweight browser interface with the Google PageSpeed API. The finished website performance checker accepts a full address, runs a mobile or desktop audit, and presents Lighthouse results such as Largest Contentful Paint, Cumulative Layout Shift, and Total Blocking Time. A server-side function keeps the API key out of visitor source code.

The starter version can remain low-cost for ordinary traffic because Cloudflare Pages and Google offer no-cost access tiers, but quotas and policies can change. Present it as an educational diagnostic, not a ranking guarantee. Field data may also be unavailable for a new or low-traffic Australian site, so laboratory and real-user results must be labelled accurately.

Prepare the project and API access

Create a Cloudflare Pages project with two files: index.html for the interface and functions/api/test.js for the API call. This arrangement keeps the front end static, serves the function and page from one domain, and avoids cross-origin setup. The same design can later move to another static host with minimal changes.

In Google Cloud Console, select or create a project, open APIs & Services, enable PageSpeed Insights API, and create an API key under Credentials. Restrict the key to that API and monitor usage. Review the current project quota and billing conditions before public launch, since Google can adjust free allowances.

Store the value as PAGESPEED_API_KEY in encrypted hosting settings. For local Pages development, use .dev.vars; for production, set the production environment variable in the dashboard. Run npx wrangler pages dev . for local checks. Keep .dev.vars out of version control, and never place the key in browser JavaScript, where a visitor could copy it and exhaust the quota.

Create the test form

Build one focused form around a labelled URL field, a mobile or desktop choice, a submit button, a live status message, and a results panel. Mobile should be the default because many Australian visitors first meet a business on a phone, including users moving between central Sydney and suburban areas over 4G or 5G.

<main>
  <p><strong>Test your website</strong></p>
  <form id="speed-form">
    <label for="site-url">Full website address</label>
    <input id="site-url" name="url" type="url"
           placeholder="https://example.com.au" required>

    <fieldset>
      <legend>Connection profile</legend>
      <label><input type="radio" name="strategy" value="mobile" checked> Mobile</label>
      <label><input type="radio" name="strategy" value="desktop"> Desktop</label>
    </fieldset>
    <button type="submit">Run speed test</button>
  </form>

  <p id="status" role="status" aria-live="polite"></p>
  <section id="results" hidden>
    <p>Performance score: <strong id="score">—</strong></p>
    <dl>
      <dt>Largest Contentful Paint</dt><dd id="lcp">—</dd>
      <dt>Cumulative Layout Shift</dt><dd id="cls">—</dd>
      <dt>Total Blocking Time</dt><dd id="tbt">—</dd>
      <dt>Speed Index</dt><dd id="speed-index">—</dd>
    </dl>
  </section>
</main>

Require a complete address with http:// or https://, then validate it both in the browser and at the server. Limit the input length, reject embedded credentials, and reject localhost or local-network names. These checks reduce malformed requests and prevent the public function from becoming a general-purpose API relay.

Keep the layout narrow, make the button easy to tap, and reserve space for results so the page does not jump while data loads. For Australian small businesses and online stores, a .com.au placeholder such as example.com.au makes the tool feel locally relevant without implying that only Australian domains can be tested.

Connect the PageSpeed API

The PageSpeed Insights v5 endpoint accepts the target URL, a strategy of mobile or desktop, and your server-held key. This Pages Function validates the request, forwards only controlled values, and reduces the large response to the fields needed by the interface. The visitor never receives the API key.

export async function onRequestPost({ request, env }) {
  const body = await request.json().catch(() => ({}));
  const raw = String(body.url || "").trim();
  let target;

  try {
    target = new URL(raw);
  } catch {}

  const blocked = target?.hostname === "localhost" ||
    target?.hostname.endsWith(".local");

  if (!target || raw.length > 2048 ||
      !["http:", "https:"].includes(target.protocol) ||
      target.username || target.password || blocked) {
    return Response.json(
      { error: "Enter a valid public website URL." },
      { status: 400 }
    );
  }

  if (!env.PAGESPEED_API_KEY) {
    return Response.json(
      { error: "The server API key is missing." },
      { status: 500 }
    );
  }

  const endpoint = new URL(
    "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
  );
  endpoint.searchParams.set("url", target.href);
  endpoint.searchParams.set(
    "strategy",
    body.strategy === "desktop" ? "desktop" : "mobile"
  );
  endpoint.searchParams.set("category", "performance");
  endpoint.searchParams.set("key", env.PAGESPEED_API_KEY);

  const response = await fetch(endpoint);
  if (!response.ok) {
    const message = response.status === 429
      ? "The free API quota has been reached. Try again later."
      : "Google could not test this website.";

    return Response.json(
      { error: message },
      { status: response.status === 429 ? 429 : 502 }
    );
  }

  const data = await response.json();
  const lighthouse = data.lighthouseResult;
  const audits = lighthouse?.audits || {};
  const value = (id) => audits[id]?.numericValue ?? null;
  const score = lighthouse?.categories?.performance?.score;

  return Response.json({
    score: typeof score === "number" ? Math.round(score * 100) : null,
    metrics: {
      lcp: value("largest-contentful-paint"),
      cls: value("cumulative-layout-shift"),
      tbt: value("total-blocking-time"),
      speedIndex: value("speed-index")
    },
    fieldRating: data.loadingExperience?.overall_category || null
  });
}

The endpoint is fixed inside the function, so users cannot turn it into a proxy for arbitrary services. The browser receives a score, four lab metrics, and an optional field-experience category. Avoid saving raw API payloads in local storage; store results only if a later feature genuinely needs them and the retention policy is clear.

Convert the Lighthouse performance score from zero-to-one to a familiar percentage. Keep timing metrics in milliseconds and layout shift as a decimal. Do not fabricate Interaction to Next Paint from this laboratory response: INP comes from field measurements, while PageSpeed can return a separate field category when a site has enough traffic.

Render useful performance results

On the client, post the validated address and selected profile to /api/test, then populate the result elements with textContent. This avoids inserting a submitted domain as HTML. Keep the status message visible during a slow test, and convert an API error into readable text instead of leaving an empty results box.

const form = document.querySelector("#speed-form");
const status = document.querySelector("#status");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  status.textContent = "Running the test…";

  try {
    const target = new URL(form.elements.url.value);
    const strategy = form.elements.strategy.value;

    const response = await fetch("/api/test", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ url: target.href, strategy })
    });

    const data = await response.json();
    if (!response.ok) {
      throw new Error(data.error || "Test failed.");
    }

    document.querySelector("#score").textContent = data.score ?? "—";

    const fields = {
      lcp: "lcp",
      cls: "cls",
      tbt: "tbt",
      "speed-index": "speedIndex"
    };

    for (const [id, key] of Object.entries(fields)) {
      const value = data.metrics[key];
      document.querySelector(`#${id}`).textContent =
        value == null ? "—" :
        `${Math.round(value * 100) / 100}${id === "cls" ? "" : " ms"}`;
    }

    document.querySelector("#results").hidden = false;
    status.textContent = "Test complete.";
  } catch (error) {
    status.textContent = error.message;
  }
});

Add a one-line explanation beside each number. LCP describes when the main content appears, CLS captures unexpected movement, TBT estimates main-thread blocking, and Speed Index shows how quickly the visible page fills. This language turns a technical report into a practical website performance checker for owners, creators, and online sellers.

Let users compare mobile and desktop runs because the results can differ sharply. Google’s good Core Web Vitals reference points are LCP at 2.5 seconds or less, CLS at 0.1 or less, and INP at 200 milliseconds or less. Keep field INP separate from lab TBT, and describe an improved score as progress rather than a promise of higher search visibility.

Protect users and control API use

Australian privacy law matters if you log submissions or retain test history. Under the Privacy Act 1988 and the Australian Privacy Principles, collect only what the service needs, explain what you retain, and secure it. A URL and timestamp may be innocuous, while IP addresses and account details can be personal information. An API key is a secret, not an analytics dataset.

If you add email reports, the Spam Act 2003 requires consent, a genuine business purpose, sender identification, and a working unsubscribe route. The Australian Consumer Law also applies to claims that the tool is free, guaranteed to improve a score, or backed by a particular Google quota. If you charge for an extension, display prices in Australian dollars and include GST where required.

Add rate limiting, a Content Security Policy, and strict output encoding because a public checker accepts untrusted input. Do not retain personal submissions or expose raw upstream errors. Display attribution such as “Data provided by Google PageSpeed Insights,” direct users to the relevant Google result, and state that the independent tool is not affiliated with Google.

Launch, promote, and improve the tool

Deploy the Pages project, connect a custom domain, and set the production secret before sharing the URL. Test on a real phone, run both profiles against a small Australian site, and confirm that the function returns valid JSON. Check browser source to ensure the key is absent, then simulate a quota failure to confirm that visitors receive a friendly message.

Invite testers from different locations rather than judging the tool from one office connection. A page that feels instant on Melbourne’s NBN network may still wait on mobile data in regional Western Australia. Checks from Sydney, Brisbane, Perth, and Adelaide across fixed broadband, 4G, and 5G can expose oversized images, slow third-party scripts, and inconsistent caching.

Build a resource page around the checker, publish supporting guides on Core Web Vitals, and offer an optional detailed report without hiding the basic result. The broader practical advice on website tutorials can help you explain findings to clients and develop useful web maintenance or content services. Start today by creating the project, enabling the API, and publishing a mobile-first version that gives every visitor a clear score, plain-language metrics, and an actionable next step.