Template analysis

Microscope reads every .twig file in your site as text and applies eight structural rules. It never executes a template or renders a page.

How it reads a template

Each file is parsed into a structure of Twig tags and their containment, then the rules run over it. Two details matter:

  • Containment is tracked by byte offset, not by line number. A one-line {% for x in xs %}{{ craft.entries().all() }}{% endfor %} is entirely inside a loop, but every part of it shares the opening tag’s line — and that is one of the commonest ways the mistake gets written. A line-based analyser misses it silently.
  • Unknown tags don’t break the structure. The parser matches an {% endfoo %} to its {% foo %} rather than blindly popping the top of the stack, so a tag from a plugin it has never heard of doesn’t corrupt everything after it.

Findings name the template and the line, and include the offending snippet. Where a rule fires many times, the report shows a table of occurrences.

The rules

Queries in loops

A new element query started inside a {% for %} loop. This is the single most expensive mistake it is possible to make in a Craft template, and the one that most reliably explains “the site is fine locally but slow in production”: the query count scales with the number of rows, so it looks harmless on a page with three entries and falls over on a page with three hundred.

The fix is almost always eager loading — fetch the related elements once, before the loop, with .with().

Relations in loops

A relational field executed inside a loop — entry.someField.all(), with no craft. in sight. It reads like property access, which is exactly why it gets missed, but a relational field is an unexecuted element query, and calling .all(), .one() or .count() on it inside a loop runs it once per row.

The fix is again .with() on the outer query.

Unbounded queries

An element query with no .limit(). Craft’s element queries return everything by default, so a template written against a section with twelve entries keeps working as that section reaches twelve thousand — it just gets slower every month, which makes it very hard to attribute to any particular change.

Counting via all()

.all()|length where .count() belongs. The first hydrates every matching element into memory to measure the size of the array; the second asks the database for a number.

Transforms in loops

Image transforms requested one at a time inside a loop. Each one is a separate check for — and possibly a generation of — a derivative file.

Missing cache tag

A query-heavy template with no {% cache %} anywhere in it. This rule is advisory by design: caching is a judgement call, and a template full of queries that renders per-user content should not be cached. The rule reports the shape and leaves the decision to a person rather than pretending there is a right answer it can compute.

Includes in loops

{% include %} inside a loop without only. Without it, the entire parent context is copied into the included template on every iteration.

Image dimensions

<img> tags without intrinsic width and height, or without a loading hint. The only front-end rule in the set, and it earns its place: missing dimensions are the leading cause of layout shift, which is a third of the Core Web Vitals score and something a static scan can detect with no false positives worth worrying about.

Which templates get read

Your site’s own template root, always. You can add more directories under Microscope → Settings — one path per line, resolved from the Craft base path.

A ceiling of 1,500 templates applies by default, so a site with a large vendored template library can’t turn a scan into a filesystem crawl. Raise it if you need to, or turn template scanning off entirely. See Configuration.

Running only this area

php craft microscope/scan/run --category=templates --verbose

Template analysis is the one area where a CLI scan is exactly as accurate as a control panel one — it reads files, not runtime state.