Mobile SEO Audit Checklist: 20 Checks for Mobile-First Sites

Mobile SEO audit checklist: 20 checks for mobile-first indexing, from content parity to tap targets and throttled LCP, with failure rates from 188 sites.

BugViso

15 min read

A mobile SEO audit checks that the version of your page Google's smartphone crawler sees has the same content, links, metadata and structured data as desktop, and that the page is usable and fast on a phone. Since mobile-first indexing, the mobile render is the page Google indexes. Anything missing on mobile is effectively missing from search.

The tool most people used for this is gone. Google retired Search Console's Mobile Usability report, the Mobile-Friendly Test and its API on 1 December 2023. Mobile problems didn't go away with it. On 9 October 2026 we rendered 188 random homepages on desktop and on a 390px phone profile. 43.6% had tap targets failing WCAG 2.2, 15.1% blocked pinch-zoom, 12.2% scrolled sideways at 360px, and 19.7% showed less than 80% of their desktop text on mobile.

Below are 20 checks in three priority tiers, each with how to test it and how often it failed in our sample, plus a script that runs the parity and usability checks for you.


The Audit Matrix at a Glance

#CheckTierFailed in our sample
1Mobile shows the same main contentP019.7% (<80% of desktop words)
2Same title, meta description and robots metaP00%
3Same canonicalP01.1%
4Same headings (H1)P03.2%
5Same internal linksP06.4% (<80% of desktop links)
6Same structured dataP00%
7Same hreflang and alternate tagsP0not sampled
8Googlebot Smartphone can fetch CSS, JS and imagesP0not sampled
9Viewport meta tag presentP11.1% missing
10Pinch-zoom not blockedP115.1%
11No horizontal scroll (320–412px)P15.9% at 390px, 12.2% at 360px
12Tap targets ≥ 24px or spaced (WCAG 2.5.8)P143.6%
13Readable text sizeP113.8% (>10% of text under 12px)
14No intrusive interstitialsP1consent banners covered a median 46% of the screen
15Lazy content loads without interactionP1not sampled
16LCP ≤ 2.5s on a throttled mobile loadP274.6% slower (lab)
17CLS ≤ 0.1 on mobileP219.4% (lab)
18Responsive images (srcset/sizes)P2not sampled
19Low main-thread blocking (TBT/INP)P2median TBT 1,615ms (lab)
20Mobile-only templates (m-dot, AMP) annotated correctlyP2not sampled

Method. The 188 homepages come from our website accessibility statistics sample (random, Tranco ranks 1,001–50,000). Each was rendered in Chromium at 1366×900 and with BugViso's iPhone 16 Pro profile (390×844, touch, mobile user agent), then resized to 360px for the overflow check. Checks 16, 17 and 19 come from a separate throttled mobile lab run of the same sample (201 homepages; 412×823, 150ms RTT, 1.6 Mbps, 4× CPU slowdown). "Not sampled" means we didn't measure it at scale. Those checks still matter.


Tier P0: Content and Indexing Parity

Google's mobile-first indexing guide is explicit: use the same content, metadata, structured data and robots directives on mobile as on desktop. These checks protect what gets indexed.

1. Same main content

Compare the visible text of the mobile and desktop renders. In our sample, 19.7% of homepages showed less than 80% of their desktop word count on mobile, and 5.3% less than half. Common causes are sections removed with display: none at small breakpoints, "desktop-only" components, and carousels that render one slide instead of all of them.

Content collapsed into accordions or tabs is fine: Google indexes it. Content removed from the mobile DOM isn't. When the visible counts differ, check whether the missing text is in the mobile HTML at all.

2. Same title, meta description and robots meta

These were identical in every homepage we tested, because modern responsive sites share one HTML document. The risk lives in dynamic serving (different HTML by user agent) and separate m-dot sites, where a stray noindex on the mobile template drops pages from the index.

3. Same canonical

1.1% differed. On responsive sites this usually comes from a client-side router that rewrites the canonical differently per breakpoint, or JavaScript that runs only on mobile.

4. Same headings

3.2% had a different number of H1s on mobile, typically a mobile header component with its own <h1> logo, or a desktop hero H1 hidden at small widths. One clear H1 on both renders is the goal.

6.4% exposed less than 80% of desktop links on mobile. Hamburger menus are fine if the links are in the DOM. Menus fetched only when tapped aren't. Lost links mean lost crawl paths and lost internal PageRank. Our guide on footer and sidebar navigation links covers the trade-offs.

6. Same structured data

All 188 homepages served identical JSON-LD to both renders. This fails mainly on m-dot sites and dynamically served templates that drop schema to save bytes.

7. Same hreflang and alternate annotations

International sites must carry identical hreflang sets on mobile, and m-dot sites need rel="alternate"/rel="canonical" pairs between versions.

8. Googlebot Smartphone can fetch everything

