1. The preprocess hook that turns off your page cache
Symptom: the site feels slow for anonymous visitors and nobody can say why. Server response time looks fine in isolation. Enabling more caching does not help.
Cause: somewhere in a custom module, a preprocess hook sets max-age to zero on a block that appears on every page. A social links block. A cart block. Something small and harmless-looking.
Drupal's cacheability metadata bubbles up. A max-age of zero on one block on every page means the page itself is uncacheable, which means Internal Page Cache is doing nothing site-wide. One line in one hook, and the whole anonymous caching layer is off.
Fix: replace the max-age with the cache contexts and tags that actually describe the variance. If a block varies by user, say user. If it depends on cart contents, tag it with the cart. Setting max-age to zero is almost never the right answer; it is what people reach for when they cannot work out which context they need.
This is the single most valuable thing an audit finds, because it is invisible, site-wide, and the fix is small.
2. N+1 entity loads inside loops
Symptom: certain pages are much slower than others and the difference does not track page weight.
Cause: referencedEntities() called inside a foreach. Ten partnership levels means the same query ran ten times. A slider making twenty-plus individual entity loads to display five images. Award pages loading related nodes one at a time.
Fix: batch them. Node::loadMultiple(), Paragraph::loadMultiple(), and move any lookup that does not change per iteration outside the loop. This is not clever code, it is just noticing.
N+1 is hard to see in review because each individual call is correct. It is only wrong in aggregate, which is exactly the kind of thing that survives code review and shows up in an audit.
3. Configuration read fresh on every request
Symptom: a stubborn baseline of database queries on every page, regardless of what the page does.
Cause: repeated ConfigPages::config() calls, or the equivalent, scattered across preprocess functions and block plugins. On one site I counted sixteen per page request, each hitting the database, most of them asking for the same thing.
Fix: a drupal_static() helper, or proper dependency injection with the value resolved once. The saving per call is small; the saving across sixteen calls on every request is not.
4. A test suite that cannot see JavaScript errors
Symptom: the suite is green. Everybody trusts it. Bugs still reach production.
Cause: Playwright and Behat assert on the DOM. If a script throws and the markup is still present, the assertions pass. A broken global.js that kills every Drupal behaviour on every page can produce zero test failures.
How you prove it: fault injection. Insert a known fault, run the suite, record whether it noticed. Repeat with different fault types: a PHP exception in a block plugin, a JavaScript syntax error, a wrong selector, a missing asset file, a renamed template element, a removed form class.
On the last site where I did this, five of ten injected faults went completely undetected, and the suite was blind to every JavaScript fault. That is not an opinion about coverage. It is a measurement.
Fix: a pageerror listener that fails the test on any uncaught exception, and a response listener that fails on 4xx or 5xx for JS and CSS assets. Two small helpers took detection from 50% to 70% on that site.
5. Meta output hardcoded in a theme template
Symptom: you have the Metatag module configured correctly and the meta description is still empty in the page source.
Cause: an empty <meta name="description"> hardcoded in html.html.twig, overriding Drupal's output on every page. Someone added it years ago as a placeholder.
Fix: delete the line. On Drupal this class of SEO problem is almost always a template bug rather than a content problem, which is why an SEO audit that only looks at the rendered output tells you what is wrong but not why. Fix it at the source and it lands on every page at once.
6. Modules enabled but never configured
Symptom: the module list says the site handles Open Graph. Sharing a link produces no preview.
Cause: the module was installed, the ticket was closed, and nobody set the defaults. Enabled is not configured.
Fix: defaults for every content type, then verify against the rendered output rather than the admin screen. This one is worth checking on any site where the module list is longer than the person maintaining it can explain.
7. No structured data at all
Symptom: competitors get rich results and you do not.
Cause: no JSON-LD anywhere. Not misconfigured, simply absent.
Fix: Organization at minimum, then the types that match what the site actually is: Product and Offer for Commerce, Article for editorial, FAQPage where you genuinely answer questions. This matters more than it used to, because AI-generated answers lean on explicit machine-readable claims rather than inferring them from prose.
8. Duplicate URLs nobody remembers creating
Symptom: two URLs serve the same content and Google has quietly picked the wrong one, or split the signals between them.
Cause: a legacy path left behind by a migration, an underscore version of a hyphenated URL, an old-index from a redesign. Deploy tooling that copies files without removing what is no longer wanted lets these accumulate for years.
Fix: 301 each duplicate to its canonical page, then remove the orphaned file. Check the canonical tag on the duplicate too; a stale one pointing at the old homepage is worse than none.
9. Third-party JavaScript that does nothing
Symptom: a poor mobile score with no obvious culprit in the site's own code.
Cause: a library loaded twice in two different forms, or a bundle kept after the feature that needed it was removed. Icon libraries are the classic: the CSS build and the SVG-JS build both loaded, when the CSS alone renders every icon on the site.
Fix: check what each third-party file is actually used for before optimising anything else. It is the highest ratio of saving to risk in the whole exercise.
10. The one I found on my own site
Auditing other people's Drupal for a living does not make you immune, and it would be dishonest to write this list without saying so. Running the same process over graith.co.uk in August 2026 turned up:
- 1.44 MB of dead JavaScript on seventeen pages. The Font Awesome SVG-JS bundle loading alongside the Font Awesome CSS, which already rendered every icon in use. Pure download, no effect. It was on my PageSpeed optimisation service page, which is its own kind of comment.
- A broken page title. A double-encoded em dash rendering as mojibake in Google's results, on the homepage, for an unknown length of time.
- Two 1024px PNGs as the LCP element on a service page, giving that page a 9.1 second mobile LCP. Converting them to WebP took 1.14 MB down to 26 KB.
- Production dependencies pinned to pre-release versions, an alpha and a beta, both years old.
Third-party payload per page went from 1.84 MB to 387 KB. The service page went from 74 to 98 on mobile. None of this was exotic either. It was there because I had not looked.
What this means if you are commissioning one
Two things follow from the list.
First, most of the value is in the invisible findings. Nothing above announced itself. No error log, no bug report, no unhappy user filing a ticket that says "your page cache is off". They are found by reading code and measuring behaviour, which is why a scan report is not an audit.
Second, an audit is worth more from someone who did not write the code. Not because your team is not good, but because a fresh reader has no memory of why a line is there. The preprocess hook with max-age zero is invisible to the person who added it for a reason that was correct in 2019.
If you want this run over your site, I do it as a fixed-price Drupal site audit: a written scope, a severity-rated report with the fixes named, and remediation quoted separately so you are free to hand the findings to your own team. Agencies commission it white-label for their clients, which is covered under white-label Drupal for agencies.
The full method, including the fault injection table from the site in point 4, is written up in the AI-powered Drupal site quality audit case study.
