Andrew Welch · Insights · #frontend #performance #craftcms

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

Speed up your Craft CMS Templates with Eager Loading

Eager-Load­ing Ele­ments allows you to speed up your Craft CMS tem­plates by fetch­ing entries from the data­base more efficiently.

Craft Cms Eager Loading Elements

One of the won­der­ful things about Craft CMS is that you have great flex­i­bil­i­ty in design­ing both the fron­tend and the back­end of your website.

This is due in large part to Craft’s con­cept of Ele­ments. Most of the things you inter­act with on the back­end such as Entries, Cat­e­gories, Assets, Users, and even Matrix blocks are all Ele­ments. Ele­ments are all stored sep­a­rate­ly, so any ref­er­ence from one Ele­ment to anoth­er is a rela­tion.

So if you cre­ate an Entry with an Asset field in it, the Asset isn’t actu­al­ly stored in the Entry itself; instead, the Entry Ele­ment just points to the Asset Element.

This is a key concept, because understanding how Elements are stored allows you to understand how they are loaded

The fol­low­ing things in Craft CMS are all Elements:

  • Assets
  • Cat­e­gories
  • Entries
  • Glob­al Sets
  • Matrix Blocks
  • Tags
  • Users

There are also Ele­ments added by some plu­g­ins, such as Craft Com­merce Products.

Let’s take a very sim­ple exam­ple of a code pat­tern you prob­a­bly see every day:

{% for entry in craft.entries.section('blog').limit(null) %}
    <img src="{{ entry.blogImage.first().url }}" alt="{{ entry.title }}" />
{% endfor %}

Here we’re just loop­ing through all of our Entries in the blog sec­tion, and out­putting the blogImage Asset url in an <img> tag.

In order to do this, Craft has to first fetch all of the blog Entries, so it knows how many blog Entries there are. Then each time through the loop it access­es the blogImage Asset to get its URL.

But remem­ber the Asset Ele­ment isn’t actu­al­ly stored in the Entry Ele­ment, it’s a rela­tion that points to the Asset Ele­ment. So each time through the loop, Craft has to do anoth­er data­base access to get the Asset Ele­ment. Only then can it out­put the URL.

Database accesses are costly, because they require building a query, then accessing the database, which then reads from the storage device

This is a clas­sic n+1 prob­lem, which just means that as the num­ber of things goes up, the time it takes to do some­thing with them increas­es linearly.

This is rough­ly equiv­a­lent to ask­ing your sig­nif­i­cant oth­er to run to the store to pick some­thing up for you… and then as soon as they get back, you ask them to run back out and pick up some­thing else. And then repeat this dozens, or even hun­dreds of times. Don’t try this at home, folks.

Woman Headache

Lazy load­ing your shop­ping list is not rec­om­mend­ed for mar­i­tal bliss

Nor­mal­ly this type of lazy load­ing” that Craft does is a good thing, because it can’t know what rela­tions you’ll need to access ahead of time. If Craft loaded every­thing every time, it’d be like buy­ing out the entire store with­out regard for what you actu­al­ly need.

So Craft just loads rela­tions like Assets as you access them. But this also can be inef­fi­cient if we know we’re going to need cer­tain things.

If only there was a way to tell Craft what we need ahead of time…

Link Enter Eager-Loading Elements

Con­sid­er our the­o­ret­i­cal (and poten­tial­ly dan­ger­ous) shop­ping anal­o­gy. Instead of adding anoth­er round trip to the store for our sig­nif­i­cant oth­er for each thing we want, would­n’t it be nice if we did some­thing sen­si­ble? Some­thing like prepar­ing a shop­ping list ahead of time, and poten­tial­ly sav­ing our relationship?

This is exactly what Eager Loading allows you to do: tell Craft ahead of time what you’re going to need

Craft CMS intro­duced the con­cept of Eager-Load­ing Ele­ments in Craft 2.6. Lever­ag­ing Eager-Load­ing Ele­ments, our exam­ple would change to be this:

{% for entry in craft.entries.section('blog').with(['blogImage']).limit(null) %}
    <img src="{{ entry.blogImage[0].url }}" alt="{{ entry.title }}" />
{% endfor %}

Two things to note here:

  • We added .with(['blogImage']) to our craft.entries, which tells Craft Hey, while you’re fetch­ing these entries, we want all of the blogImage Asset Ele­ments, too”
  • We changed .first() to [0] (tech­ni­cal­ly, we’re going from Object syn­tax to Array syntax)

What’s hap­pened here is we’ve asked Craft to give us our Entries, and while it’s fetch­ing them, to also fetch each blogImage Asset Ele­ment in each Entry, too. 

Under the hood, when you ask for Entries that have rela­tions to oth­er Ele­ments, the thing you get back is an Ele­ment­Cri­te­ri­aMod­el that tells Craft how to retrieve the Ele­ment (in this case, an Asset). But it does­n’t actu­al­ly retrieve it until you access it. This is lazy loading.

When you ask for Entries with rela­tions to oth­er Ele­ments that are Eager Loaded, instead of an ElementCriteraModel, Craft fetch­es the rela­tions and returns an array of the Asset ele­ments. Thus the change from .first() Object syn­tax to [0] Array syntax.

For details on the syn­tax you can use for Eager Load­ing, check out the Craft help arti­cle Eager-Load­ing Ele­ments.

Link Real World Examples

The rea­son we want to use Eager Load­ing is to reduce the num­ber of SQL data­base queries need­ed to load a page, and thus increase performance.

So let’s take a look at some real world examples.

Welcome To The Real World

We will once again use this web­site you’re read­ing right now as our guinea pig. Here’s what the Field Lay­out looks like for our blog section:

Blog Field Layout

blog chan­nel field layout

Here’s what the fields actu­al­ly are:

  • Blog Cat­e­go­ry → Cat­e­go­ry Ele­ment
  • Blog Tags → Tag Ele­ment
  • Blog Sum­ma­ry → Rich Text Field
  • Blog Image → Asset Ele­ment
  • Blog Con­tent → Matrix Block Ele­ment
  • Blog SEOSEO­mat­ic Meta

So it’s a pret­ty sim­ple set­up, we only have 6 fields, but 4 out of the 6 fields are Ele­ments, and thus are rela­tions that we could be Eager Load­ing. The Blog Con­tent field is a con­tent builder” Matrix Block for our blog con­tent, as described in the Cre­at­ing a Con­tent Builder in Craft CMS article.

The Matrix Blocks for the Blog Con­tent look like this:

Content Builder Image Block

Blog Con­tent Matrix Block

Only the image Matrix Block type has a rela­tion, in that it has an Asset Ele­ment in it (actu­al­ly a Focus Point field, but it’s the same thing).

The first thing we should do before we address any per­for­mance issues is to gath­er data to see where the per­for­mance bot­tle­necks are. All of the tim­ing tests are done in local dev on our Home­stead VM, with dev­Mode on and all caching is dis­abled so we can see the raw results. 

Because we’re doing all of these tim­ing tests in an envi­ron­ment with all sorts of debug­ging enabled, it’s going to be sig­nif­i­cant­ly slow­er in absolute terms than on live pro­duc­tion. How­ev­er, the rel­a­tive tim­ings should be rough­ly similar.

Here are the data­base queries for the Blog Index page:

Blog Index Queries Normal

Blog Index data­base queries with nor­mal loading

Wow, so we have 120 data­base queries just to dis­play a sim­ple page of the 9 most recent blog entries, and a thumb­nail image of each. Also don’t just look at the num­ber of queries, look at the time: 1.24399s

Here’s what the code looks like that loads our entries:

{% set entries = craft.entries ({
            section: 'blog',
            order: 'postDate desc',
            limit: limitBlogs,
            relatedTo: relatedBlogs,
        }) %}

        {% for entry in entries %}

Let’s apply a bit of what we’ve learned, and Eager Load some of these Ele­ments that are in our Entry as relations:

{% set entries = craft.entries ({
            section: 'blog',
            with: ['blogImage', 'blogCategory', 'blogTags'],
            order: 'postDate desc',
            limit: limitBlogs,
            relatedTo: relatedBlogs,
        }) %}

        {% for entry in entries %}

So we’ve just added the line with: ['blogImage', 'blogCategory', 'blogTags'], to tell Craft we want to Eager Load these things, because we know we’ll be using them.

Note that even though the blogContent is a Matrix Block Ele­ment, and thus it could be a can­di­date for Eager Load­ing, we’re not ask­ing for it to be Eager Loaded. That’s because we’re not using the blogContent at all on the Blog Index page, so there’s no rea­son to load it!

So how’d we do? Here’s what the tim­ings look like after we’ve added Eager Loading:

Blog Index Queries Eager Loading

Blog Index data­base queries with Eager Loading

Holy moly! We near­ly cut the num­ber of queries in half, down to 62 queries from 120, and the time down to 1.03165s from 1.24399s. That’s a 49% sav­ings in the num­ber of queries, and an 18% sav­ings in the amount of time that it took.

In A Vacuum Of Space

While an 18% sav­ings in time may not seem like a big deal in a vac­u­um, in real world con­di­tions where there are mul­ti­ple peo­ple hit­ting your web­site con­cur­rent­ly, there will be con­tention for the data­base that snow­balls. Every­one has to get in line to have their data­base queries ful­filled, so any delay can have a cas­cad­ing effect on the web­site’s performance.

Our Blog Index page is pret­ty light­weight any­way, it only loads the most recent 9 blog entries. Let’s scale it up a bit, and have it load all of the blog entries on the page (about 40 at cur­rent count):

Blog Index Queries Normal All

All Blog Index entries with nor­mal loading

Now we’re up to a whop­ping 352 queries that takes 3.25565s. Let’s see what it looks like if we add in Eager Loading:

Blog Index Queries Eager Loading All

All Blog Index entries with Eager Loading

With Eager Load­ing added, we’ve got­ten it down to 120 queries from 352 queries, and the time down to 2.42931s from 3.25565s. That’s a 76% sav­ings in the num­ber of queries, and a 26% sav­ings in the amount of time that it took.

I think it’s pret­ty clear that as the num­ber of things goes up, the sav­ings we get from Eager Load­ing just gets bet­ter and bet­ter. This will make our web­site scale up nice­ly as the client adds content.

Link Eager Load All The Things!

Keep in mind that although the exam­ple we used was with Entry Ele­ments, any­thing that’s an Ele­ment can poten­tial­ly have rela­tions to oth­er Elements.

Eager Load All The Things

For exam­ple, you might have added Cat­e­go­ry fields to your Assets. These too are things that are rela­tions, can be eager loaded, for example:

{% set asset = entry.blogImage.with(['someCategory']).first() %}
{% set entries = craft.entries ({
            section: 'blog',
            with: ['blogImage.someCategory', 'blogCategory', 'blogTags'],
            order: 'postDate desc',
            limit: limitBlogs,
            relatedTo: relatedBlogs,
        }) %}

        {% for entry in entries %}

The dot syn­tax” 'blogImage.someCategory' tells Craft to Eager Load the blogImage Asset from the Entry, and while it’s at it, also Eager Load the someCategory Cat­e­go­ry from the Asset.

Most everything in Craft is an Element; you can Eager Load any Element into any Element, even nested Elements!

This ends up being a pret­ty pow­er­ful way to design your back­end con­ve­nient­ly with nest­ed Ele­ments, but not suf­fer a per­for­mance penal­ty on the fron­tend when you go to load them.

Just tell Craft what you want Eager Loaded — no mat­ter how deeply nest­ed — and away you go!

Link What About Auto-Injected Entry’s?

One nice thing that Craft does for you is it auto-injects an entry or category vari­able for Entry or Cat­e­go­ry Tem­plates. It’s a nice con­ve­nience, because then you can just access the entry vari­able with­out wor­ry­ing about pars­ing the URL, and load­ing the appro­pri­ate Entry.