If robots.txt blocks the CSS or JavaScript the mobile layout needs, Google renders a broken page. Test with Search Console's URL Inspection tool (Test live URL → View tested page → Screenshot), which uses the smartphone crawler.


Tier P1: Mobile Usability

9. Viewport meta tag

html
<!-- ✅ Required for a responsive layout -->
<meta name="viewport" content="width=device-width, initial-scale=1">

Only 1.1% of homepages lacked it. Without it, mobile browsers lay the page out at a 980px desktop width and zoom out.

10. Pinch-zoom not blocked

html
<!-- ❌ Blocks zoom: fails WCAG 1.4.4 Resize Text and axe's meta-viewport rule -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">

15.1% of homepages blocked zoom with user-scalable=no (9.7%) or maximum-scale=1 (10.8%), often to stop iOS auto-zooming into form fields. The correct fix for that is a form field font size of at least 16px, not disabling zoom. See MDN's viewport reference.

11. No horizontal scroll

5.9% scrolled sideways at 390px and 12.2% at 360px. Test at 320, 360, 390 and 412px. The cause is almost always one element that's too wide, as explained in our guide to fixing horizontal scroll on mobile.

12. Tap targets

43.6% of homepages had at least one target under 24×24px without enough spacing, failing WCAG 2.2 SC 2.5.8. Most failures were text links 16–19px tall, a few pixels of padding from passing. Fixes are in our guide to tap targets that aren't sized appropriately.

13. Readable text

58.0% of homepages rendered some text below 12px, and 13.8% rendered more than a tenth of their text that small. Body copy should be at least 16px on mobile. Legal footers and captions can go smaller, but not below 12px.

14. No intrusive interstitials

Google's guidance penalises pop-ups that cover the main content on arrival, with exceptions for legally required notices like cookie consent. On mobile, the consent banners we detected covered a median 46% of the screen. They're allowed, but cap them at about half the viewport and make dismissing them easy. Newsletter modals that fire on load aren't exempt.

15. Content loads without interaction

Googlebot doesn't scroll or click. It renders with a tall viewport, so loading="lazy" and IntersectionObserver-based loading work, but content that loads only after a tap, swipe or scroll event isn't indexed. Load "read more" text into the DOM and reveal it with CSS.


Tier P2: Mobile Performance

16. LCP on a throttled mobile load

In our throttled lab run, the median homepage's Largest Contentful Paint was 4.6s, and only 25.4% came in at 2.5s or under. Lab numbers are harsher than field data, but the gap between desktop and mobile is real. Our article on why LCP is slow on mobile but fast on desktop explains the mechanics.

17. CLS on mobile

80.6% of homepages stayed at or under 0.1 CLS in the lab run. On mobile, the usual offenders are late-injected banners, ads without reserved space, and web fonts swapping.

18. Responsive images

Serve phone-sized images with srcset and sizes. A 2,400px hero image on a 390px screen wastes bandwidth on the slowest connections.

19. Main-thread blocking

The median Total Blocking Time in our 4×-CPU-throttled run was 1,615ms, which shows how much JavaScript a mid-range phone has to chew through. High TBT predicts poor INP once users start tapping.

20. Separate mobile templates

If you still run an m-dot site or AMP pages, verify the bidirectional annotations, redirects by user agent, and content parity (checks 1–7) for every template pair. Migrating to a single responsive template removes most of this checklist's P0 risk.


Run the Parity and Usability Checks With One Script

This Playwright script renders each URL on desktop and as a phone, compares everything in Tier P0 that can be read from the DOM, and runs the Tier P1 usability checks:

python
#!/usr/bin/env python3
"""mobile_parity.py: mobile-first indexing parity + mobile usability checks for one or more URLs.

Usage:
    pip install playwright && playwright install chromium
    python3 mobile_parity.py https://example.com [https://example.com/product ...]
"""
import asyncio, sys

from playwright.async_api import async_playwright

MOBILE_UA = ("Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) "
             "Chrome/128.0.0.0 Mobile Safari/537.36")
