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.
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
| # | Check | Tier | Failed in our sample |
|---|---|---|---|
| 1 | Mobile shows the same main content | P0 | 19.7% (<80% of desktop words) |
| 2 | Same title, meta description and robots meta | P0 | 0% |
| 3 | Same canonical | P0 | 1.1% |
| 4 | Same headings (H1) | P0 | 3.2% |
| 5 | Same internal links | P0 | 6.4% (<80% of desktop links) |
| 6 | Same structured data | P0 | 0% |
| 7 | Same hreflang and alternate tags | P0 | not sampled |
| 8 | Googlebot Smartphone can fetch CSS, JS and images | P0 | not sampled |
| 9 | Viewport meta tag present | P1 | 1.1% missing |
| 10 | Pinch-zoom not blocked | P1 | 15.1% |
| 11 | No horizontal scroll (320–412px) | P1 | 5.9% at 390px, 12.2% at 360px |
| 12 | Tap targets ≥ 24px or spaced (WCAG 2.5.8) | P1 | 43.6% |
| 13 | Readable text size | P1 | 13.8% (>10% of text under 12px) |
| 14 | No intrusive interstitials | P1 | consent banners covered a median 46% of the screen |
| 15 | Lazy content loads without interaction | P1 | not sampled |
| 16 | LCP ≤ 2.5s on a throttled mobile load | P2 | 74.6% slower (lab) |
| 17 | CLS ≤ 0.1 on mobile | P2 | 19.4% (lab) |
| 18 | Responsive images (srcset/sizes) | P2 | not sampled |
| 19 | Low main-thread blocking (TBT/INP) | P2 | median TBT 1,615ms (lab) |
| 20 | Mobile-only templates (m-dot, AMP) annotated correctly | P2 | not 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.
5. Same internal links
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
<!-- ✅ 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
<!-- ❌ 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:
#!/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):
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 altParity 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: 16pxinstead. - 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.
See where your site stands
Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.