squirrelscan
GitHub

Critical request chains: the blocking head resources counted

Blocking stylesheets and scripts in head, plus every inline CSS @import a browser fetches. squirrel counts each as a link in the chain before first paint.

What a critical request chain is

A critical request chain is a sequence of requests that must complete in order before the page can render. The HTML arrives, the parser finds a stylesheet, the stylesheet arrives, and inside it an @import names another file that has to be fetched before anything can paint. Each link costs a full round trip, and round trips are the part of the load that a faster connection does not fix.

Depth matters more than count. Four independent blocking resources fetch in parallel; four chained ones fetch one after another. @import is the worst offender because the browser cannot even discover the second URL until the first file is parsed.

What squirrel checks

The rule runs on every crawled page and emits one check:

  • critical-request-chains, at info status with the message No <head> element found when the page has no head.
  • critical-request-chains, warning with the message N critical request chain(s) found and listing up to ten members as items. A stylesheet reads CSS: <href>, a script JS: <src>, and the page’s @imports are folded into one entry reading N CSS @import(s) found (each adds a network round-trip). The details record the blocking CSS count, the blocking JS count, the import count, the number of preloads and any overflow.
  • critical-request-chains, passing with the message No critical request chains detected, with the preload count in the details.

Three things are counted, all from <head> except the imports. A blocking stylesheet is a link[rel="stylesheet"] that is neither media="print" nor disabled. A blocking script is a classic script[src] with a non-empty src and neither async nor defer. A type="module" script is deferred by default, and a classic script with nomodule or a script whose type is not JavaScript never runs in a modern browser, so none of those count. In every <style> element on the page, each @import a browser would fetch is counted, and the total contributes exactly one entry to the list rather than one entry each. Comments, strings and url() are read as single tokens, the way a browser reads them, so an @import inside a comment (/* @import url(a.css); */), a string or a url() is not an import. Neither is one that comes after any other rule, or one inside a block such as @media: a browser ignores those and never fetches them. Only @charset and, before the first import, @layer statements may come before an import. An @import with no URL is dropped too.

The count is a count of blocking resources rather than a measured chain depth, since a static read of the HTML cannot see what a stylesheet fetches once parsed.

Elements inside <noscript> are skipped: a browser with scripting enabled reads <noscript> content as plain text, so nothing in it loads or renders. See content inside noscript.

How to fix it

<style>/* styles the first screen needs */</style>
<link rel="stylesheet" href="/rest.css" media="print" onload="this.media='all'" />
<script src="/app.js" defer></script>

Replace every @import with a <link>, which lets the browser fetch in parallel instead of serially. Add defer to scripts and inline the styles the first screen needs.

The limit worth knowing is type="module", which this rule treats as non-blocking. A module script is deferred by default, so that is correct for parsing, but its dependency graph is itself a chain the rule cannot see, which makes the count a floor.

Rule IDperf/critical-request-chains
CategoryPerformance
ScopePer-page
Severitywarning
Weight5/10

Enable / disable

Disable this rule

[rules]
disable = ["perf/critical-request-chains"]

Disable all Performance rules

[rules]
disable = ["perf/*"]

Enable only this rule

[rules]
enable = ["perf/critical-request-chains"]

enable replaces the default ["*"], so this is the only rule selected. Leave disable out: a rule that matches any disable pattern is skipped even when enable names it. Only an enabled setting under a rule’s own [rule_options] table overrides both lists.

Performance findings ship in every audit next to the SEO, security and agent experience rules. See Core Web Vitals for how an agent works through a report.

References

Check your site

Run squirrel audit https://example.com and open the Performance section of the report. Every blocking resource is listed with its URL. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.

Type to search…

↑↓ navigate↵ openesc close