FACTS = r"""() => {
  const q = (s) => document.querySelector(s), attr = (s, a) => (q(s) || {}).getAttribute ? q(s).getAttribute(a) : null;
  const types = [];
  for (const s of document.querySelectorAll('script[type="application/ld+json"]')) {
    try { const walk = (o) => { if (Array.isArray(o)) o.forEach(walk); else if (o && typeof o === 'object') {
            if (o['@type']) types.push([].concat(o['@type']).join('+')); if (o['@graph']) walk(o['@graph']); } };
          walk(JSON.parse(s.textContent)); } catch (e) { types.push('INVALID'); }
  }
  const vp = attr('meta[name="viewport"]', 'content');
  return {
    title: document.title.trim(), description: attr('meta[name="description"]', 'content'),
    robots: attr('meta[name="robots"]', 'content'), canonical: (q('link[rel="canonical"]') || {}).href || null,
    h1: [...document.querySelectorAll('h1')].map(h => h.innerText.trim()).filter(Boolean),
    words: ((document.body && document.body.innerText) || '').split(/\s+/).filter(Boolean).length,
    links: new Set([...document.querySelectorAll('a[href]')].map(a => a.href.split('#')[0])).size,
    imgs_no_alt: [...document.querySelectorAll('img')].filter(i => !i.hasAttribute('alt')).length,
    jsonld: [...new Set(types)].sort().join(', '), hreflang: document.querySelectorAll('link[rel="alternate"][hreflang]').length,
    viewport: vp,
  };
}"""
MOBILE_ONLY = r"""() => {
  const vw = document.documentElement.clientWidth;
  let chars = 0, small = 0;
  const w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
  while (w.nextNode()) { const t = w.currentNode.textContent.trim(), el = w.currentNode.parentElement;
    if (!t || !el || !el.getClientRects().length) continue; chars += t.length; if (parseFloat(getComputedStyle(el).fontSize) < 12) small += t.length; }
  const T = [...document.querySelectorAll('a,button,input:not([type=hidden]),select,textarea,[role=button]')].map(el => [el, el.getBoundingClientRect()])
    .filter(([el, r]) => r.width > 2 && r.height > 2 && el.offsetParent !== null &&
      !(el.tagName === 'A' && getComputedStyle(el).display === 'inline' && [...el.parentElement.childNodes].some(n => n !== el && n.nodeType === 3 && n.textContent.trim())));
  const d = (x, y, r) => Math.hypot(Math.max(r.left - x, 0, x - r.right), Math.max(r.top - y, 0, y - r.bottom));
  const fails = T.filter(([el, r]) => (r.width < 24 || r.height < 24) &&
    T.some(([o, q]) => o !== el && d(r.left + r.width / 2, r.top + r.height / 2, q) < 12)).length;
  return {overflow_px: Math.max(0, document.documentElement.scrollWidth - vw), small_text_pct: chars ? +(100 * small / chars).toFixed(1) : 0,
          tap_fails: fails, targets: T.length};
}"""


async def render(browser, url, mobile):
    opts = dict(viewport={"width": 390, "height": 844}, device_scale_factor=3, is_mobile=True, has_touch=True,
                user_agent=MOBILE_UA) if mobile else dict(viewport={"width": 1366, "height": 900})
    ctx = await browser.new_context(**opts)
    page = await ctx.new_page()
    await page.goto(url, wait_until="load", timeout=45000)
    await page.wait_for_timeout(2000)
    facts = await page.evaluate(FACTS)
    extra = await page.evaluate(MOBILE_ONLY) if mobile else {}
    await ctx.close()
    return facts, extra


def line(ok, label, detail=""):
    print(f"  {'✓' if ok else '✗'} {label:34s} {detail}")
    return ok


async def main(urls):
    all_ok = True
    async with async_playwright() as pw:
        browser = await pw.chromium.launch()
        for url in urls:
            (d, _), (m, x) = await render(browser, url, False), await render(browser, url, True)
            print(f"\n{url}\n  -- parity (desktop vs mobile) --")
            ok = all([
                line(d["title"] == m["title"], "title", "" if d["title"] == m["title"] else f"{d['title'][:40]!r} vs {m['title'][:40]!r}"),
                line(d["description"] == m["description"], "meta description"),
                line(d["robots"] == m["robots"], "robots meta", f"{d['robots']} vs {m['robots']}" if d["robots"] != m["robots"] else (m["robots"] or "")),
                line(d["canonical"] == m["canonical"], "canonical", m["canonical"] or "none"),
                line(d["h1"] == m["h1"], "H1", f"{len(d['h1'])} vs {len(m['h1'])}"),
                line(m["words"] >= 0.8 * d["words"], "visible words (mobile ≥ 80%)", f"{d['words']} vs {m['words']}"),
                line(m["links"] >= 0.8 * d["links"], "unique links (mobile ≥ 80%)", f"{d['links']} vs {m['links']}"),
                line(d["jsonld"] == m["jsonld"], "JSON-LD types", m["jsonld"] or "none"),
                line(d["hreflang"] == m["hreflang"], "hreflang tags", f"{d['hreflang']} vs {m['hreflang']}"),
            ])
            vp = (m["viewport"] or "").replace(" ", "").lower()
            print("  -- mobile usability --")
            ok &= all([
                line("width=device-width" in vp, "viewport meta", m["viewport"] or "MISSING"),
                line(not ("user-scalable=no" in vp or "user-scalable=0" in vp or "maximum-scale=1," in vp + ","
                          or "maximum-scale=1.0" in vp), "pinch-zoom allowed"),
                line(x["overflow_px"] == 0, "no horizontal overflow", f"{x['overflow_px']}px too wide" if x["overflow_px"] else ""),
                line(x["small_text_pct"] <= 10, "text under 12px ≤ 10%", f"{x['small_text_pct']}%"),
                line(x["tap_fails"] == 0, "tap targets (WCAG 2.5.8)", f"{x['tap_fails']} of {x['targets']} fail"),
                line(m["imgs_no_alt"] == 0, "images have alt", f"{m['imgs_no_alt']} missing" if m["imgs_no_alt"] else ""),
            ])
            all_ok &= ok
        await browser.close()
    sys.exit(0 if all_ok else 1)


