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>
tutorial.ddev.site/blog
Screenshot of blog index with downsampled images
The optimized images are indistinguishable from the originals at this scale.

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:

  1. Navigate to
    1. Settings
    2. Assets
    3. Image Transforms
    ;
  2. Click + New image transform;
  3. 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;
  4. 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:

tutorial-two.ddev.site/admin/admin/entries/about/46-about
Screenshot of the image editor in the Craft control panel
1
2
Changing an asset’s focal point.

In the image editor, click Focal Point and drag the control that appears to the desired position:

tutorial-two.ddev.site/admin/admin/entries/about/46-about
Screenshot of the image editor in the Craft control panel
1
2
Changing an asset’s focal point.

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!

tutorial.ddev.site/about
Screenshot of avatar tastefully cropping subject
Our profile picture is now resampled and cropped in such a way that the subject is still in view.

# 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 %}