← Back to Blog

Modern browser features for small business sites: six measured, two to skip

· 50 min read Modern browser features for small business sites: six measured, two to skip

On September 29, 2026, I sat down with six browser features that keep turning up in performance advice, and one question about a small motel group's booking sites: which of these would actually help a guest book a room on a phone? The six are the Speculation Rules API, cross-document view transitions, Interaction to Next Paint (INP) work that defers chat and accessibility widgets, the native <dialog> element and the popover attribute, fetchpriority on the hero image, and precise autofill tokens with passkeys. The next day I measured each one on the live sites, on an emulated mid-range phone with analytics blocked, and tried every change I was considering in my test browser before recommending it. The results didn't match the advice. The priority hint everyone recommends had nothing to do, because the largest thing on those phone screens was a paragraph of text. The page transition cost about a quarter second of dead taps on every page change. A page Chrome prerendered and nobody opened fired a Meta Pixel PageView anyway. And the plainest item on the list, autofill tokens, turned up a spam trap that Chrome can fill in along with the real fields, which is all it takes for the form service to throw a real message away.

The calls, in the order I'd make them

  • Autofill tokens: adopt. They're values such as email or tel in a field's autocomplete attribute that tell the browser which saved detail goes in which box. Every browser supports them, and valid ones are how a field meets WCAG 1.3.5. Checking them is also how I found a spam trap Chrome fills in, which can make a form service throw a real message away without a trace. Passkeys, which ride on the same attribute: skip unless your site has a sign-in.
  • Speculation rules: adopt carefully. They let the browser load a page before the visitor taps its link. Use prefetch, which downloads only the next page's HTML; a prefetch rule made the booking page paint 136 ms sooner in Chrome. Skip prerender, which runs the whole page in a hidden tab, unless every tag on your pages waits until the page is shown: it ran the Meta Pixel and Microsoft Clarity on pages nobody opened, counting visits that never happened.
  • fetchpriority: adopt, on one image, once you've confirmed an image is your Largest Contentful Paint (LCP) element, the largest image or block of text on the first screen. It asks the browser to fetch that image at high priority. Here it had nothing to speed up; smaller image files did.
  • Native dialog and popover: adopt for anything new. They're the browser's built-in pop-up panels. The popover opened no faster than the hand-built panel it would replace. The real trouble was a fixed bottom bar that covered the chat button on every phone and hid keyboard focus; two lines of CSS fixed the focus.
  • INP and deferring widgets: adopt carefully, after you read your field data, the measurements from real visitors. INP measures how quickly a page answers taps, clicks and key presses, judged by the slowest ones. Deferring small widgets bought nothing; deferring a map far down the page did.
  • Cross-document view transitions: skip them on pages people use to get something done. They fade one page into the next. No faster paint, dead taps while each new page faded in, and the fade ignored the phone's reduce motion setting.

How I tested

I took every measurement on September 30, 2026, on the group's live sites, read-only, in Chrome 146 and 148 driven by Puppeteer: a 412 by 915 pixel screen at a 2.625 pixel ratio, the CPU slowed 4x, and a Fast 4G network profile (Slow 4G where I say so). Analytics and ad requests were blocked and no form was submitted, so none of my visits reached the group's analytics or inbox. I tried each change I describe by serving the edited file to my test browser only. Nothing here had shipped on those sites as of that date: this is what I measured and recommended, not a changelog.

Every number a call rests on was checked by a second, separate set of scripts, which re-measured the first pass and added tests it had missed, such as the delay before a new page accepts a tap and the tablet runs; where the two disagreed I use the second. An A/A run with identical pages on both sides moved LCP by up to about 70 ms, so I don't treat a smaller difference as an effect unless it held in nearly every paired round. Two lab limits apply throughout. Desktop Chrome with a phone profile doesn't run Android's viewport heuristics, so the moderate speculation setting explained below fired at the touch in my runs; on an Android phone it can fire earlier. And DevTools throttling shares bandwidth evenly between requests and adds its latency to the response body, which makes priority hints look weaker than they can be on a congested real connection.

Autofill tokens: adopt. Passkeys: only if you run a sign-in

The autocomplete attribute takes tokens from a fixed list in the HTML standard: name, given-name, family-name, email, tel, street-address, postal-code, organization, bday, one-time-code, username, current-password, new-password and more, optionally preceded by a section- name and shipping or billing, with webauthn allowed last (HTML standard). The right token puts the right saved value in the right box and tells assistive technology what the field is for, which is what WCAG 2.2 success criterion 1.3.5, Identify Input Purpose, asks for at level AA (W3C). In Chrome's study of thousands of address and payment forms on top US sites, people who used autofill abandoned forms 75% less often and spent about 35% less time filling them in; Chrome calls that a correlation, not proof of cause (Chrome for Developers, December 17, 2024).

A small contact form, done right:

<form action="/contact" method="post">
  <label for="c-name">Full name</label>
  <input id="c-name" name="name" autocomplete="name">

  <label for="c-email">Email</label>
  <input id="c-email" name="email" type="email" autocomplete="email">

  <label for="c-tel">Phone</label>
  <input id="c-tel" name="phone" type="tel" autocomplete="tel">

  <label for="c-street">Street address</label>
  <input id="c-street" name="street" autocomplete="address-line1">

  <label for="c-zip">ZIP code</label>
  <input id="c-zip" name="zip" autocomplete="postal-code">

  <!-- honeypot: hidden from people, autofill and screen readers; bots still see it in the HTML -->
  <p class="hp" hidden>
    <label for="c-hp">Leave this empty</label>
    <input id="c-hp" name="hp_field" tabindex="-1" autocomplete="off">
  </p>

  <button type="submit">Send</button>
</form>

For a one-time code, web.dev uses a text field with a numeric keyboard, because type="number" can drop leading zeros (web.dev):

<input type="text" inputmode="numeric" autocomplete="one-time-code" pattern="\d{6}" required>

As of September 30, 2026, MDN's compatibility data (browser-compat-data 8.1.3) has the attribute in every browser (MDN compatibility data), one-time-code in Chrome 93 (Chrome for Android 84), Firefox 109 and Safari 12, and the webauthn token in Chrome 108, Firefox 122 and Safari 16.4 (token support).

Chrome filled the spam trap. One of the group's sites guards its forms with a honeypot: a text field pushed off-screen with CSS, marked autocomplete="off", with the word company in its name. With a made-up profile that held a company name, an autofill started from the name field put the company into the trap on 6 of 6 forms, in Chrome 146 and Chrome 148; DevTools showed Chrome had classified the box as a company name field, off or not. A profile with no company left it empty (0 of 2). The form service behind those forms quietly discards any submission whose trap holds a value and doesn't list it with the spam, so the visitor's message just disappears. Adding the hidden attribute to the trap's wrapper stopped it (0 of 6) while name, phone and email still filled, and it removed a bug I hadn't gone looking for: the off-screen field was a focusable text box, exposed to screen readers as "Do not fill this in".

The limits: it takes a visitor accepting a Chrome suggestion from a profile that holds a company, I showed it through the DevTools fill path rather than on a phone and only in Chrome, not Safari, and I don't know how many real visitors have a company saved.

One tap filled five fields. An autofill started from the booking form's name field in Chrome 148 filled 5 of its 18 fields: name, phone, email and street address through their tokens, plus the employer box, which Chrome guessed from its label. The stay dates, drop-downs, date of birth, emergency contact and typed signature stayed empty, and adding the missing tokens didn't change the count. Chrome gave a date input marked bday no autofill type at all, so a date-of-birth picker gets no help from Chrome's autofill whatever the markup says. DevTools fills without showing the suggestion menu, so this is what gets filled, not what Chrome offers.

