Skip to Content
LoadingScripts Loading

Scripts loading

Chrome 54+ Edge 17+ Firefox 61+ Safari 16.4+ Details

Analyzes all scripts on the page, showing their loading strategy and identifying potential performance issues. Scripts are often the biggest cause of main thread blocking and delayed rendering. This snippet helps you audit which scripts are blocking the critical rendering path.

Why this matters

Render-blocking scripts in <head> without async or defer pause HTML parsing while they download and execute. This delays First Contentful Paint (FCP) and Largest Contentful Paint (LCP). Even a single blocking script can add hundreds of milliseconds to your load time, especially on slower connections.

How scripts affect HTML parsing

Switch between <script>, async, and defer, then step through to see when the parser blocks, when the script downloads, and when it executes. Open demo in a new tab ↗

Loading strategy comparison

StrategyDownloadsExecutesBlocks ParsingOrder Guaranteed
NormalWhen encounteredImmediately Yes Yes
asyncIn parallelWhen readyBriefly No
deferIn parallelAfter parsing No Yes
moduleIn parallelAfter parsing No Yes
async moduleIn parallelWhen readyBriefly No

When to use each

StrategyBest For
NormalCritical scripts that must run before content (rare)
asyncIndependent scripts (analytics, ads)
deferScripts that need DOM, must run in order
moduleModern ES modules with imports

Performance tip: Most scripts should use defer. Use async only for truly independent scripts that don’t rely on DOM or other scripts.

Snippet

Run it from an AI agent: the prompt uses the CLI to measure any URL and report the result.

// Script Loading Analysis
// https://webperf-snippets.net

