Loading
Speculation Rules Inspector

Speculation rules inspector

Inspects how a page uses the Speculation Rules API to prefetch and prerender future navigations, and whether the current page was itself prerendered. A prerendered page can activate instantly, which makes navigations feel immediate and changes how load metrics are measured.

Why this matters

Prerendering loads and renders a page in the background before the user navigates to it. When the user clicks, the browser activates the prerendered page instead of loading it, so LCP can be close to zero. Rules that are missing, invalid or too aggressive either miss that benefit or waste bandwidth and CPU.

Timing values on a prerendered page are relative to the start of the prerender, not to the activation. activationStart is the offset that turns them into user-perceived timings, which is why metric snippets subtract it.

What the snippet reports

  • Whether the browser supports speculation rules
  • Every <script type="speculationrules"> block, parsed into one item per rule (action, source, eagerness, URLs, where conditions)
  • Invalid JSON, which the browser silently ignores
  • Rules that are likely to waste resources, such as eager document prerender rules
  • document.prerendering, activationStart and the navigation deliveryType ("navigational-prefetch" when the page came from a prefetch)
  • The prerenderingchange event, so the activation of a prerendered page is recorded

Snippet

// Prerender & Speculation Rules Inspector
// https://webperf-snippets.nucliweb.net

(() => {
  const MAX_ITEMS = 50;
  // Chrome limits eager prerenders per page; more than this is wasted or dropped
  const EAGER_PRERENDER_LIMIT = 10;

  // Activation of a prerendered page is only observable while the snippet listens
  const state = { activated: !document.prerendering, activatedAt: null };
  if (document.prerendering) {
    document.addEventListener(
      'prerenderingchange',
      () => {
        state.activated = true;
        state.activatedAt = Math.round(performance.now());
        console.log('%cPrerendered page activated', 'color: #22c55e; font-weight: bold;');
      },
      { once: true }
    );
  }

  const inspect = () => {
    const issues = [];
    const items = [];
    const actions = {};
    const supported = typeof window.HTMLScriptElement.supports === 'function'
      && window.HTMLScriptElement.supports('speculationrules');

    const [nav] = performance.getEntriesByType('navigation');
    const activationStart = Math.round(nav?.activationStart ?? 0);
    const deliveryType = nav?.deliveryType ?? '';

    const scripts = [...document.querySelectorAll('script[type="speculationrules"]')];

    scripts.forEach((script, index) => {
      let parsed;
      try {
        parsed = JSON.parse(script.textContent);
      } catch (e) {
        issues.push({
          severity: 'error',
          message: `Speculation rules script #${index + 1} is not valid JSON (${e.message}). The browser ignores the whole script.`,
        });
        return;
      }

      for (const [action, rules] of Object.entries(parsed ?? {})) {
        if (!Array.isArray(rules)) continue;
        actions[action] = (actions[action] ?? 0) + rules.length;

        for (const rule of rules) {
          const source = rule.source ?? (rule.where ? 'document' : 'list');
          const urls = Array.isArray(rule.urls) ? rule.urls : [];
          const eagerness = rule.eagerness ?? (source === 'document' ? 'conservative' : 'immediate');
          items.push({
            action,
            source,
            eagerness,
            urls: urls.slice(0, 20),
            urlCount: urls.length,
            where: rule.where ?? null,
            requires: rule.requires ?? [],
            referrerPolicy: rule.referrer_policy ?? null,
            scriptIndex: index,
          });

          const isPrerender = action === 'prerender' || action === 'prerender_until_script';
          if (isPrerender && source === 'list' && (eagerness === 'immediate' || eagerness === 'eager') && urls.length > EAGER_PRERENDER_LIMIT) {
            issues.push({
              severity: 'warning',
              message: `A ${action} rule lists ${urls.length} URLs with eagerness "${eagerness}". Chrome caps eager prerenders, so the extra URLs are not prerendered. Fix: list fewer URLs or use "moderate".`,
            });
          }
          if (isPrerender && source === 'document' && (eagerness === 'immediate' || eagerness === 'eager')) {
            issues.push({
              severity: 'warning',
              message: `A document ${action} rule uses eagerness "${eagerness}", which prerenders matching links without a user signal and can waste bandwidth and CPU. Fix: use "moderate" or "conservative".`,
            });
          }
        }
      }
    });

    if (scripts.length === 0) {
      issues.push({ severity: 'info', message: 'No speculation rules found in the DOM. Rules delivered in the Speculation-Rules HTTP header are not readable from JavaScript.' });
    } else if (items.length === 0 && !issues.some((i) => i.severity === 'error')) {
      issues.push({ severity: 'info', message: 'Speculation rules scripts were found but contain no prefetch or prerender rules.' });
    }
    if (!supported && scripts.length > 0) {
      issues.push({ severity: 'info', message: 'This browser does not support speculation rules, so the rules are ignored.' });
    }
    if (activationStart > 0) {
      issues.push({ severity: 'info', message: `This page was prerendered and activated at ${activationStart}ms. Navigation timings are relative to activationStart.` });
    }

    return {
      script: 'Speculation-Rules-Inspector',
      status: 'ok',
      count: items.length,
      details: {
        supported,
        prerendering: document.prerendering === true,
        activated: state.activated,
        activatedAt: state.activatedAt,
        activationStart,
        deliveryType,
        scriptCount: scripts.length,
        actions,
      },
      items: items.slice(0, MAX_ITEMS),
      issues,
    };
  };

  window.getSpeculationRulesInspection = inspect;
  const result = inspect();

  console.group('Speculation rules');
  console.log(`Prerendering: ${result.details.prerendering}, activationStart: ${result.details.activationStart}ms, deliveryType: "${result.details.deliveryType}"`);
  if (result.items.length > 0) console.table(result.items.map(({ action, source, eagerness, urlCount }) => ({ action, source, eagerness, urlCount })));
  result.issues.forEach((i) => console.log(`[${i.severity}] ${i.message}`));
  console.groupEnd();

  if (document.prerendering) {
    return {
      script: 'Speculation-Rules-Inspector',
      status: 'tracking',
      message: 'This page is prerendering. Call getSpeculationRulesInspection() after activation to see the final timings.',
      getDataFn: 'getSpeculationRulesInspection',
    };
  }
  return result;
})();

Understanding the results

The snippet returns the following structure.

FieldDescription
countNumber of rules found across all speculation rules scripts
itemsOne entry per rule, capped at 50
details.supportedWhether the browser supports speculation rules
details.prerenderingtrue while the page is being prerendered
details.activatedtrue once the page is visible to the user, or was never prerendered
details.activationStartMilliseconds between the start of the prerender and the activation, 0 for a normal load
details.deliveryType"navigational-prefetch" for a prefetched navigation, empty otherwise
details.scriptCountNumber of script[type="speculationrules"] elements
issuesInvalid JSON, rules that waste resources, and informational notes

Eagerness

eagerness controls when the browser speculates. List rules default to immediate, document rules default to conservative.

ValueTrigger
immediateAs soon as the rules are observed
eagerShortly after the rules are observed
moderateOn pointer hover for about 200ms, or on pointer down
conservativeOn pointer down or touch start
⚠️
Rules delivered through the Speculation-Rules HTTP header are not readable from JavaScript. The snippet reports them as missing when no script element exists. Check the response headers in the Network panel.

Checking a prerendered page

Run the snippet in a page that is prerendering to get status: "tracking". After the page is activated, call getSpeculationRulesInspection() to read activated, activatedAt and activationStart. In DevTools, the Application panel lists speculative loads under Speculative loads, with the status and failure reason of each one.

Further reading