Optimization + Cleanup
It’s worth taking a moment to talk about some Craft features that can improve your project’s speed, accessibility, and maintainability—and that are directly relevant to what we’ve built so far.
We have a host of other ideas for next steps if you would prefer to jump into self-guided learning!
# Includes
When building the blog and topic index pages, there was a lot of repeated code involved with outputting the post previews. Twig’s include
tag can help us reduce that repetition by extracting it into a separate template, and referencing it in both places.
We’ll start by taking the entire for
loop from templates/blog/index.twig
and moving it to a new file, templates/blog/_feed.twig
:
{% for post in posts %}
{% set image = post.featureImage.one() %}
<article>
{% if image %}
<div class="thumbnail">
{{ image.getImg() }}
</div>
{% endif %}
<h2>{{ post.title }}</h2>
<time datetime="{{ post.postDate | atom }}">{{ post.postDate | date }}</time>
{{ post.summary | md }}
<a href="{{ post.url }}">Continue Reading</a>
</article>
{% endfor %}
In place of that block of code in templates/blog/index.twig
, include
the new template (then make the same change to templates/blog/_topic.twig
):
{% include 'blog/_feed' %}
By default, Twig exposes all variables in the current “context” to included templates, so posts
is available as though the partial was right there, in-line.
Some people prefer to pass variables explicitly so that they know exactly what will and won’t be accessible within a template:
{% include 'blog/_feed' with {
posts: posts
} only %}
The only
at the end of the tag ensures that no other variables from the current template are leaked into the included template. Craft’s global variables (and our siteInfo
global set) will still be available.
# Asset Transforms
Applying CSS to our front-end finally got the images under control… but we’re still asking the client to download significantly larger files than necessary, for the size they’re displayed at. In fact, our blog index weighs a staggering 30MB!
The asset URLs that Craft generates for the <img>
tags via image.getImg()
look like this:
https://tutorial.ddev.site/uploads/images/highway-26.jpg
This points to the unmodified, original file—exactly as it was uploaded. Craft always keeps these around, but we can ask for an optimized version via transforms.
Transforms are defined directly in your templates, or in the control panel. Each transform has a mode, which determines what other options are required. For our blog index, the most important qualities are the final width of the image (about 640px, or double that for high-resolution displays), and that important subjects aren’t cropped out.
Let’s add these constraints to the template:
<div class="thumbnail">
{{ image.getImg({
mode: 'fit',
width: 1200,
}) }}
</div>
Refreshing the blog index, you may notice a delay before the images are displayed again—that’s to be expected, as Craft only generates a transform when it’s requested for the first time.
According to the browser’s Network tab (or looking at the original and transformed files on-disk), this one change reduced our page’s total size to about 310KB—or by 99%!
# Named Transforms
Applying these settings to all pages (and remembering to synchronize any tweaks between them) can be cumbersome. Fortunately, we can centralize these definitions in the control panel, and refer to them by only a handle:
- Navigate to
- Settings
- Assets
- Image Transforms
- Click + New image transform;
- Provide these settings, leaving others at their defaults:
- Name: Thumbnail;
- Handle:
thumbnail
; - Mode: Fit;
- Width:
1200
(units are in pixels, so use only numbers in this field); - Allow Upscaling: Turn off;
- Click Save;
Now we can reference this transform throughout our templates by its handle, rather than its specifics:
<div class="thumbnail">
{{ image.getImg('thumbnail') }}
</div>
# Focal Points
On the About page, we’ve cropped the Profile Photo image as a circle, which almost perfectly cut out the subject (in the lower third of the image). Craft doesn’t know about the content of our image—or what kind of frame it will be displayed in—but we can give it some hints.
The first step will be to define a transform. This is a one-off treatment, so we won’t bother going through the control panel:
<div class="photo">
{{ profileImage.getImg({
mode: 'crop',
width: 360,
height: 360,
}) }}
</div>
This brings another marked improvement to the size of our pages—from 8MB for a single image to just over 40KB… but our subject is still getting cropped out!
Open the About page in the control panel, then double-click the attached asset in the
Profile Image field. In the slideout, open the metadata panel with the control in the upper-right corner, then click Edit image in the preview window:In the image editor, click
Focal Point and drag the control that appears to the desired position:Click Save to confirm the new focal point, and return to the About page in the front-end. Refreshing the page will regenerate the transform, this time respecting the newly-defined focal point!
# Eager-Loading
Our blog’s indexes show a list of posts, each of which includes an image. We perform one query for the list of posts, then each time we output a post, we perform another query to get the asset attached via its Feature Image field.
This is what’s called an “N+1” problem: the number of queries required to load the content is proportional to the amount of content—and that could be a lot. If we had 50 posts, it would take 51 queries to display the blog feed; if we also wanted to display topics for each post in the feed, it would be 101 queries!
Craft addresses this with a feature called eager loading, which allows us to declare in the main query (or the secondary queries, as we’ll see in a moment) what nested elements we are apt to need, for each result:
{% set posts = craft.entries()
.section('blog')
.with([
'featureImage',
])
.all() %}
Without any changes to our templates, Craft is able to fetch the posts, then gather all the assets used on those posts and match them up—and it will do this in just two queries, regardless of how many posts are in a feed!
Only relational fields (Assets, Entries, Categories, and Tags) need to be eager-loaded—and not every scenario demands it! The best rule of thumb is to look out for nested for
loops, or any time you use a relational field inside a for
loop.
# Lazy Eager-Loading
If you aren’t sure what data you need, ahead of time, you can defer eager-loading until those related or nested elements are actually needed. Instead of defining the eager-load requirements in both the blog index and category templates, we can update the shared blog/_feed.twig
template:
{% for post in posts %}
{% set image = post.featureImage.eagerly().one() %}
{# ... #}
{% endfor %}
# Transforms
When we defined image transforms for posts’ featured images on the blog index, we inadvertently triggered an additional query for each post, on top of the asset query. Fortunately, eager loading is supported for transforms (named and unnamed), as well:
{% set posts = craft.entries()
.section('blog')
.with([
['featureImage', { withTransforms: ['thumbnail'] }],
])
.all() %}
While eager-loading can provide some performance benefits here, maintaining transform options between queries and usage can become difficult. This is a great reason to consolidate transforms in the control panel!
The equivalent solution for lazily eager-loaded elements would look like this (again in templates/blog/_feed.twig
):
{% for post in posts %}
{% set image = post.featureImage
.withTransforms([
{ mode: 'fit', width: 1200 },
])
.eagerly()
.one() %}
{# ... #}
{% endfor %}