Craft CMS performance optimisation

by Billy Patel
Craft CMS performance optimisation
Back to blog

Most Craft sites I get called in to fix are not slow because Craft is slow. They are slow because someone built them on the happy path and never came back to think about how the system behaves under load. The good news is that the patterns that fix a slow Craft site are stable. They have not changed much in years. Learn them once and you can keep applying them.

This is a pattern-level guide. I am not going to walk you through control panel screens because those move. I am going to walk you through the decisions that compound, the kind of thinking that produces a fast site rather than a slow site that needs rescuing later.

Caching strategy as a layered concern

Caching in Craft is not one thing. It is at least four things and you need to understand each before you start invalidating anything.

Template caching is the most local form. Wrapping an expensive Twig block in a cache tag stops Craft from re-executing it on every request. Good for fragments that are shared across many pages, like a navigation tree or a featured-articles block. Bad as a blanket solution because invalidation is your problem.

Query caching sits one level down. Craft can cache the results of a query so that the same query on the same page does not hit the database twice. Useful when you have multiple components on a page that pull from the same dataset.

Full-page caching is what most people mean when they say caching. Once a page has been generated, the HTML is stored and the next request serves the cached version without booting Craft at all. Blitz is the standard plugin for this and it is the right default for any high-traffic content site. It also handles cache invalidation when related content changes, which is the part you would otherwise spend months getting right yourself.

CDN caching sits above all of that. Cloudflare or a similar edge cache holds the HTML at the edge so the request never reaches your origin server. This is the layer that turns a fast site into a globally fast site. Configured properly it pairs with Blitz so the CDN respects the same invalidation events.

The cache invalidation question is harder than the caching question. The pattern that works is to invalidate aggressively on content change and lean on the CDN for raw delivery speed. A page is faster to regenerate from scratch than it is to serve stale, in most editorial contexts.

Eager loading and the n plus 1 problem

The single most common performance bug I see in Craft templates is the n plus 1 problem. You query a set of entries, then loop over them and access a relation inside the loop. Each access triggers another database query. A list of forty entries with three relations each can produce 120 unnecessary queries on a single page.

The fix is eager loading. Tell Craft up front which relations you intend to access and it will fetch them in a single batched query. The .with() method on entry queries is the mechanism. Pass an array of relation handles and Craft does the rest.

You can eager-load nested relations. Article with category with parent category. You can eager-load Matrix block fields. You can eager-load assets and the transforms you intend to render. If you write a template that touches a relation inside a loop, you should be eager-loading that relation. Always.

The debug toolbar in dev mode shows you the queries per request. If a list page is running more than ten or fifteen queries, you have an eager-loading problem. Fix it before you reach for any other optimisation.

Asset transforms and image strategy

Images are usually the biggest performance win on a content site. Craft has a transform system that lets you resize, crop and format images on the fly or ahead of time. The thinking that matters is when you generate the transform.

Default behaviour is to generate transforms on first request. The first visitor to a page sees a delay while the transform runs. Every visitor after that gets the cached file. On a busy site that is fine because the first visitor is rare. On a content site with long-tail traffic, where many pages are visited once a week, the first-visit penalty hits often.

Eager transforms generate the transform when the entry is saved rather than when it is requested. That moves the cost into the editorial workflow, which is the right place for it. Editors do not notice the half-second per image because they are saving and reviewing anyway.

Focal points are the other piece. Editors set the focal point per image and the crop logic respects it. That means an article hero image is composed correctly without the editor having to crop manually for each aspect ratio. Wire this into your image fields and your design system will use it everywhere.

Format choice matters. Serve modern formats with a fallback. Specify dimensions in the markup. Use the srcset attribute so the browser picks the right size for the viewport. None of this is Craft-specific but Craft makes it easy if you wire it in once.

Database query optimisation

Craft's entry query API is a thin layer over the underlying ORM, with every query parameter mapping to SQL. Knowing which parameters are cheap and which are expensive is part of writing fast Craft templates.

Filtering by section, type, ID, slug, status and date is cheap. Those are indexed columns and the database does them fast. Filtering by a custom field value is more expensive because it joins through the content table. If you filter by a custom field often, add a database index on that column. A migration to add a single index can take a 400ms query down to 5ms.

Sorting by a related field is the slowest common pattern. If you find yourself doing this on every request, consider denormalising. Store the value you need to sort by directly on the entry, populated from an event listener when the related entry changes. The added complexity is worth it if the query runs thousands of times a day.

Limit and offset are free. Always paginate large result sets. Loading a thousand entries to display ten is one of the easiest mistakes to make and one of the easiest to fix.

GraphQL query cost