(() => {
  // @shared isOwnRequest
  // True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
  // did not make it, so no snippet counts it.
  function isOwnRequest(entry) {
    return entry.name.endsWith("#webperf-snippets");
  }
  // @end-shared isOwnRequest

  // Domains that belong to the site but differ from the page's root domain, such as its own CDN.
  // They count as first party. Example: const OWN_DOMAINS = ["bbci.co.uk", "bbc.co.uk"];
  const OWN_DOMAINS = [];

  // @shared formatBytes
  function formatBytes(bytes) {
    if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return "-";
    if (bytes === 0) return "0 B";
    const units = ["B", "KB", "MB", "GB"];
    const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1));
    return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
  }
  // @end-shared formatBytes

  const formatMs = (ms) => (ms > 0 ? ms.toFixed(0) + "ms" : "-");

  // @shared getRootDomain
  function getRootDomain(hostname) {
    const host = hostname.replace(/\.$/, "");
    // An IP address has no registrable domain, so each address is its own root
    if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":")) return host;
    const parts = host.split(".");
    if (parts.length <= 2) return host;
    // Country-code domains with a second-level suffix: example.co.uk, example.com.au, example.ac.jp
    const secondLevelSuffixes = ["ac", "co", "com", "edu", "go", "gob", "gouv", "gov", "govt", "mil", "ne", "net", "nom", "or", "org", "sch"];
    const tld = parts[parts.length - 1];
    const sld = parts[parts.length - 2];
    if (tld.length === 2 && secondLevelSuffixes.includes(sld)) return parts.slice(-3).join(".");
    return parts.slice(-2).join(".");
  }
  // @end-shared getRootDomain

  // @shared isFirstParty
  function isFirstParty(hostname) {
    const root = getRootDomain(hostname);
    if (root === getRootDomain(location.hostname)) return true;
    return OWN_DOMAINS.some((d) => getRootDomain(String(d).trim().toLowerCase().replace(/^[a-z]+:\/\//, "").split("/")[0]) === root);
  }
  // @end-shared isFirstParty

  // @shared logOwnDomainsHint
  function logOwnDomainsHint(thirdPartyCount) {
    if (OWN_DOMAINS.length === 0 && thirdPartyCount > 0) {
      console.log(
        '%cℹ️ OWN_DOMAINS is empty. If this site serves its own assets from other domains (for example a CDN), add them at the top of the snippet, such as const OWN_DOMAINS = ["cdn.example.net"];, and run it again so they count as first party.',
        "color: #3b82f6;"
      );
    }
  }
  // @end-shared logOwnDomainsHint

  // A script whose URL cannot be parsed counts as first party
  const isFirstPartyUrl = (url) => {
    try {
      return isFirstParty(new URL(url).hostname);
    } catch {
      return true;
    }
  };

  // Get resource timing data
  const resourceTiming = new Map();
  performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e)).forEach((r) => {
    if (r.initiatorType === "script") {
      resourceTiming.set(r.name, {
        size: r.transferSize || 0,
        // Cross-origin scripts without Timing-Allow-Origin report every size as 0
        sizeKnown: r.transferSize > 0 || r.encodedBodySize > 0 || r.decodedBodySize > 0,
        duration: r.duration,
        startTime: r.startTime,
      });
    }
  });

  // Analyze all external scripts
  const externalScripts = Array.from(document.querySelectorAll("script[src]"));

  // Analyze inline scripts
  const inlineScripts = Array.from(document.querySelectorAll("script:not([src])"))
    .filter((s) => s.innerHTML.trim().length > 0);

  const scripts = externalScripts.map((script) => {
    const src = script.src;
    const timing = resourceTiming.get(src) || {};
    const inHead = script.closest("head") !== null;
    const isModule = script.type === "module";
    const isAsync = script.async;
    const isDefer = script.defer;

    // Determine loading strategy
    let strategy = "blocking";
    if (isModule && isAsync) {
      strategy = "async module";
    } else if (isModule) {
      strategy = "module";
    } else if (isAsync) {
      strategy = "async";
    } else if (isDefer) {
      strategy = "defer";
    }

    const isBlocking = strategy === "blocking";
    const firstParty = isFirstPartyUrl(src);

    return {
      src,
      shortSrc: src.split("/").pop()?.split("?")[0] || src,
      strategy,
      isBlocking,
      inHead,
      firstParty,
      isModule,
      isAsync,
      isDefer,
      size: timing.size || 0,
      sizeKnown: !!timing.sizeKnown,
      duration: timing.duration || 0,
      startTime: timing.startTime || 0,
      element: script,
    };
  });

  // Categorize scripts
  const blocking = scripts.filter((s) => s.isBlocking);
  const blockingInHead = blocking.filter((s) => s.inHead);
  const asyncScripts = scripts.filter((s) => s.strategy === "async" || s.strategy === "async module");
  const deferScripts = scripts.filter((s) => s.strategy === "defer");
  const moduleScripts = scripts.filter((s) => s.strategy === "module");
  const thirdPartyBlocking = blocking.filter((s) => !s.firstParty);

  // Calculate totals
  const totalSize = scripts.reduce((sum, s) => sum + s.size, 0);
  const blockingSize = blocking.reduce((sum, s) => sum + s.size, 0);

  // Rating
  let rating, ratingColor;
  if (blockingInHead.length === 0) {
    rating = "Good";
    ratingColor = "#22c55e";
  } else if (blockingInHead.length <= 2 && thirdPartyBlocking.length === 0) {
    rating = "Needs Review";
    ratingColor = "#f59e0b";
  } else {
    rating = "Needs Optimization";
    ratingColor = "#ef4444";
  }

  // Display results
  console.group("%c📜 Script Loading Analysis", "font-weight: bold; font-size: 14px;");

  // Summary
  console.log("");
  console.log("%cSummary:", "font-weight: bold;");
  console.log(`   Total external scripts: ${scripts.length}`);
  console.log(`   Total inline scripts: ${inlineScripts.length}`);
  console.log(`   Total size: ${formatBytes(totalSize)}`);
  console.log("");
  console.log("%cBy loading strategy:", "font-weight: bold;");
  console.log(`   🔴 Blocking: ${blocking.length} (${formatBytes(blockingSize)})`);
  console.log(`   ⚡ Async: ${asyncScripts.length}`);
  console.log(`   📋 Defer: ${deferScripts.length}`);
  console.log(`   📦 Module: ${moduleScripts.length}`);
  console.log("");
  console.log(`   Rating: %c${rating}`, `color: ${ratingColor}; font-weight: bold;`);

  // Detailed table
  if (scripts.length > 0) {
    console.log("");
    console.group("%c📊 External Scripts", "color: #3b82f6; font-weight: bold;");

    const tableData = scripts
      .sort((a, b) => {
        // Sort: blocking first, then by size
        if (a.isBlocking !== b.isBlocking) return a.isBlocking ? -1 : 1;
        return b.size - a.size;
      })
      .map((s) => ({
        Script: s.shortSrc,
        Strategy: s.isBlocking ? `🔴 ${s.strategy}` : s.strategy,
        Location: s.inHead ? "head" : "body",
        Party: s.firstParty ? "1st" : "3rd",
        Size: formatBytes(s.size),
        Duration: formatMs(s.duration),
      }));

    console.table(tableData);

    // Elements for inspection
    console.log("");
    console.log("%c🔎 Elements (sorted by load order):", "font-weight: bold;");
    scripts
      .sort((a, b) => a.startTime - b.startTime)
      .forEach((s, i) => {
        const marker = s.isBlocking ? "🔴" : "✅";
        console.log(`${i + 1}. ${marker} ${s.shortSrc}`, s.element);
      });

    console.groupEnd();
  }

  // Issues
  const issues = [];

  if (blockingInHead.length > 0) {
    issues.push({
      severity: "error",
      message: `${blockingInHead.length} blocking script(s) in <head>`,
      scripts: blockingInHead,
      fix: "Add 'defer' or 'async' attribute, or move to end of <body>",
    });
  }

  if (thirdPartyBlocking.length > 0) {
    issues.push({
      severity: "error",
      message: `${thirdPartyBlocking.length} third-party blocking script(s)`,
      scripts: thirdPartyBlocking,
      fix: "Add 'async' for independent scripts, or load dynamically",
    });
  }

  const largeBlocking = blocking.filter((s) => s.size > 50 * 1024);
  if (largeBlocking.length > 0) {
    issues.push({
      severity: "warning",
      message: `${largeBlocking.length} large blocking script(s) (> 50 KB)`,
      scripts: largeBlocking,
      fix: "Split code, use defer, or lazy load",
    });
  }

  // Scripts that could be deferred
  const couldDefer = blocking.filter((s) => {
    // Scripts at end of body could likely use defer
    const bodyScripts = Array.from(document.body.querySelectorAll("script[src]"));
    const isLastInBody = bodyScripts.indexOf(s.element) >= bodyScripts.length - 3;
    return isLastInBody;
  });

  if (couldDefer.length > 0 && blocking.length > couldDefer.length) {
    issues.push({
      severity: "info",
      message: `${blocking.length - couldDefer.length} blocking script(s) could potentially use defer`,
      fix: "Test with defer attribute to improve parsing performance",
    });
  }

  if (issues.length > 0) {
    console.log("");
    console.group("%c⚠️ Issues Found", "color: #ef4444; font-weight: bold;");

    issues.forEach((issue) => {
      const icon = issue.severity === "error" ? "🔴" : issue.severity === "warning" ? "🟡" : "💡";
      console.log("");
      console.log(`%c${icon} ${issue.message}`, "font-weight: bold;");
      if (issue.scripts) {
        issue.scripts.forEach((s) => {
          console.log(`   • ${s.shortSrc} (${formatBytes(s.size)})`);
        });
      }
      console.log(`   → ${issue.fix}`);
    });

    console.groupEnd();
  } else if (scripts.length > 0) {
    console.log("");
    console.log(
      "%c✅ All scripts use non-blocking loading strategies!",
      "color: #22c55e; font-weight: bold;"
    );
  }

  // First/Third party breakdown
  const firstPartyScripts = scripts.filter((s) => s.firstParty);
  const thirdPartyScripts = scripts.filter((s) => !s.firstParty);

  if (thirdPartyScripts.length > 0) {
    console.log("");
    console.group("%c🌐 Third-Party Scripts", "color: #8b5cf6; font-weight: bold;");

    const thirdPartyByHost = new Map();
    thirdPartyScripts.forEach((s) => {
      try {
        const host = new URL(s.src).hostname;
        if (!thirdPartyByHost.has(host)) {
          thirdPartyByHost.set(host, { count: 0, size: 0, blocking: 0 });
        }
        const data = thirdPartyByHost.get(host);
        data.count++;
        data.size += s.size;
        if (s.isBlocking) data.blocking++;
      } catch {}
    });

    const hostTable = Array.from(thirdPartyByHost.entries())
      .sort((a, b) => b[1].size - a[1].size)
      .map(([host, data]) => ({
        Host: host,
        Scripts: data.count,
        Size: formatBytes(data.size),
        Blocking: data.blocking > 0 ? `⚠️ ${data.blocking}` : "0",
      }));

    console.table(hostTable);
    console.groupEnd();
  }

  // Best practices (defined here so firstPartyScripts/thirdPartyScripts are in scope for return)
  console.log("");
  console.group("%c📝 Best Practices", "color: #3b82f6; font-weight: bold;");
  console.log("");
  console.log("%cFor most scripts (need DOM, run in order):", "font-weight: bold;");
  console.log('%c<script src="app.js" defer></script>', "font-family: monospace; color: #22c55e;");
  console.log("");
  console.log("%cFor independent scripts (analytics, ads):", "font-weight: bold;");
  console.log('%c<script src="analytics.js" async></script>', "font-family: monospace; color: #22c55e;");
  console.log("");
  console.log("%cFor ES modules:", "font-weight: bold;");
  console.log('%c<script type="module" src="app.mjs"></script>', "font-family: monospace; color: #22c55e;");
  console.log("");
  console.log("%cFor critical inline scripts:", "font-weight: bold;");
  console.log('%c<script>/* Only essential initialization */</script>', "font-family: monospace; color: #22c55e;");
  logOwnDomainsHint(thirdPartyScripts.length);
  console.groupEnd();

  console.groupEnd();

  const agentRating = blockingInHead.length === 0 ? "good" :
    (blockingInHead.length <= 2 && thirdPartyBlocking.length === 0) ? "needs-improvement" : "poor";
  // The list keeps the 50 scripts to look at first (render-blocking, then third-party, then the largest); count and details cover all of them
  const MAX_ITEMS = 50;
  const rankedScripts = [...scripts].sort(
    (a, b) =>
      Number(b.isBlocking) - Number(a.isBlocking) ||
      Number(a.firstParty) - Number(b.firstParty) ||
      b.size - a.size
  );
  return {
    script: "Script-Loading",
    status: "ok",
    count: scripts.length,
    corsLimitedAnalysis: scripts.some((s) => !s.sizeKnown),
    rating: agentRating,
    details: {
      totalSizeBytes: totalSize,
      sizeUnknownCount: scripts.filter((s) => !s.sizeKnown).length,
      byStrategy: {
        blocking: blocking.length,
        async: asyncScripts.length,
        defer: deferScripts.length,
        module: moduleScripts.length,
      },
      byParty: {
        firstParty: firstPartyScripts.length,
        thirdParty: thirdPartyScripts.length,
      },
      thirdPartyBlockingCount: thirdPartyBlocking.length,
    },
    items: rankedScripts.slice(0, MAX_ITEMS).map((s) => ({
      url: s.src,
      shortName: s.shortSrc,
      strategy: s.strategy,
      location: s.inHead ? "head" : "body",
      party: s.firstParty ? "first" : "third",
      sizeBytes: s.size,
      sizeKnown: s.sizeKnown,
      durationMs: Math.round(s.duration),
    })),
    issues: [
      ...issues.map((i) => ({ severity: i.severity, message: i.message })),
      ...(scripts.some((s) => !s.sizeKnown)
        ? [{ severity: "info", message: `${scripts.filter((s) => !s.sizeKnown).length} script(s) have an unknown size (missing Timing-Allow-Origin); total size is a lower bound` }]
        : []),
    ],
  };
})();

