Examples of Eager Loading Elements in Twig and PHP
By default, Craft lazy loads elements in your templates. In many cases, this works just fine and no further optimization is needed. However, if your content requires queries to the database to retrieve additional related data, you may want to consider optimizing your queries by eager loading elements.
A good rule of thumb is to consider using eager loading if you are looping through content with Relations fields (Assets, Categories, Entries, Users, Tags, and Commerce Products), Matrix Fields, or Image Transforms.
The Craft documentation provides us several examples of Eager Loading Elements, so this article will focus on extending those examples to a few more use cases, and discussing a few scenarios that the documentation doesn’t speak to directly.
Eager-Loading Multiple Elements
Our most basic example of eager-loading, we pass the handle of the field we want to eager-load to the with
parameter:
{% set entries = craft.entries({
section: 'news',
with: [
'assetFieldHandle'
]
}) %}
The with
tag accepts an array of items, so we can add as many eager-loadable items to the array as we want. We will need to explicitly define each item that we want eager loaded:
{% set entries = craft.entries({
section: 'news',
with: [
'assetFieldHandle',
'categoryFieldHandle',
'entriesFieldHandle',
'usersFieldHandle',
'tagsFieldHandle',
'productsFieldHandle',
'matrixFieldHandle',
]
}) %}
Eager-Loading Multiple Elements with Custom Parameters
While you only need to provide the handle of the item you want to eager load, you are also able to define additional custom parameters on each element you are eager-loading. To do so you need to update the syntax of your eager-loaded item from a simple string 'assetFieldHandle'
, to an array ['assetFieldHandle', { kind: 'image' }]
. In this example we are limiting our eager-loaded assets to only retrieve images.
{% set entries = craft.entries({
section: 'news',
with: [
['assetFieldHandle', { kind: 'image' }]
]
}) %}
Adding conditions like these can help us optimize our query to just return the data we need. The second parameter can accept any criteria parameters that you might use when using the standard Craft tag to query that element.
In the above example our with
parameter:
['assetFieldHandle', { kind: 'image' }]
Is doing the same thing we would do when querying an asset using the craft.assets
tag:
{% set assets = craft.assets.kind('image') %}
Or, an Asset Relations Field :
{% set assets = entry.assetFieldHandle.kind('image') %}
We can mix simple eager-loaded element handles with complex eager-loaded items with custom parameters:
{% set entries = craft.entries({
section: 'news',
with: [
['assetFieldHandle', {
kind: 'image'
}],
'categoryFieldHandle',
['entriesFieldHandle' {
before: someDate,
after: someOtherDate
}],
'usersFieldHandle',
['tagsFieldHandle'],
'productsFieldHandle',
'matrixFieldHandle',
]
}) %}
Eager-Loading Matrix Blocks and Image-Transform Indexes
There are two types of eager-loadable items that have a different syntax: Matrix Blocks and Image Transform Indexes. The principles behind eager-loading these items are the same, but the syntax is slightly different.
Eager-Loading Matrix Blocks
When we load data from a field within a Matrix Blocks, we have to identify which Block Type our field exists under.
{% set entries = craft.entries({
section: 'news',
with: [
'matrixFieldHandle.blockTypeHandle:assetFieldHandle'
]
}) %}
Note that the first two segments are separated by a .
and the second two segments are separated by a :
.
The first part matrixFieldHandle.blockTypeHandle
helps Craft identify where the field you want to eager load is. The final parameter assetFieldHandle
identifies the specific field you wish to get data for.
Eager-Loading Image Transform Indexes
Image Transform Indexes follow the same syntax as the examples we’ve seen so far, however, they differ in a different way. To eager-load Image Transform Indexes you need to trigger them with a different parameter: withTransforms
. The withTransforms
parameter is unique to assets.
{% set assets = entry.assetsField({
withTransforms: [
'assetFieldHandle'
]
}) %}
You can pre-load transforms by handle, or just by passing the parameters of your transform:
{% set assets = entry.assetsField({
withTransforms: [
'assetFieldHandle'
{ width: 600, height: 400 },
{ width: 600 }
]
}) %}
Above we are using the withTransforms
parameter on our craft.assets
tag. We can also us the withTransforms
parameter when we eager-load data for our Asset fields and Asset fields within Matrix Blocks:
{% set entries = craft.entries({
section: 'news',
with: [
'assetOneFieldHandle',
['assetTwoFieldHandle', {
withTransforms: ['thumb']
}],
['matrixFieldHandle.blockTypeHandle:assetThreeFieldHandle', {
withTransforms: [
{ width: 600, height: 400 },
{ width: 600 },
'highlight',
{ width: 180 },
'thumb'
]
}],
'assetFourFieldHandle',
'assetFiveOneFieldHandle',
]
}) %}
Eager-Loading across more Element Types
While most of the examples you see for eager-loading involve Entries and Assets and Matrix Blocks, you are not limited to these Elements. Eager-Loading is possible for all Element Types in your templates, including Element Types in third-party plugins.
Eager-Loading Categories
{% set categories = craft.categories({
group: 'topics',
with: [
'assetFieldHandle'
]
}) %}
Eager-Loading Tags
{% set tags = craft.tags({
group: 'blogTags',
with: [
'assetFieldHandle'
]
}) %}
Eager-Loading Users
{% set users = craft.users({
group: 'authors',
with: [
'assetFieldHandle'
]
}) %}
These examples are all quite simple, but the eager-loading feature behaves the same across all element types, so we apply any of the more advanced examples we’ve looked at above to any of these Elements.
Eager-Loading Elements on an Entry Detail Page
While the craft.entries
tag provides us the with
parameter to eager-load elements, on our individual entries pages, where Craft makes the EntryModel available via an entry
variable, we never have a chance to optimize our Entry query with eager-loading.
What if you have a complicated Entry with lots of Assets and a big, complicated Matrix Field with a hundred Matrix Blocks that you need to load?
The answer to this question is two-fold. First, If you are only querying a single element from the database, there are no additional benefits if you use eager-loading. The addition of the with
parameter will increase the number of queries needed to get your data, potentially leading to a less efficient query to get back a single entry.
Second, while Craft may retrieve your Entry element in the initial query, Craft waits to query the additional data for your Relations fields, Matrix Blocks, and Image Transform Indexes until you attempt to access this information by displaying it in your template. This gives you the opportunity to use eager-loading if you have a particularly large subset of data you will be outputting on the page.
Let’s assume we have an Entry with a big Matrix Field. Here is a basic template for an entry detail page:
{# Entry Detail Page #}
{{ entry.title }}
{{ entry.body }}
Above, we output the entry.title
and entry.body
fields but we never try to display our entry.matrixFieldHandle
so Craft never performs the additional queries that are needed to get back the Matrix data.
When we do decide to display that data, we can use all that we’ve learned above to optimize the query to retrieve that data. Matrix Blocks are an Element Type, so all the same syntax applies.
{# Entry Detail Page with Matrix #}
{{ entry.title }}
{{ entry.body }}
{% set blocks = entry.matrixFieldHandle.find({
with: [
'blockTypeHandle:assetFieldHandle',
'blockTypeHandle:entriesFieldHandle',
['blockTypeHandle:anotherAssetFieldHandle', {
withTransforms: ['thumb']
}]
]
}) %}
{% for block in blocks %}
...
{% endfor %}
Note that our syntax for identifying the items to eager-load within the Matrix field changes slightly from when we were identifying which items to load within the Matrix field from the Entry level above. When eager-loading items from the Matrix field directly, we only have to identify the block type handle and field handle.
Bringing it all together
In the following example we eager-load data for an Asset Field, Entry Field, Matrix Field, and an Asset Field within our Matrix Field which uses an Image Transform.
{% set entries = craft.entries({
section: 'news',
with: [
'highlightImage',
'featuredThings',
['featuredThings.image:featureImage', {
kind: 'image',
withTransforms: [
'thumb'
]
}],
'relatedEntries',
]
}) %}
{% for entry in entries %}
{{ entry.title }}
{{ entry.body }}
{# Get the eager-loaded asset, if there is one #}
{% set image = entry.highlightImage[0] ?? null %}
{% if image %}
<img src="{{ image.url }}" alt="{{ image.title }}">
{% endif %}
{# Loop through our eager-loaded Matrix blocks #}
{% for block in entry.featuredThings %}
{% if block.type == 'image' %}
{# Get the eager-loaded matrix asset, if there is one #}
{% set thumb = entry.featureImage[0] ?? null %}
{% if thumb %}
{# Get the eager-loaded image transform #}
<img src="{{ image.url('thumb') }}" alt="{{ image.title }}">
{% endif %}
{% endif %}
{% endfor %}
{# Get the eager loaded related entries #}
{% for relatedEntry in entry.relatedEntries %}
{{ relatedEntry.title }}
{% endfor %}
{% endfor %}
Above, you may note that we have to use a different syntax for our assets when we eager load them vs. when we lazy load them in the default way. This is further explained in the docs and is due to eager-loaded elements being returned as an array instead of as an ElementCriteriaModel. Unlike an ElementCriteriaModel, which has several helper methods such as first()
, last()
, and find()
, an array is just an array of data.
We can't access eager-loaded elements in the same way that we access lazy-loaded elements, but we can access lazy-loaded elements in the same way that we access eager-loaded elements.
Lazy-Loaded Elements
{# OK #}
{% set image = entry.assetsField[0] ?? null %}
{# OK #}
{% set image = entry.assetsField.first() %}
Eager-Loaded Elements
{# OK #}
{% set image = entry.assetsField[0] ?? null %}
{# THROWS ERROR #}
{% set image = entry.assetsField.first() %}
The same is true for eager-loaded Matrix blocks, but we don't see a change in the code above because we are using a loop, not accessing a single item.
Lazy-Loaded Elements
{# OK #}
{% for block in entry.matrixFieldHandle %}
...
{% endfor %}
{# OK #}
{% for block in entry.matrixFieldHandle.find() %}
...
{% endfor %}
Eager-Loaded Elements
{# OK #}
{% for block in entry.matrixFieldHandle %}
...
{% endfor %}
{# THROWS ERROR #}
{% for block in entry.matrixFieldHandle.find() %}
...
{% endfor %}
Keep this in mind as you use optimize your templates and share data between templates using includes and macros. It's possible, that if you have eager-loaded data from one page, and lazy-loaded data from another page, that a shared include or macro could throw an error if you are not using a syntax that works for both scenarios.
Eager-Loading Elements in plugins using PHP
Everything we have done here in Twig, we can also do in our plugins using PHP. One method of doing so, is to use the with
parameter in the same way we saw in our Twig templates - providing the with
parameter with an array of information about the elements we want to eager load.
$criteria = craft()->elements->getCriteria(ElementType::Entry);
$criteria->handle = 'news';
$criteria->with = array('assetFieldHandle');
$elements = $criteria->find();
While the above appears to be using the with
parameter alongside all the other details of the query, behind the scenes, Craft doesn’t use that information until after the initial query is performed. Eager-loading elements takes place afterwards, with an additional query to the database.
The above is the same as querying for our elements, and then calling a second method (craft()->elements->eagerLoadElements()
) to retrieve the elements you wish to eager load and add them to the source Element Model :
$criteria = craft()->elements->getCriteria(ElementType::Entry);
$criteria->handle = 'news';
$elements = $criteria->find();
craft()->elements->eagerLoadElements($elements, array(
'assetFieldHandle'
));