The only down­side is because the Entry has already been loaded, we don’t get a chance to tell Craft what we want Eager Loaded with that Entry.

Remem­ber, though, that our Entry will get pre­loaded with the Ele­ment­Cri­te­ri­aMod­el for any of the Ele­ments that are lazy loaded. So we can just do some­thing like we did in the first exam­ple to eager load Ele­ments that are in our entry: 

{% set blocks = entry.blogContent.with(['image:image']) %}

{% for block in blocks %}

What we’re doing is ask­ing Craft to load all of our blogContent Matrix Block Ele­ments with the field image for any Matrix Block of the type image.

This is cer­tain­ly bet­ter than lazy load­ing the image Asset every time through the loop, but it can end up being a lit­tle ver­bose in your tem­plates, espe­cial­ly if you have a whole lot of Ele­ments in your Entries that you have to explic­it­ly Eager Load.

Eager Beaver

So I wrote a small plu­g­in Eager Beaver for Craft 2.6.x & Eager Beaver for Craft 3.x. This plu­g­in just lets you use the same Eager Load­ing syn­tax for already loaded entry (or what­ev­er) Ele­ments that you use for Ele­ments that you load yourself.

For exam­ple, this is what I use on _entry.twig tem­plate for each Blog entry:

{% do eagerLoadElements(entry, [
    'author.userPicture',
    'blogCategory',
    'blogImage',
    'blogContent.image:image'
]) %}

The first para­me­ter to eagerLoadElements() is just the Ele­ment we want to eager load things into, and the sec­ond para­me­ter is the same with that you use for nor­mal Eager-Load­ing Ele­ments.

I pre­fer Eager Load­ing every­thing in one fell swoop this way, rather than doing it à la carte through­out my tem­plates. But either method works, use what­ev­er you prefer.

Note that on Craft 3, you can do the exact same thing that the Eager Beaver plu­g­in does by using craft.app.elements.eagerLoadElements:

{% do craft.app.elements.eagerLoadElements(
    className(entry),
    [entry],
    ['assetsField', 'categoriesField.assetField', 'matrixField.blockType:assetsField']
) %}

The first para­me­ter is the class name of the ele­ment type we’re eager load­ing ele­ments into (in this case, an entry). The sec­ond para­me­ter is an array of ele­ments we’re eager load­ing ele­ments into (in this case, an array with just our entry in it). Final­ly, the third para­me­ter is dot-nota­tion of what ele­ments we want to eager load. c.f.: eager­Load­Ele­ments()

Link What About the Element API?

Some­thing that is get­ting more and more com­mon these days is Craft being accessed via the Ele­ment API by fron­tend JavaScript or oth­er ser­vices. This is exact­ly how we did things in the Auto­com­plete Search with the Ele­ment API & Vue­JS article.

Well, we can use Eager Load­ing in the Ele­ment API as well! We can just do some­thing like this:

<?php
namespace Craft;

return [
    'endpoints' => [
        'api/search' => [
            'elementType' => 'Entry',
            'paginate' => false,
            'criteria' => [
                'section' => 'blog',
                'with' => [
                    'blogCategory',
                    'blogTags'
                ],
                'limit' => 9,

Note the with key; look famil­iar? Yep, we can eager load things here just like we can every­where else.

It’s impor­tant not to over­look opti­miz­ing your API end­points like this as well, espe­cial­ly as they become more and more com­mon­ly used.

Link Don’t Be Too Eager

We’ve focused on using caching to help with per­for­mance in the The Craft {% cache %} Tag In-Depth and the Sta­t­ic Page Caching with Craft CMS arti­cles. It’s impor­tant to not use caching as a way to mask or var­nish-over per­for­mance problems.

Varnishing Over

We should opti­mize our tem­plates with things like Eager Load­ing before we add a caching lay­er on top to help with concurrency.

We don’t want our cache miss­es to bog down the web­site, and real­ly, things like Eager Load­ing are pret­ty sim­ple to do.

Go forth, and Eager Load!