If you are using Craft as a headless backend, the same eager-loading principles apply but the surface is different. GraphQL queries can ask for arbitrary nested data, which means a single innocuous-looking query from the front end can cascade into hundreds of database calls.

Craft has a query complexity limit you can set, which rejects requests that exceed a defined cost. Use it. Set it low enough to catch unintended deep queries and let the front-end team negotiate up if they need more.

Persisted queries are the other pattern worth knowing. Define your queries on the server, give them stable IDs and let the client reference the ID rather than sending the full query. That lets you cache aggressively, reject anything unknown and stop a single bad query from taking down the API.

Twig profiling and where time actually goes

Optimising the wrong thing is the most common waste of time in performance work. Before you spend a day on Blitz tuning, find out where the time is actually going.

In dev mode, the Craft debug toolbar shows you total request time, broken down by database, application bootstrap and template rendering. If your bootstrap is 80ms and your queries are 20ms, you do not have a query problem. If your queries are 600ms, you do.

For deeper analysis, Blackfire or Xdebug profiler give you line-by-line cost. They are heavier to set up but they pay back on any site you intend to maintain for years. Run them on the slowest page and the patterns will be obvious within an hour.

Hosting tier matched to need

Hosting is the cheapest performance win on a site that is undersized. A Craft site running on shared hosting that costs ten pounds a month is going to feel slow no matter how clever your template caching is. Match the hosting to the scale of the site.

For a single-site marketing build with modest traffic, a managed Craft host is usually right. Servd and Krystal both run Craft-specific environments and the operational overhead is low. For a high-traffic editorial site, a tuned VPS or a Cloudways instance gives you more headroom for the same money. For an enterprise-scale Craft application, Craft Cloud is now the official option and is designed around Craft from the ground up.

The decision goes beyond raw CPU. PHP version matters. Opcache configuration matters. The database server location relative to the app server matters. None of these things are exotic but they need to be checked.

CDN strategy that actually pairs with Craft

A CDN in front of a Craft site is the difference between fast and globally fast. Cloudflare in front of Blitz is a combination I have shipped many times and it is hard to beat for the cost.

The pattern that matters is making sure the CDN respects the same invalidation events as Blitz. When an entry changes, Blitz purges its cache, then signals the CDN to purge the same URLs. Without that link, the CDN serves stale content for hours and editors complain that the site is not updating.

Assets and transforms should be served from the CDN with long expiry headers. Anything with a filename that changes when the content changes can be cached for a year. HTML pages need shorter expiry plus active invalidation.

The patterns that compound

A fast Craft site is rarely fast because of one heroic optimisation. It is fast because someone made twenty small decisions correctly. Eager-loaded the relations. Indexed the custom field. Eager-generated the transforms. Paginated the list. Cached the navigation. Purged the CDN. Each decision saves twenty or fifty milliseconds. Together they are the difference between a 2.5 second page and a 400 millisecond page.

Core Web Vitals affect search rankings now in a way they did not five years ago. If you want the background on that, core web vitals in plain English covers what each one actually measures and why.

If you have inherited a Craft site that feels slow, the audit usually starts in the same place. Database queries first. Eager loading second. Caching third. Hosting last because it is the most expensive change for the smallest typical win. If you want help with that audit, Craft CMS development and rescue is where I cover it as a service and support and maintenance is where the ongoing performance work lives.

Frequently asked questions

What is the biggest cause of slow Craft CMS sites?

The n plus 1 query problem. A template loops over entries and accesses a relation inside the loop, triggering one extra database query per iteration. The fix is eager loading using .with() on the entry query, which batches the related data into a single query. It is the first thing I check on any slow Craft site.

Is the Blitz plugin worth it for Craft CMS?

For any content site with meaningful traffic, yes. Blitz handles full-page static caching with intelligent invalidation when related content changes. It does the difficult part of cache invalidation for you. A Blitz-cached Craft page paired with a CDN serves in tens of milliseconds.

How do I make Craft CMS images load faster?

Eager-generate the transforms on entry save rather than on first request. Set focal points on image fields so crops respect editorial intent. Serve modern formats with a fallback. Specify dimensions and srcset in the markup so the browser picks the right size for the viewport. Cache the asset files at the CDN with long expiry.

What is the right hosting for a Craft CMS site?

Match it to the scale. A managed Craft host like Servd or Krystal suits most marketing sites. A tuned VPS or Cloudways instance suits high-traffic editorial sites. Craft Cloud, the official Pixel and Tonic platform, is the right answer for enterprise-scale Craft applications because it is designed around Craft specifically.

Got a Craft site that feels slow?

If you have inherited a Craft site that is not performing or you want a performance audit before launch, get in touch and tell me what you are dealing with.

Get in touch
Message Call Email