if __name__ == "__main__":
    asyncio.run(main(sys.argv[1:] or ["https://example.com"]))

Output for a small search-tool homepage from our sample (9 October 2026, domain replaced):

text
https://site-search.example/
  -- parity (desktop vs mobile) --
  ✓ title
  ✓ meta description
  ✓ robots meta
  ✓ canonical                          none
  ✓ H1                                 0 vs 0
  ✓ visible words (mobile ≥ 80%)       206 vs 206
  ✓ unique links (mobile ≥ 80%)        17 vs 17
  ✓ JSON-LD types                      none
  ✓ hreflang tags                      0 vs 0
  -- mobile usability --
  ✓ viewport meta                      width=device-width, initial-scale=1.0,minimum-scale=1.0,maximum-scale=5.0
  ✓ pinch-zoom allowed
  ✓ no horizontal overflow
  ✓ text under 12px ≤ 10%              1.5%
  ✗ tap targets (WCAG 2.5.8)           8 of 27 fail
  ✓ images have alt

Parity checks pass trivially here because there's no canonical, no H1 and no schema on either version. "Consistent" isn't the same as "good", so read the parity section together with your regular on-page audit.


How BugViso Audits the Mobile Version

Every BugViso scan includes a mobile device-emulation pass: the page is loaded in Chromium as an iPhone 16 Pro (390×844, DPR 3, touch, mobile user agent). It checks the viewport meta tag, horizontal overflow (with the furthest-overflowing element), tap targets against WCAG 2.5.8's 24px rule with the spacing exception, targets under the 44px recommendation, and touch-target collisions. It also records mobile Core Web Vitals.

The performance section adds a throttled-network simulation (Slow and Fast 3G profiles) for LCP, so you can see how the page behaves on a weak connection rather than only on the scanner's network. Accessibility runs axe-core with the WCAG 2.0, 2.1 and 2.2 A/AA rule tags, which includes the meta-viewport rule that catches zoom blocking.

BugViso doesn't compare a desktop render with a mobile render field by field, so use the script above for checks 1–8. You can run a free mobile audit with BugViso, and the WCAG accessibility and mobile checks page lists the mobile pass in full.


High-Risk Oversights

  • Testing only one phone width. In our sample, overflow doubled between 390px and 360px.
  • Trusting a desktop crawler for a mobile-first index. Crawl with a smartphone user agent and viewport, or at least spot-check templates in URL Inspection.
  • Hiding content to "simplify" mobile. It works as design, but it's an indexing decision. Collapse content instead of removing it.
  • Blocking zoom to fix iOS input zoom. Set form fields to font-size: 16px instead.
  • Treating cookie banners as harmless. They're exempt from the interstitial guidance, but they still cover half the screen and can shift layout.

FAQ

What is mobile-first indexing?

Google predominantly uses the mobile version of a page, rendered by Googlebot Smartphone, for indexing and ranking. If content, links or structured data exist only on desktop, Google largely doesn't see them.

What replaced Google's Mobile-Friendly Test?

Google retired the Mobile Usability report, the Mobile-Friendly Test and its API on 1 December 2023 and pointed site owners to Chrome's built-in auditing tools. URL Inspection in Search Console still shows how Googlebot Smartphone renders a page. Neither covers this whole checklist, which is why a crawl-based audit is still needed.

How do I check if mobile and desktop content match?

Render both versions and compare the visible text, links, headings, canonical and structured data. The script in this checklist does that per URL. In our sample, about one in five homepages showed less than 80% of its desktop text on mobile.

Does mobile page speed affect rankings?

Core Web Vitals are part of Google's page experience signals and are measured on real mobile users through Chrome's field data. Speed is a modest ranking factor, but slow mobile pages lose visitors regardless.

How often should I run a mobile SEO audit?

After every redesign, template change or new third-party script, and on a monthly schedule otherwise. Mobile regressions, like a new banner, a wider component or a hidden section, are easy to ship and hard to notice on a desktop screen.


Conclusion

Mobile-first indexing means your mobile render is your page: check parity first, then usability, then speed, using the 20 checks above, and let a BugViso scan cover the mobile-usability and performance tiers on every audit.

Found this useful? Share it.

See where your site stands

Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.