A Two-Character Regex Fix That Was Quietly Defeating Its Own Preload Tags

A CSS preload optimization shipped, ran on every page load, and did nothing. Not "did nothing dramatic" — did nothing at all, silently, for the length of time it took someone to notice the preload tags weren't matching anything a browser would actually reuse. The fix was two characters in a regular expression.

What the module actually does

A new custom module, linkhub_performance, registers a Symfony event subscriber on KernelEvents::RESPONSE that runs late in the response cycle (priority -250, meaning after most other response processing). It takes the fully-rendered HTML response, regex-matches every <link rel="stylesheet"> tag in it, and injects a matching <link rel="preload" as="style"> tag right after the charset meta tag — the idea being that the browser sees the preload hint early and starts fetching the stylesheet before it would otherwise discover the link further down the page.

The bug

The original regex for capturing the CSS URL out of each stylesheet link tag was:

  • /<link[^>]*?rel=[\"']stylesheet[\"'][^>]*?href=[\"']([^\"']*\.css)/i

The capture group stops at \.css — it matches up through the literal characters ".css" and no further. Drupal's asset aggregation system doesn't serve plain style.css URLs, though. It serves URLs with a cache-busting query string appended, something like /sites/default/files/css/css_AbCdEf123.css?delta=0&language=en&theme=linkhub&include=.... The regex captured only the part up to and including .css, silently dropping everything after it.

Why that made the preload tags pointless rather than just imprecise

A browser matches preload hints to the resource that actually gets requested by exact URL. A preload tag pointing at /sites/default/files/css/css_AbCdEf123.css and an actual stylesheet request for /sites/default/files/css/css_AbCdEf123.css?delta=0&language=en... are, as far as the browser's resource cache is concerned, two different URLs. The preload fires, fetches a URL nothing else on the page requests, and the browser's DevTools would show exactly what you'd expect from an unused preload: a real network request, a downloaded resource, and a warning that the preloaded resource "was not used within a few seconds" — because the actual stylesheet load, with its query string intact, triggered a second, separate fetch for what was functionally the same file.

The module was doing real work — parsing the response, generating markup, inserting it in the right place — and producing markup that looked completely correct read in isolation. <link rel="preload" href="/sites/default/files/css/css_AbCdEf123.css" as="style"> is a syntactically valid, plausible-looking preload tag. Nothing about it announces that it doesn't match anything.

The fix

  • /<link[^>]*?rel=[\"']stylesheet[\"'][^>]*?href=[\"']([^\"']*\.css[^\"']*)[\"']/i

The capture group now continues past \.css with [^\"']* — any characters that aren't a quote — up to the closing quote of the href attribute. That's the entire difference: the first version's capture group ended exactly at the literal string it was looking for; the second treats that string as a marker to look past, not a boundary to stop at.

Why this class of bug is easy to ship and hard to notice

Nothing about the broken version fails visibly. The module runs without error, on every request. The generated HTML validates. A quick glance at the page source shows preload tags sitting right where they're supposed to be. The only way to actually catch this is to check whether the preloaded URL matches a real, subsequently-requested resource — either in a browser's network panel, or by tracing the regex against a real aggregated CSS URL rather than a hand-typed example without a query string. Writing the pattern against style.css as a mental model, instead of against what Drupal's aggregation system actually outputs, is enough to produce code that's syntactically fine and functionally inert. A preload hint that never matches isn't a bug that degrades performance — it adds a wasted request and changes nothing else, which is exactly why it can sit unnoticed in production without anyone's page feeling different.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Please share this article on your favorite website or platform.