# Scripts loading

> Source: <https://webperf-snippets.net/Loading/Script-Loading>

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

Interactive demo: [Interactive timeline of how a normal, async, and defer script each affect HTML parsing, downloading, and execution](https://webperf-snippets.net/demos/script-loading-timeline.html). Switch between \<script>, async, and defer, then step through to see when the parser blocks, when the script downloads, and when it executes.

### Loading strategy comparison

| Strategy         | Downloads        | Executes      | Blocks Parsing | Order Guaranteed |
| ---------------- | ---------------- | ------------- | -------------- | ---------------- |
| **Normal**       | When encountered | Immediately   | ✅ Yes          | ✅ Yes            |
| **async**        | In parallel      | When ready    | Briefly        | ❌ No             |
| **defer**        | In parallel      | After parsing | ❌ No           | ✅ Yes            |
| **module**       | In parallel      | After parsing | ❌ No           | ✅ Yes            |
| **async module** | In parallel      | When ready    | Briefly        | ❌ No             |

### When to use each

| Strategy   | Best For                                             |
| ---------- | ---------------------------------------------------- |
| **Normal** | Critical scripts that must run before content (rare) |
| **async**  | Independent scripts (analytics, ads)                 |
| **defer**  | Scripts that need DOM, must run in order             |
| **module** | Modern ES modules with imports                       |

> **Note:** **Performance tip:** Most scripts should use `defer`. Use `async` only for truly independent scripts that don't rely on DOM or other scripts.

### Snippet

```js
// 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

| Column   | Description                                                                                                              |
| -------- | ------------------------------------------------------------------------------------------------------------------------ |
| Script   | Filename                                                                                                                 |
| Strategy | Loading strategy with indicator                                                                                          |
| Location | head or body                                                                                                             |
| Party    | 1st (your domain) or 3rd (external)                                                                                      |
| Size     | Transfer size; unknown for cross-origin scripts without `Timing-Allow-Origin` (`sizeKnown: false` in the returned items) |
| Duration | Time to load                                                                                                             |

### Issues detected

| Issue                            | Severity | Why It Matters                         |
| -------------------------------- | -------- | -------------------------------------- |
| Blocking scripts in `<head>`     | Error    | Delays all content below               |
| Third-party blocking scripts     | Error    | External dependency blocking your page |
| Large blocking scripts (> 50 KB) | Warning  | Significant parsing delay              |

### Script loading decision tree

Diagram: 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 otherwise

> **Note:** 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

| Feature                                                                                                                     | Chrome | Edge | Firefox | Safari |
| --------------------------------------------------------------------------------------------------------------------------- | :----: | :--: | :-----: | :----: |
| [PerformanceResourceTiming.transferSize](https://developer.mozilla.org/docs/Web/API/PerformanceResourceTiming/transferSize) |  ✅ 54  | ✅ 17 |   ✅ 45  | ✅ 16.4 |
| [Document.scripts](https://developer.mozilla.org/docs/Web/API/Document/scripts)                                             |  ✅ 36  | ✅ 12 |   ✅ 61  | ✅ 10.1 |
| All of the above                                                                                                            |  ✅ 54  | ✅ 17 |   ✅ 61  | ✅ 16.4 |

Source: [MDN browser compatibility data](https://github.com/mdn/browser-compat-data), version 8.1.4.

### Further reading

- [Efficiently load JavaScript](https://web.dev/articles/efficiently-load-third-party-javascript) | web.dev
- [Script loading strategies](https://developer.chrome.com/docs/lighthouse/performance/render-blocking-resources) | Chrome Developers
- [JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) | MDN
- [async vs defer](https://javascript.info/script-async-defer) | javascript.info