The emergency contact got the guest's own number. With no attribute, with off and with an invented token, Chrome filled the visitor's own phone number into the emergency contact box whenever a fill started there (3 of 3). A section-emergency token kept the box out of the main group but still filled the visitor's number. The real defense is a plain label and a check at the receiving end.

Phone numbers arrive in two shapes. Chrome wrote a saved US number as 15555550123 into a field marked tel and as 5555550123 into one marked tel-national. For a non-US number, tel kept the country code and tel-national dropped it. So tel is the safer token when some visitors come from abroad, and whatever reads the number should compare digits only.

My call on tokens: adopt. Give every field that asks about the visitor a valid token. Don't fight autofill with off or invented values, and hide spam traps properly instead of parking them off-screen under a name that looks like a real field.

  • autocomplete="off" is not an off switch: MDN says that in most browsers it doesn't stop a password manager from filling sign-in fields, and in my tests Chrome ignored it on the spam trap and the emergency contact box.
  • Made-up values: nope, chrome-off or phone are not tokens. MDN says invented values break autofill, and the field then has no purpose for WCAG 1.3.5. Use tel, not phone, and postal-code, not zip.
  • Spam traps: hide the wrapper with the hidden attribute or display: none, not by moving it off-screen, and find out what your form service does with a filled trap. To test your own form, save an address that includes a company name in Chrome, fill the form from its name field with autofill, send it, and check that it arrives.
  • Autofill needs a real form: browsers may require a name or id, a surrounding <form> and a submit button (MDN).

Passkeys ride on the same attribute. A passkey signs someone in with the device's screen lock (a fingerprint, their face or a PIN) through the WebAuthn API. With what's called conditional UI, a username webauthn token plus a pending navigator.credentials.get() call with mediation: 'conditional' lists saved passkeys in the username field's autofill menu next to saved passwords, so one form serves both kinds of users (web.dev):

<input type="text" name="username" autocomplete="username webauthn">
const passkeyAbort = new AbortController(); // call passkeyAbort.abort() before any other WebAuthn call

async function offerPasskeys(optionsFromServer) {
  if (!window.PublicKeyCredential || !PublicKeyCredential.isConditionalMediationAvailable) return;
  if (!(await PublicKeyCredential.isConditionalMediationAvailable())) return;
  const credential = await navigator.credentials.get({
    publicKey: optionsFromServer, // from your server: a fresh challenge every time, as bytes, not a string
    signal: passkeyAbort.signal,
    mediation: 'conditional'
  });
  // send credential to your server, which checks the signature against the stored public key
}

The page is the easy half. The server has to create a fresh challenge for every sign-in attempt, store each user's public key, check the returned signature and use a relying party ID (rpId) that exactly matches the one used at registration, all over HTTPS (MDN). Support isn't the obstacle: Web Authentication has been Baseline widely available since March 7, 2024, from Chrome 67, Firefox 60 and Safari 13 (webstatus.dev), the conditional check has worked across browsers since October 2023 (MDN), and passkey autofill runs in Chrome 108+, Edge 122+, Firefox 122+ and Safari 16.1+, iOS included. Passkeys sync by default on Android 9+, iOS and iPadOS 16+, macOS 13+ and ChromeOS 129+ (passkeys.dev).

Where a site has accounts, the published results are strong. NHS login cut median sign-in from 43 seconds with a password plus a text-message code to 5 seconds with passkeys and conditional UI, and conditional UI grew passkey use about 1.7 times (Chrome for Developers). Mercari's passkey sign-ins succeed 82.5% of the time against 67.7% for text-message codes, in 4.4 seconds against 17 (Google for Developers), and at Yahoo! JAPAN the share of support inquiries about forgotten IDs or passwords fell 25% from its peak (web.dev).

For the motel group I evaluated passkeys rather than measuring them. Guests have no accounts, so there's nothing to add for them. The only sign-in is a staff page built to work with no JavaScript at all, under a Content-Security-Policy that allows no scripts; passkeys need page script, so adopting them means giving that rule up. On a shared front-desk computer, a platform passkey follows the computer's login, not the person at the desk. And the staff sign-in already carries username and current-password tokens, which is what lets password managers work there.

  • Keep passwords during the transition: web.dev says sites need to support both kinds of users for now, which is what conditional UI is for.
  • Check the user-verified flag: with userVerification set to preferred, the authenticator may skip verification. Abort a pending conditional get() before starting any other WebAuthn call.
  • Prerender: credential calls wait until a prerendered page is shown, so don't prerender a sign-in page expecting a prompt.
  • Audit tools: the WebAuthn / Passkey Posture Audit warns when a homepage has no passkey markers; ignore that if you have no accounts.

My call on passkeys: skip them unless your site has a sign-in. If a provider runs it, look for the passkey setting there; Clerk: The Fastest Way I've Found to Ship Real SSO covers one such provider. If you run sign-in yourself, add passkeys next to passwords, offer one right after a successful sign-in the way NHS login does, and use device words like Fingerprint or Face ID rather than explaining what a passkey is.

The Form Conversion Audit flags fields with no autocomplete attribute but treats any value, even off or an invented one, as present, so it can't catch a bad token; the Mega Analyzer rows below can. The WCAG A11y Audit and WCAG Fix Generator cover labels.

Speculation rules: prefetch yes, prerender only if every tag waits

Speculation rules are a block of JSON inside a <script type="speculationrules"> element, or a Speculation-Rules response header naming a JSON file, that tell the browser which of your links to load before the tap. Prefetch downloads the next page's HTML and nothing else; prerender loads and runs the whole page in a hidden tab so it can appear at once (MDN). Eagerness decides when: immediate as soon as the rules are read, eager after a 10 ms hover on desktop, moderate after a 200 ms hover on desktop, or at pointer down if that comes first (on phones, at pointer down and, since August 2025, from viewport heuristics), and conservative on pointer or touch down. List rules default to immediate and document rules to conservative (Chrome for Developers).

None of this was hypothetical here: some of the group's pages already carried Chrome's suggested starter rule, prerender at moderate on every same-site link, and one site already used a prefetch rule delivered by header. This is the prefetch version I'd start from; change the excluded paths to the ones your own sign-out, cart, checkout and thank-you pages use:

<script type="speculationrules">
{
  "prefetch": [
    {
      "where": {
        "and": [
          { "href_matches": "/*" },
          { "not": { "href_matches": "/thank-you*" } },
          { "not": { "href_matches": "/cart*" } },
          { "not": { "href_matches": "/checkout*" } },
          { "not": { "href_matches": "/logout*" } },
          { "not": { "selector_matches": "a[href*='?'], .no-prefetch" } }
        ]
      },
      "eagerness": "moderate"
    }
  ]
}
</script>

With a strict Content-Security-Policy (CSP), the header route keeps the rules out of the HTML and out of the policy:

# on every page
Speculation-Rules: "/speculationrules.json"

# on /speculationrules.json itself (same JSON as the inline example)
Content-Type: application/speculationrules+json

Support as of September 30, 2026: the script type dates from Chrome, Edge and Chrome for Android 109 and Samsung Internet 21, but document rules with where and eagerness, like the example above, and the header need Chrome, Edge and Chrome for Android 121+ and Samsung Internet 25+. Firefox has none, and Mozilla's position is neutral, citing complexity (webstatus.dev). Safari 26.2 and later has prefetch only, behind a flag that is off by default, and no prerender (MDN compatibility data). Every browser on an iPhone uses WebKit, so iPhones ignore the rules by default (web.dev), and browsers without support just load pages as before. WordPress 6.8 and later already add a conservative prefetch rule for logged-out visitors on sites with pretty permalinks (WordPress).