Understanding the results

Summary section

  • Total scripts (external and inline)
  • Breakdown by loading strategy
  • Overall rating based on blocking scripts

Loading strategy legend

  • Blocking: Stops HTML parsing (bad for performance)
  • Async: Downloads in parallel, executes when ready
  • Defer: Downloads in parallel, executes after parsing
  • Module: ES module (deferred by default)

Details table

ColumnDescription
ScriptFilename
StrategyLoading strategy with indicator
Locationhead or body
Party1st (your domain) or 3rd (external)
SizeTransfer size; unknown for cross-origin scripts without Timing-Allow-Origin (sizeKnown: false in the returned items)
DurationTime to load

Issues detected

IssueSeverityWhy It Matters
Blocking scripts in <head>ErrorDelays all content below
Third-party blocking scriptsErrorExternal dependency blocking your page
Large blocking scripts (> 50 KB)WarningSignificant parsing delay

Script loading decision tree

Decision tree for script loading: critical tiny scripts are inlined, other critical scripts go at the end of body or are preloaded, non-critical scripts use defer when order matters and async otherwiseConnections: Script Loading Strategy to Critical for initial render?; Critical for initial render? to Very small < 1 KB? (Yes); Very small < 1 KB? to Inline the script <script>...</script> (Yes); Very small < 1 KB? to Place at end of <body> or use preload (No); Critical for initial render? to Needs to run in order? (No); Needs to run in order? to Use defer <script defer> (Yes); Needs to run in order? to Use async <script async> (No).Script Loading StrategyCritical forinitial render?Very small< 1 KB?Inline the script<script>...</script>Place at end of <body>or use preloadNeeds to runin order?Use defer<script defer>Use async<script async>YesYesNoNoYesNo
Decision tree for script loading: critical tiny scripts are inlined, other critical scripts go at the end of body or are preloaded, non-critical scripts use defer when order matters and async otherwiseConnections: Script Loading Strategy to Critical for initial render?; Critical for initial render? to Very small < 1 KB? (Yes); Very small < 1 KB? to Inline the script <script>...</script> (Yes); Very small < 1 KB? to Place at end of <body> or use preload (No); Critical for initial render? to Needs to run in order? (No); Needs to run in order? to Use defer <script defer> (Yes); Needs to run in order? to Use async <script async> (No).Script LoadingStrategyCritical forinitial render?Very small< 1 KB?Inline the script<script>...</script>Place at end of<body>or use preloadNeeds to runin order?Use defer<script defer>Use async<script async>YesYesNoNoYesNo

Scripts are first party when they share the page’s root domain. A site’s own CDN on a different root domain, such as bbci.co.uk for bbc.co.uk, is reported as a third party, because the comparison uses root domains only. To count those domains as first party, list them in the OWN_DOMAINS constant at the top of the snippet, for example const OWN_DOMAINS = ["bbci.co.uk", "bbc.co.uk"];. Entries can be domains or URLs. While the list is empty and the page has third-party resources, the snippet prints a reminder in the console.

Browser support

FeatureChromeEdgeFirefoxSafari
PerformanceResourceTiming.transferSize (opens in a new tab) 54 17 45 16.4
Document.scripts (opens in a new tab) 36 12 61 10.1
All of the above 54 17 61 16.4

Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.

Further reading

Last updated on
MIT 2026 © Joan León | @nucliweb