The speed is real and small. From the home page, a tap on the Book button with a prefetch rule like this one at moderate (which fired at the touch in my setup) brought the booking page's first paint from 440 ms to 304 ms at the median of 8 runs, and to 316 ms with the site's real tags running. The prefetch was used 16 of 16 times; the first pass measured 402 to 288 ms. Each prefetch costs one extra HTML download of 9 to 23 KB, and the page a visitor lands on from search or a map gains nothing (about 1.0 s either way).

Only the first pass timed prerender. Prerendered as soon as the rules were read, the booking page painted in about 0.14 s instead of 0.40 s, but the hidden page also ran its own data requests and its tags. Started at the touch, which is what moderate does on a phone tap, prerender was slower than no rule in a smaller run of 5 (703 ms against 404), because the page was still rendering when it was shown.

Then the tags. On a page with the live prerender rule, I held the pointer over a link on desktop for 1.2 seconds without clicking. Chrome started a prerender about a quarter second in. The hidden page fired the Meta Pixel's PageView and ViewContent events a third of a second after the prerender began (336 to 397 ms), and it requested Microsoft Clarity's loader when the page's 3 second timer for Clarity fired, at about 3.5 s. Google Analytics 4 (GA4) sent nothing in 15 seconds; when I opened the page, its page_view went out only then. That held in 3 of 3 runs, and the first pass agreed (Meta at 425 and 463 ms after the rule was added, Clarity at 3.3 s). My copies of the tag scripts loaded instantly, so real hidden hits arrive a little later, and every vendor request failed inside my test browser, so nothing reached Google, Meta or Microsoft.

Phones don't need a hover. On a site with the live prerender rule, a scroll that merely started with the thumb on a button, with no tap, prerendered that button's page within 3 to 8 ms of the touch, and the hidden page fired a Meta PageView (3 of 3). The same gesture started on plain text did nothing (2 of 2). A real Android phone needs even less: since August 2025, moderate there can start a speculation 500 ms after scrolling stops, on one of the larger links near where the visitor last touched, a heuristic my desktop Chrome didn't run (Chrome for Developers).

The reason is in the code. Google Analytics delays until the page is shown (Chrome for Developers), and gtag.js checks document.prerendering to do it. The Meta Pixel's fbevents.js, Microsoft Clarity's script and Google Tag Manager's own gtm.js have no such check in the copies I read on September 30, 2026. Neither Meta's Pixel setup guide nor Microsoft's Clarity setup guide mentions prerendering, and Chrome's list of prerender-aware providers names Google Analytics but not Tag Manager, the Meta Pixel or Clarity (Chrome's provider list). Chrome suggests delaying the tag manager itself until the page is shown (Chrome for Developers). Every hidden PageView is a visit that never happened, counted in whatever reports and audiences you build from that pixel.

Prefetch is clean by comparison. A prefetched page that was never opened sent no vendor requests at all, and prefetch changed no counts: one GA4 page_view and one Meta PageView per page, with or without it.

Service workers need one guard. A service worker is a background script some sites use to work offline; if yours has one, Chrome DevTools lists it under Application, Service workers. A prefetch goes through it. When I dropped the signal during a prefetch and restored it before the tap, a worker that answers failed navigations with its offline page showed an online visitor the offline page (3 of 3 in the first pass, 2 of 2 in the second). Returning a network error for requests that carry a Sec-Purpose header made the prefetch fail cleanly, and the tap loaded the real page (2 of 2):

self.addEventListener('fetch', (event) => {
  if (event.request.mode !== 'navigate') return;
  event.respondWith(
    fetch(event.request).catch(() => {
      // A failed speculative load must fail, not turn into the offline page
      if (event.request.headers.get('Sec-Purpose')) return Response.error();
      return caches.match('/offline.html');
    })
  );
});

First visits are a race. A prefetch made before the service worker took control could be thrown away when the worker claimed the page. For taps right at page load it was used in 4 of 7; for taps 1.5 to 3 seconds after load, every time (10 of 10).

Skip your other domains. With GA4's cross-domain linker running, links to the group's other domains got a _gl parameter at the moment of the tap, so a prefetch of the plain URL was never used (2 of 2). Same-site links were untouched (none of 8 taps got the parameter).

Delivery details that bit. The header route needed no CSP change, but Chrome ignored the rules file when it was served as application/json instead of application/speculationrules+json. For inline rules, Chrome 148 accepted the 'inline-speculation-rules' keyword (MDN) for a block in the HTML and for one added by script, although Chrome's documentation says the keyword doesn't cover rules in the initial HTML, so test your own policy. A sha256 hash worked only when I computed it after the parser had turned CRLF line endings into LF.

Prerender does pay where every script is ready for it. Ray-Ban's prerendered product pages took mobile LCP from 4.69 s to 2.66 s (web.dev), and Monrif cut desktop LCP by up to 17.9% with moderate prerender. Phones are the weak spot: only 2.9% of mobile views on Monrif's La Nazione site triggered a prerender, against 13.9% on desktop (web.dev), and Google Search saw no real mobile gain from hover-based prefetch (Chrome for Developers). Chrome changed how moderate works on Android in August 2025, so older mobile numbers may not hold.

My call: adopt carefully. Prefetch at moderate eagerness on same-site links, excluding sign-out, cart, checkout, thank-you and query-string links. Use the header route with a strict CSP, add the Sec-Purpose guard if you have a service worker, and don't speculate across your own domains while GA4 cross-domain measurement is on. Skip prerender unless every script on the pages it would load waits for the page to be shown (GA4 does; the Meta Pixel, Clarity and Custom HTML tags in Tag Manager don't), or you delay the tag manager itself, as Chrome suggests. On WordPress 6.8 or later, check the built-in rule before adding another. To see what a page already has, view its source and search for speculationrules, or run the Mega Analyzer, which also reads the header. If a rule says prerender and you run the Meta Pixel or Clarity, change it to prefetch.

  • Pages that do something when loaded: sign-out, language switches, add-to-cart, sign-in steps that text a code, usage counters and URLs that start server-side ad conversion tracking are all on MDN's list; a thank-you page that fires a conversion is the common case. Exclude them in the rule, or check the Sec-Purpose header on the server.
  • Stale pages: Chrome keeps a prefetch for up to 5 minutes; the Clear-Site-Data header's "prefetchCache" and "prerenderCache" values clear speculations after a state change.
  • Rules added with innerHTML aren't registered: create the script element with document.createElement, and in Tag Manager, insert the rules with JavaScript.
  • Real hit rates run lower: for moderate, eager and conservative rules, Chrome holds only 2 prefetches and 2 prerenders at a time (first in, first out), and it skips speculation under Save-Data, Energy Saver on low battery, memory pressure, the Preload pages setting turned off (uBlock Origin turns it off) and in background tabs.
  • Header details: the value needs its quotes, the file needs the application/speculationrules+json type, and relative URLs in it resolve against the file unless the rule says "relative_to": "document".
  • A strict parser: an unknown key or bad value usually drops that rule, and a mistake at the top level drops the whole set (HTML standard).

The Speculation Rules API Audit reads inline blocks only, not the header; the Mega Analyzer row reads both. For the mechanics, see Speculation Rules will make your site feel instant if you set them up right, with the corrections listed further down.

fetchpriority: one image, and only if an image is your LCP

fetchpriority tells the browser how important one fetch is compared with the others: high, low or auto, the default. It works on <img>, on <link rel="preload"> and on <script>, and fetch() takes it as a priority option. In Chrome, images start at Low priority and are raised to High only after layout finds them on screen (since Chrome 117, the first five large images start at Medium), so fetchpriority="high" lets the hero start at High straight away (web.dev). The goal is LCP, the time until the largest image, text block or video on the first screen is drawn, which is good at 2.5 seconds or less at the 75th percentile (web.dev).

<img src="/images/storefront-1200.avif"
     srcset="/images/storefront-800.avif 800w, /images/storefront-1200.avif 1200w"
     sizes="100vw" width="1200" height="800"
     alt="The shop front from the street" fetchpriority="high">

A hero set as a CSS background can't be found in the HTML early, so preload it with the hint on the preload; when you preload an <img>, put the hint on both (Chrome for Developers):

<link rel="preload" as="image" href="/images/hero-1200.avif" fetchpriority="high">

It has been Baseline newly available since October 29, 2024 (webstatus.dev), when Firefox 132 joined Chrome and Edge 101 and Safari 17.2 on images (MDN compatibility data). Browsers without it ignore the attribute.

There was no image to prioritize. On every property home page, the phone's LCP was a paragraph of text near the top, and the rooms and booking pages showed no photo on the first screen at all. On the site I timed, LCP on the home, rooms and booking pages was 572 to 650 ms in the lab on Fast 4G and the home page about 2 s on Slow 4G, and a one-visit survey of every property home page stayed under 1.8 s, all inside the 2.5 second line. What gated it on the booking path was the stylesheet download and the rendering work, not any image.

Where a photo was the LCP, the hint still did nothing measurable. Adding fetchpriority="high" to a 169 KB photo that was already the LCP and already in the HTML made LCP 8 ms slower (22 ms slower in the first pass), and removing the hint from LCP images moved LCP by 4 to 22 ms. All of that sits inside the 70 ms A/A noise, because Chrome already requested those images at about 0.2 s, at Medium priority, without the hint.

The classic mistake is real. loading="lazy" on the LCP hero moved its request from 286 ms to 784 ms and made LCP 216 ms worse, slower in 8 of 10 runs. On a 1920 by 1080 desktop, lazy-loading a photo that was the LCP cost 152 ms.

Bytes beat hints. A responsive 31.5 KB AVIF in place of a 169 KB WebP made LCP 106 to 108 ms faster on Fast 4G, and the full fix on that page (all three photos made responsive, an unused preload removed) was 1.8 s faster on Slow 4G. A phone-sized 61 KB hero in place of a 121 KB one was 70 ms faster in 8 of 8 runs (108 ms in the first pass) and 770 ms faster on Slow 4G, with no visible difference on screen.

Tablets broke a plan that looked safe. One idea was to lazy-load a photo just below the fold on phones and preload it for wide screens only, with media="(min-width: 60rem)". It tested fine on a phone and a desktop. But on 768 by 1024 and 820 by 1180 tablets, where that photo was the LCP, it made LCP 0.37 to 0.46 s worse, with 0 of 13 rounds faster. Scoping the preload to (min-width: 37.5rem) with matching imagesizes was neutral on tablets and desktops and kept the phone's small gain, about 30 ms on Fast 4G.

One caveat cuts the other way. The throttling limit I noted at the top undersells priority hints, and web.dev reports a Google Flights experiment where fetchpriority="high" on the LCP background image took LCP from 2.6 s to 1.9 s (web.dev).

My call: adopt, on one image. It's one attribute, and on a congested real connection it can do more than my lab showed. Use it on the image that is actually your LCP, and find that image first. If your LCP is text, or the image is already early in the HTML, expect little; spend the effort on smaller image files, and never lazy-load the hero.

  • One, at most two: web.dev says a high priority on more than one or two images stops helping (web.dev).
  • Never lazy-load the LCP image: web.dev measured the delay (web.dev), and so did I.
  • Carousels: the first visible slide gets high and the hidden slides low.
  • It's a hint: the browser can overrule it.
  • Media-scoped preloads: cover every width where that image is the LCP, tablets included.
  • Check what your LCP is before trusting a tool: the Image LCP Candidate Audit assumes the first <img> in the HTML is the LCP, so a CSS background hero never shows up there, and the first <img>, often a logo, is judged in its place. PageSpeed Insights (pagespeed.web.dev) and the DevTools Performance panel name the real LCP element, and the CrUX Field Data Probe shows real-user LCP when a page has enough Chrome traffic.

For the mechanics, see Your hero image is lazy-loading. That's why your LCP score is terrible.

Native dialog and popover: yes for new work, no rewrite for its own sake

A <dialog> opened with showModal() is a modal in the top layer: it sits above everything with no z-index, gets a ::backdrop, makes the rest of the page inert (it's implicitly aria-modal), closes on Escape and sends focus back to whatever opened it (MDN). show() or the open attribute gives a non-modal dialog instead, with no inert background and no Escape by default. The popover attribute plus a popovertarget button gives a non-modal panel with no JavaScript: in its default auto mode it sits in the top layer, closes on a tap outside or on Escape (light dismiss), closes other auto popovers, returns keyboard focus to the button and sets an implicit aria-expanded (MDN). A popover has no semantics of its own and is never modal (web.dev).

<button type="button" id="open-returns">Return policy</button>

<dialog id="returns" aria-labelledby="returns-title">
  <h2 id="returns-title">Return policy</h2>
  <p>Unopened items can come back within 30 days with a receipt.</p>
  <form method="dialog">
    <button>Close</button>
  </form>
</dialog>

<script>
  document.getElementById('open-returns').addEventListener('click', () => {
    document.getElementById('returns').showModal();
  });
</script>

In browsers from late 2025 on, the button can open the dialog with no script at all (older browsers, including iPhones before Safari 26.2, ignore it and the button does nothing, so keep the script alongside for now):

<button type="button" command="show-modal" commandfor="returns">Return policy</button>

And a popover:

<button type="button" popovertarget="hours">Today's hours</button>
<div id="hours" popover>
  <p>Open 8 a.m. to 6 p.m.</p>
  <button type="button" popovertarget="hours" popovertargetaction="hide">Close</button>
</div>

Support as of September 30, 2026: <dialog> is Baseline widely available (Chrome 37, Firefox 98, Safari 15.4). Popover shipped in Chrome 114, Firefox 125 and Safari 17, but it became Baseline only on January 27, 2025, not in April 2024 as first announced, because iPhones couldn't close a popover with a tap outside until Safari 18.3 (web.dev). Command buttons (command and commandfor) became Baseline on December 12, 2025 (Chrome 135, Firefox 144, Safari 26.2). closedby is still limited (Chrome 134, Firefox 141, and Safari only in Technology Preview), so don't count on it for backdrop-click closing on iPhones, and popover="hint" is limited too.

The chat button couldn't be tapped on any phone. At every phone width, the floating chat button sat under the fixed bottom bar of action buttons. The media query that lifted it above the bar came earlier in the stylesheet than the button's base rule, with equal specificity, so the base rule always won. A real tap on the button opened the bar's payment link instead: 15 of 15 hit tests landed on the bar, and 0 of 20 taps opened the chat. It was only visible above 768 px, and the open panel ran off the left edge of the screen by 16 px on a 412 px phone and 68 px on a 360 px one.

That's the layering problem the top layer exists to prevent, so I tried the panel as a popover in my test browser. Escape and a tap outside closed it, focus went back to the button, aria-expanded tracked it, text typed into its message box survived a light dismiss, and it drew above the header, the bar and the floating buttons with no z-index. But a plain class toggle with the right ARIA and key handling also returned focus and closed on Escape, so the popover's only unique gains were light dismiss and the top layer, neither of which touches bookings, so I'd put the conversion off. Speed was a wash: 72 ms to open at the median with the class toggle and 80 ms as a popover (4x CPU, 15 interleaved samples each in an 800 px wide window, where the button could be reached, first pass only), both far under 200 ms.

The accessibility panel was already a native <dialog> opened with showModal() on every site. Its one gap: a close request that skipped the page's own close handler, which is the path the Android back gesture takes, left the screen reader live region saying the panel was open. Doing that bookkeeping in the dialog's close event fixed it.

Keyboard focus hid behind the bar. When I tabbed through with a keyboard, 9 of 90 stops on a home page and 9 of 83 on the booking page were entirely hidden behind the fixed bottom bar, three booking form fields among them. WCAG 2.2 success criterion 2.4.11, Focus Not Obscured (Minimum), rules that out at level AA (W3C). This rule, together with lifting the chat button out from under the bar, brought it to 0 on every page I measured:

@media (max-width: 768px) {
  /* keep keyboard focus clear of a fixed bottom bar: at least the bar's height */
  html { scroll-padding-bottom: 6rem; }
}

Putting the chat button back where it belonged has its own cost. As I scrolled the booking form, 33 labels and controls passed under it, with up to 17% of a field and 13% of the submit button covered. So at phone widths I'd hide it, or move it out of the form column.

My call: adopt for new work. Build modals as a <dialog> opened with showModal() and simple show-and-hide panels as popovers, but don't rewrite a working, accessible custom component just to use them; the speed is the same. Test the layering first: on a phone, tap every floating button and check that it opens what it says; on a computer, shrink the browser window to phone width and press Tab through the page, watching for the highlighted item to disappear behind a fixed bar. On these sites the change that mattered for bookings was the scroll-padding-bottom rule above, not the element.

  • Popover is never modal and has no role: use a <dialog> for anything that must block the page, never a <dialog> for a tooltip or menu, and give a popover the role its content needs, since it brings none of its own.
  • iPhones before iOS 18.3 can't tap outside to close a popover: always include a visible close button.
  • Focus comes back only sometimes: the browser restores it when the dialog was modal or focus was inside it (HTML standard), so test with a keyboard and a screen reader.
  • Name it and give it a close button: no tabindex on the <dialog> and an explicit close button that works without a keyboard (MDN), plus a name, such as an aria-labelledby that points at its visible title, which the W3C's dialog pattern calls for (W3C).
  • Buttons inside a form default to submit: a <button> with no type inside a form is a submit button even with popovertarget on it, so it submits the form and the popover never opens (HTML standard). Give popover buttons, and buttons that open a dialog, type="button"; the close button inside a <form method="dialog"> is the exception, because closing works by submitting that form.
  • Don't build an interstitial with it: a full-screen popup is still a full-screen popup (That popup is costing you rankings. Google told you in 2017., Intrusive Interstitial Audit).

This site's own accessibility panel is a native <dialog> opened with showModal(), for the same reasons. The WCAG A11y Audit checks labels and visible focus but not dialogs (the Mega Analyzer row below covers those), the WCAG Fix Generator writes fix snippets, and Your tab order is broken and keyboard users already left goes deeper on keyboard focus.

INP and deferred widgets: measure before you move anything

Interaction to Next Paint measures the delay from a click, tap or key press to the next frame the browser paints, across the whole visit, and reports the slowest interaction, ignoring one outlier for every 50. Good is 200 ms or less and poor is over 500 ms, at the 75th percentile of page loads, mobile and desktop separately; hovering, scrolling and zooming don't count (web.dev). It replaced First Input Delay as a Core Web Vital on March 12, 2024 (web.dev). Main-thread management means keeping long tasks, anything over 50 ms, out of the way when people interact: load later what isn't needed yet, and break long work up (web.dev).

Measuring it takes real visitors. Lab tools that only load a page report no INP (Total Blocking Time is a proxy, not a substitute), and the field data in CrUX, PageSpeed Insights and Search Console comes from Chrome only, leaving out Chrome on iPhones. Since Safari 26.2 shipped on December 12, 2025, the APIs behind INP are Baseline, so a real-user monitoring library can collect it from Safari and Firefox too (web.dev).

Three patterns cover most of the work. Load a heavy widget on first use, the facade pattern Lighthouse describes for chat widgets (Chrome for Developers):

<button type="button" id="chat-launcher">Chat with us</button>
<script>
  document.getElementById('chat-launcher').addEventListener('click', () => {
    const s = document.createElement('script');
    s.src = 'https://chat.example.com/widget.js'; // your chat vendor's loader
    s.async = true;
    s.onload = () => { /* call the vendor's open method here */ };
    document.head.append(s);
  }, { once: true });
</script>

Load something far down the page when the visitor gets near it:

const mapBox = document.getElementById('map');
const watcher = new IntersectionObserver((entries) => {
  if (entries.some((entry) => entry.isIntersecting)) {
    watcher.disconnect();
    loadMap(); // add the map library and draw the map
  }
}, { rootMargin: '1000px 0px' });
watcher.observe(mapBox);

And break long work into pieces, yielding between them, with a fallback:

function yieldToMain() {
  if (globalThis.scheduler && typeof scheduler.yield === 'function') {
    return scheduler.yield();
  }
  return new Promise((resolve) => setTimeout(resolve, 0));
}

As of September 30, 2026, scheduler.yield() works in Chrome 129 and Firefox 142, and requestIdleCallback in Chrome 47 and Firefox 55. Safari has neither; requestIdleCallback exists there only behind a flag in Technology Preview (MDN compatibility data).

Every tap was already under the line. With the CPU slowed 4x, every tap on the path to a booking answered in under 200 ms at the median: the menu in 72 to 80 ms, a booking form field in 40 ms, and the date and room pickers in 15 to 56 ms to the next frame. The one control near the line was the accessibility panel, at 184 ms at the median (160 to 200) with one 304 ms run on another site, and its cost sits inside its own open handler, which deferring its setup can't touch.

Where the load time went surprised me. Two thirds to three quarters of the main-thread CPU during load was rendering: text shaping, layout, style and paint. The sites' own script was 7 to 16 percent, and the accessibility panel and the chat widget each took about 1 ms to set up. So deferring those widgets to idle time bought nothing. Load CPU on a home page went from 394 ms to 396 ms, and the accessibility button appeared 130 to 300 ms later.

The deferral that paid was a map. On one site a map sat 3.7 screens down a page, and its library loaded with the page. Loading it only when the visitor scrolls within 1,000 px of it cut main-thread time at load by a median of 82 ms across paired runs, about 17 percent (medians 487 and 416 ms, better in 6 of 6 pairs), sent about 130 KB less over the wire at load (the library, its data and the map tiles), and brought first paint 200 ms sooner in 5 of 6 pairs on a busy test machine. The trade-off: on a page where the map sat about one screen down, it appeared about 0.3 s later.

I also tried content-visibility: auto on sections below the fold. It cut load CPU by up to 24 percent, but in Chrome 146 the headings and links of sections not yet rendered were missing from the accessibility tree (14 of 33 headings exposed at the top of one home page), so I didn't recommend it.

One caveat outweighs every number here: all these runs blocked the analytics and ad tags, so they measure the sites' own code. Third-party tags are exactly where web.dev reports trouble, finding a correlation between the size of tag managers and poorer INP (web.dev). That's why field data comes first. And the worst phone problem on these pages never showed up in INP at all: the chat button from the dialog section. A button nobody can reach has no interaction to time.

My call: adopt carefully, after you read your field data. Check INP in PageSpeed Insights or the CrUX Field Data Probe first; a small site may not have enough Chrome traffic to show up, and then the Third-Party Script Cost Audit at least shows which scripts weigh the most. Defer what is heavy and not needed yet: maps and video embeds far down the page, and third-party chat behind a click-to-load facade. Leave small first-party widgets alone, and give every idle or yield call a setTimeout fallback for Safari.

  • Facades hide features until the real widget loads: an unread-message badge, for example, appears only after it does. Lighthouse removed its facade audit in Lighthouse 13, so a lab score won't remind you either way.
  • Don't defer what people need early: an accessibility button that appears up to 0.3 s later is later for exactly the people who use it.
  • Prerender and INP: holding all scripts until a prerendered page is shown piles the work into that moment (Chrome for Developers).
  • Frames count: taps inside an iframe count toward INP but aren't reported to your page's scripts, so real-user data and CrUX can differ.
  • Widgets added by a tag manager: they aren't in your HTML, so no static check sees them.

The Third-Party Script Cost Audit finds the heavy scripts and the CWV Fix Generator writes patches. For background, see Why your site feels slow even when PageSpeed says it's fast, Your Lighthouse score is lying to you. Here's what Google actually measures. and INP attribution: which click broke the 200 ms budget. Whether a chat widget belongs on the page at all is its own question, covered in When Live Chat and Booking Widgets Actually Earn Their Screen Space.

Cross-document view transitions: skip them on pages people use to get things done

One CSS rule in the stylesheet of both pages, @view-transition { navigation: auto; }, makes a same-origin link navigation cross-fade instead of switching at once, with unchanged parts such as a shared header staying put, and no JavaScript. It covers link clicks and Back and Forward, not the address bar, bookmarks or reloads. Both pages must share an origin (a subdomain counts as another origin), and Chrome skips the transition if the navigation takes more than 4 seconds (Chrome for Developers). The WebKit team calls it two lines of code you can use on every website today (WebKit), and it is. The question is what those two lines buy on a page someone is trying to use.

If you keep it anywhere, guard it:

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

A site-level reduce-motion switch stored in localStorage and read by a deferred script can't reach the next page's CSS before its first frame, so honor it on the page being left:

// On the page being left: honor the site's own reduce-motion switch
window.addEventListener('pageswap', (event) => {
  if (event.viewTransition && document.documentElement.classList.contains('reduce-motion')) {
    event.viewTransition.skipTransition();
  }
});

Support as of September 30, 2026: Chrome, Edge and Chrome for Android 126+ (June 2024), Safari and iOS 18.2+ (December 2024) and Samsung Internet 28+ (webstatus.dev). Firefox has no cross-document transitions, although same-document ones shipped in Firefox 144 and became Baseline on October 14, 2025, and Interop 2026 adds cross-document transitions to its focus area (WebKit). The pageswap event is in Chrome 124 and, partly, Safari 18.2, but not Firefox. Browsers without support simply load the next page; as WebKit puts it, the fallback is to do nothing.

It ignored the reduce motion setting. Every site in the group but one had the opt-in line, with no media condition. With the phone set to reduce motion, and separately with the site's own Reduce Motion switch on, the fade still ran (five animations of 250 ms), forward and on Back, on every site I sampled, although the sites' accessibility page says that switch turns off all animations and transitions. Their reduced-motion CSS targeted *, *::before, *::after, which never selects the ::view-transition pseudo-elements.

To be precise about the harm: WebKit's guidance is that simple cross-fades aren't known to cause adverse effects for people sensitive to motion, while slides, zooms and depth effects should respect the setting, and WCAG 2.3.3 (level AAA) asks that motion triggered by interaction can be turned off (W3C). So the problem here was less harm than a broken promise. Anything beyond a cross-fade should always get the guard.

It didn't paint any faster. With the rule on and off (8 alternating runs per arm, same files otherwise), tap to first paint was 364.5 ms with the fade and 377.5 ms without, and LCP equaled first paint every time. The first pass, with 12 runs per arm, agreed: its medians moved both ways inside overlapping quartiles.

Taps went dead. During the fade, which lasted about 0.3 s after each new page appeared (median 300.5 to 322.5 ms), a real touch tap landed on the page's root <html> element instead of the button under the finger (4 of 4). The moment the new page accepted a tap moved from 357.5 ms to 622.5 ms at the median, 265 ms later, with quartiles that didn't overlap, on every page change. A home to rooms to booking path pays it twice.

One wide page killed it. A translated page whose longest button label couldn't wrap was 418 px wide on a 412 px phone, and every transition into or out of it aborted with an InvalidStateError (12 of 12). Letting the label wrap made the page fit, and the transitions ran.

The guard works. In my test browser, the media query version parsed as auto if (prefers-reduced-motion: no-preference), faded normally without the setting and ran no transition with reduce set. With the site's switch on, the pageswap listener skipped the transition, forward and on Back. Pages restored from the back/forward cache ran the transition too (15 of 15 restores came from that cache), so test Back as well as forward.

My call: skip it on working pages. On the booking path I measured, it bought no speed and cost about a quarter second of dead taps on every page change. Both pages must opt in, so leaving the rule out of those pages' CSS is enough; on a site with one shared stylesheet, that means deleting the @view-transition rule. If the look matters elsewhere on the site, adopt it carefully: wrap the rule in a no-preference media query, honor any site-level motion switch in a pageswap listener, keep every page exactly as wide as the screen, and don't add render blocking to make it prettier.

  • Same origin only: sister sites on separate domains never animate between each other.
  • Duplicate names skip the transition: every view-transition-name on a page must be unique (Chrome for Developers).
  • Slow navigations lose it: over 4 seconds in Chrome, and the transition is skipped.
  • Render blocking: Chrome advises against blocking="render" unless you can measure its effect on your visitors.
  • No business case yet: the only figure in Chrome's case studies, redBus's 7% more sales, came together with INP work and without an A/B test (Chrome for Developers).

The View Transitions API Audit checks the opt-in, names and the reduced-motion guard, and the User Preference Media Queries Audit checks prefers-reduced-motion support. Why a View Transitions API Audit Matters for UX covers the mechanics (its support lines are corrected below), and Your accessibility toggles combine, and nobody tests the combinations covers the switch problem.

Corrections to my April posts

Three of my April posts are out of date or wrong on these points.

  • Your hero image is lazy-loading. That's why your LCP score is terrible.: it said Firefox had fetchpriority behind a flag. Firefox 132 shipped it on October 29, 2024, which made the attribute Baseline.
  • Why a View Transitions API Audit Matters for UX: it dated Safari support to Safari 18 in September 2024, which brought only same-document transitions. The cross-document kind that post covers arrived in Safari 18.2, in December 2024. And only the @view-transition rule is required; names, startViewTransition() and the pseudo-element styles are optional.
  • Speculation Rules will make your site feel instant if you set them up right: it called the API Chrome-only. Edge and Samsung Internet support it too; Firefox still has none, and Safari 26.2 has prefetch only behind a flag that is off by default. Its line that GA4 deduplicates prerendered page views isn't what Chrome documents: Google Analytics waits until the page is shown. The Meta Pixel and Clarity don't.

What the Mega Analyzer checks

The Mega Analyzer got rows for these features on September 30, 2026. Most sit in a new Modern browser features card in the Perf + AI tab, after Mobile presentation. None of the new rows is scored; the one score change is to the existing parser-blocking row, covered under widget scripts below.

  • Speculation rules: reads inline <script type="speculationrules"> blocks, the Speculation-Rules header and up to two rules files the header names. It fails only one thing: a rule at immediate, eager or moderate eagerness that can load a sign-out, add-to-cart or other action link before the click. It warns when browsers would ignore the rules (invalid JSON, a src attribute, an unquoted header, a rules file with the wrong content type or a 404, a CSP that blocks the inline block), when a rule can load cart or checkout pages early or loads everything as soon as the page opens, and when individual rules have errors. It notes missing rules, rules a performance plugin holds until the first click, scroll or key press, and headers it couldn't check.
  • View transitions: appears only on pages that use them. It passes a plain cross-fade, and motion that comes with a reduced-motion fallback; a pass means the rule is sound, not that the fade is worth its dead taps. It warns when two elements share a view-transition-name, when the page adds motion with no fallback (only when it read every stylesheet and script), and when the site's own reduced-motion rule never reaches the transition layers, as with the motel sites' *, *::before, *::after reset. This site got that warning too on the day I wrote the row: its stylesheet opted in with no media condition, and its reduced-motion rule used the same selector. I moved the opt-in inside a prefers-reduced-motion: no-preference query and made the site's own Reduce Motion switch skip the transition, and the row passes here now.
  • Dialog and popover: passes native dialogs and popovers. It warns on wiring that does nothing as written (a dialog shown with the open attribute but marked modal, a popovertarget that points at an element with no popover attribute or sits on something that isn't a button, a popover button inside a form that would submit it) and on custom dialogs with no accessible name. A popovertarget or commandfor that names a missing id is counted in the A11y tab's broken-reference rule.
  • Passkeys: appears only on pages with a visible password field, and never warns. It passes a webauthn token, a WebAuthn API call or passkey sign-in markup; otherwise it notes a passkey-capable sign-in provider with no passkey signal in the page, or no passkey option at all.
  • Widget scripts: an unscored note in the Performance card naming known chat, accessibility overlay, review, popup and booking widgets that load with the page. Widgets that already load on first use or when idle are left out, and so are consent managers, A/B testing tools and translation services that have to run first. The scored parser-blocking row keeps its title and weight and now names the vendors it recognizes. It also stops counting consent managers, A/B testing tools and translation services that have to run before the page is shown, and scripts Cloudflare Rocket Loader has already deferred, so a site whose only blocking scripts are those now passes that row.
  • Autofill: in the A11y tab, shared with the Site Analyzer. The missing-token rule now classifies fields, so search boxes, spam traps, one-time codes and fields about someone else are left out. A new failure flags values browsers don't recognize (autocomplete="phone" instead of tel), and a new warning flags sign-in or contact fields marked off instead of carrying a token. In the Site Analyzer those two new results are shown but not scored; the missing-token rule scores as it always has.
  • fetchpriority: no new row; the existing hero image row already checks it.

What a single pass can't see matters as much. The analyzer reads files and never runs the page, so it can't measure INP or LCP, see whether a prerender happens or which tags fire inside one, read rules, CSS or widgets that a script or tag manager adds later, or see layout: a bar covering a button, a page wider than the screen, focus hidden behind a fixed element. It can't tell whether Chrome will autofill a spam trap, and it can't see the server side of passkeys or what a service worker does with a prefetch. When it couldn't read a file, it says not verified instead of guessing. For everything else, use field data, DevTools and a real phone.

When to leave it alone

  • A one-page site: speculation rules and view transitions need a next page to exist.
  • Tags that don't wait for a prerender: if you run the Meta Pixel, Clarity or Custom HTML tags in Tag Manager, don't prerender. Prefetch is fine, except on links to your other domains while GA4 cross-domain measurement is on.
  • WordPress 6.8 or later: it already prefetches conservatively for logged-out visitors on sites with pretty permalinks. Look before adding a second rule.
  • Your LCP is text: fetchpriority has nothing to do.
  • Small widgets of your own and accessibility controls: don't defer them.
  • A custom dialog that already works: if it passes keyboard and screen reader tests, leave it until you rebuild that part anyway.
  • No accounts: no passkeys, and ignore a passkey warning from any audit tool, mine included.
  • Fields about someone else: no personal tokens on an emergency contact box; label it and check what arrives.
  • Pages people use to get something done: no view transitions.

If you're building your own site rather than paying for a $1,500 quote, most of what's worth doing here is an attribute or a rule, not a rebuild. That suits the premise of The $97 Launch: an accessible small business site in 30 days for about $97.

Fact-check notes and sources

  • Source: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html establishes the autofill token list and grammar, what off means, and that webauthn comes last and only on input and textarea.
  • Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete establishes that off does not stop most password managers, that invented values break autofill, and that browsers may require a name or id, a form and a submit button.
  • Source: https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html establishes WCAG 2.2 success criterion 1.3.5, Identify Input Purpose, at level AA.
  • Source: https://developer.chrome.com/blog/autofill-insights-2024 establishes 75% lower abandonment and about 35% less filling time with autofill, from a correlational study of US forms (December 17, 2024).
  • Source: https://web.dev/articles/sms-otp-form establishes the recommended one-time code field and why type="number" is avoided; https://bcd.developer.mozilla.org/bcd/api/v0/current/html.elements.input.autocomplete.json establishes that the attribute works in every browser, and https://bcd.developer.mozilla.org/bcd/api/v0/current/html.elements.form.autocomplete.json establishes support for the one-time-code and webauthn tokens (MDN browser-compat-data 8.1.3, read September 30, 2026).
  • Source: https://web.dev/articles/passkey-form-autofill establishes conditional UI, the username webauthn token, the server's jobs, the user-verified flag, aborting a pending request and keeping passwords during the transition; https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API and https://api.webstatus.dev/v1/features/webauthn establish that WebAuthn is Baseline widely available and HTTPS only; https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/isConditionalMediationAvailable_static establishes cross-browser support for the conditional check since October 2023; https://passkeys.dev/device-support/ establishes passkey autofill and synced passkey support by platform.
  • Source: https://developer.chrome.com/blog/nhs-passkeys-case-study establishes NHS login's 43 to 5 second median sign-in, the 1.7x growth from conditional UI, and its offer after a successful sign-in with device wording; https://developers.google.com/identity/passkeys/case-studies/mercari establishes Mercari's 82.5% against 67.7% sign-in success and 4.4 against 17 seconds; https://web.dev/case-studies/yahoo-japan-passkeys establishes that the share of Yahoo! JAPAN's inquiries about forgotten IDs or passwords fell 25% from its peak.
  • Source: https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API establishes what prefetch and prerender do, the 5 minute prefetch cache, the Clear-Site-Data values, the Sec-Purpose header, the unsafe URL list and the APIs deferred until a prerendered page is shown, credential calls included.
  • Source: https://developer.chrome.com/docs/web-platform/prerender-pages establishes the starter rule, eagerness defaults and heuristics, Chrome's limits and skip conditions, header delivery, that Google Analytics delays until activation, and that rules inserted with innerHTML are not registered; https://developer.chrome.com/docs/web-platform/implementing-speculation-rules establishes delaying the tag manager, inserting rules through Tag Manager with JavaScript, and the INP cost of holding all work until activation.
  • Source: https://html.spec.whatwg.org/multipage/speculative-loading.html establishes the required application/speculationrules+json type, that CSP does not apply to header-loaded rule sets, and the strict parser; https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src establishes the 'inline-speculation-rules' source.
  • Source: https://api.webstatus.dev/v1/features/speculation-rules, https://bcd.developer.mozilla.org/bcd/api/v0/current/html.elements.script.type.speculationrules.json and https://bcd.developer.mozilla.org/bcd/api/v0/current/http.headers.Speculation-Rules.json establish browser support and Mozilla's neutral position, read September 30, 2026; https://web.dev/blog/lcp-and-inp-are-now-baseline-newly-available establishes that every browser on iOS uses WebKit.
  • Source: https://web.dev/case-studies/rayban-speculation-rules establishes Ray-Ban's prerender results; https://web.dev/case-studies/monrif-cwv establishes Monrif's desktop LCP gain and how rarely hover eagerness fired on phones; https://developer.chrome.com/blog/search-speculation-rules establishes Google Search's mobile result; https://make.wordpress.org/core/2025/03/06/speculative-loading-in-6-8/ establishes WordPress 6.8's default conservative prefetch.
  • Source: https://docs.google.com/spreadsheets/d/1uAmhCcI9kweRG8o8dcwSoyS8WKDkGdexGfvpD4rFJ7w/edit?gid=0#gid=0 establishes Chrome's list of prerender-aware providers, which names Google Analytics and not Tag Manager, the Meta Pixel or Clarity; https://developers.facebook.com/docs/meta-pixel/get-started/ and https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-setup establish that Meta's Pixel setup guide and Microsoft's Clarity setup guide say nothing about prerendering (checked September 30, 2026).
  • Source: https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX, https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX, https://connect.facebook.net/en_US/fbevents.js and https://scripts.clarity.ms/0.8.71/clarity.js, fetched September 30, 2026 with placeholder IDs, establish that only gtag.js contains a prerender check.
  • Source: https://web.dev/articles/fetch-priority establishes the syntax, the default image priorities, the Google Flights result and the carousel advice; https://web.dev/articles/optimize-lcp establishes the one or two image limit; https://web.dev/articles/lcp-lazy-loading establishes the cost of lazy-loading the LCP image; https://web.dev/articles/lcp establishes what LCP measures and the 2.5 second threshold.
  • Source: https://api.webstatus.dev/v1/features/fetch-priority and https://bcd.developer.mozilla.org/bcd/api/v0/current/html.elements.img.fetchpriority.json establish Baseline since October 29, 2024 (Chrome 101, Firefox 132, Safari 17.2); https://developer.chrome.com/docs/performance/insights/lcp-discovery establishes that a CSS background hero needs a preload and that the hint belongs on both the preload and the image.
  • Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog, https://developer.mozilla.org/en-US/docs/Glossary/Top_layer and https://html.spec.whatwg.org/multipage/interactive-elements.html establish showModal() against show(), the top layer, the focus return rules, closedby and the dialog accessibility rules; https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ establishes that a modal dialog is named by an aria-labelledby pointing at its visible title, or an aria-label.
  • Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/popover, https://developer.mozilla.org/en-US/docs/Web/API/Popover_API/Using, https://web.dev/blog/popover-api and https://web.dev/blog/popover-baseline establish popover behavior, that it has no semantics, and its January 27, 2025 Baseline date with the iOS 18.3 fix; https://api.webstatus.dev/v1/features/dialog, https://api.webstatus.dev/v1/features/popover, https://api.webstatus.dev/v1/features/invoker-commands, https://bcd.developer.mozilla.org/bcd/api/v0/current/html.elements.dialog.json and https://bcd.developer.mozilla.org/bcd/api/v0/current/html.global_attributes.popover.json establish support (webstatus.dev dates the full popover feature to Chrome 116; BCD has the attribute from Chrome 114).
  • Source: https://html.spec.whatwg.org/multipage/form-elements.html establishes that a button with no type inside a form is a submit button unless it carries command or commandfor, and that submitting ends its activation before any popover opens (read September 30, 2026).
  • Source: https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html establishes WCAG 2.2 success criterion 2.4.11, Focus Not Obscured (Minimum), at level AA: a component that receives keyboard focus must not be entirely hidden by author-created content.
  • Source: https://web.dev/articles/inp and https://web.dev/blog/inp-cwv-launch establish INP's definition, thresholds and March 12, 2024 launch; https://web.dev/blog/lcp-and-inp-are-now-baseline-newly-available establishes Safari 26.2 support and that CrUX stays Chrome-only.
  • Source: https://web.dev/articles/optimize-long-tasks establishes the 50 ms long task and scheduler.yield() with a fallback; https://bcd.developer.mozilla.org/bcd/api/v0/current/api.Scheduler.yield.json and https://bcd.developer.mozilla.org/bcd/api/v0/current/api.Window.requestIdleCallback.json establish that Safari has neither.
  • Source: https://web.dev/articles/tag-best-practices establishes the link between tag manager size and poorer INP; https://developer.chrome.com/docs/lighthouse/performance/third-party-facades establishes the facade pattern, its trade-offs and its removal from Lighthouse 13.
  • Source: https://developer.chrome.com/docs/web-platform/view-transitions/cross-document establishes the opt-in, the same-origin rule, which navigations animate, the 4 second timeout and the render-blocking advice; https://developer.chrome.com/docs/web-platform/view-transitions/same-document establishes that duplicate names skip a transition.
  • Source: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@view-transition, https://api.webstatus.dev/v1/features/cross-document-view-transitions and https://bcd.developer.mozilla.org/bcd/api/v0/current/css.at-rules.view-transition.json establish cross-document support; https://api.webstatus.dev/v1/features/view-transitions establishes same-document Baseline on October 14, 2025; https://bcd.developer.mozilla.org/bcd/api/v0/current/api.Window.pageswap_event.json establishes pageswap support; https://webkit.org/blog/17818/announcing-interop-2026/ establishes that Interop 2026 adds cross-document transitions.
  • Source: https://webkit.org/blog/16967/two-lines-of-cross-document-view-transitions-code-you-can-use-on-every-website-today/ establishes WebKit's fallback and reduced-motion guidance; https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html establishes WCAG 2.3.3 at level AAA; https://developer.chrome.com/blog/view-transitions-case-studies establishes that the only business figure, redBus's 7% more sales, came with INP work and no A/B test.
  • My own tests: measured September 30, 2026 on a small motel group's live sites, read-only, with Chrome 146 and 148 driven by Puppeteer 24.40: a 412 by 915 screen at a 2.625 pixel ratio, the CPU slowed 4x and a Fast 4G profile (Slow 4G where stated), analytics and ad requests blocked, no form submitted and no request other than GET sent. Proposed changes were tested by serving the edited files to my test browser only; none had shipped on those sites as of that date. Every figure a call rests on was checked by a second, separate set of scripts, which re-measured the first pass and added tests it had missed (the delay before a new page accepts a tap, the tablet LCP runs, the phone scroll that starts a prerender); the second pass wins where they differ (the fade window, the map savings, the first-visit prefetch race, the hidden-focus counts). The autofill results come from Chrome's DevTools autofill with a made-up profile, not from a phone. The tag script checks used copies fetched September 30, 2026 with placeholder IDs. What the form service does with a filled spam trap comes from its own documentation, read September 30, 2026.

Related reading

This post is informational, not legal advice. Mentions of Google, Meta, Microsoft, Apple, Mozilla and other third parties are nominative fair use. No affiliation is implied.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility Statement • Privacy Policy

Last updated: April 2026