How Core dna caches pages, what makes a render fast, how code reaches a site and what stays behind, the go-live checklist, and running a site once it is live. For developers, leads and site operators who already build pages in a theme repository.
L2–L3
Unit 02 · Making pages fast
The three query-per-row shapes
Making pages fastPage 8 of 34Updated
The most common cause of a slow render, in the three shapes it takes in a template: a relation read in a loop, a lookup helper called in a loop, and a component tree that resolves the same thing at every level. Written so a reviewer can recognise each on sight.
Who this is for. Theme developers and the people who review their templates. Afterwards you can spot a query per row in a template before it reaches production, and count what it costs.
Templates on Core dna call methods on records, and a method that reaches another record loads it from the database the first time it is called. That is what makes templates pleasant to write, and it is how a listing of 30 items runs 60 or 90 queries: the query is not written anywhere, it happens inside a getter. Each query is fast. The count is what makes the page slow, and the count grows with the number of rows.
A getter that returns another record, or a collection of them, called once per row:
Template code
{foreach $posts as $post}
<article>
<h3><a href="{$post->getFullPath()}">{$post->getName()|escape}</a></h3>
<p>{foreach $post->getAuthors() as $author}{$author->getDisplayName()|escape} {/foreach}</p>
<p>{foreach $post->getCategories() as $category}{$category->getName()|escape} {/foreach}</p>
</article>
{/foreach}
{% for post in posts %}
<article>
<h3><a href="{{ post.getFullPath() }}">{{ post.getName()|e }}</a></h3>
<p>{% for author in post.getAuthors() %}{{ author.getDisplayName()|e }} {% endfor %}</p>
<p>{% for category in post.getCategories() %}{{ category.getName()|e }} {% endfor %}</p>
</article>
{% endfor %}
The list itself is one query. A post's authors and its categories are many-to-many relations loaded on first use, so each post adds at least two: 30 posts cost at least 61 queries. A single related record, such as a page's parent, costs one query per distinct record the first time it is reached, so a list whose rows share a parent pays once and a list whose rows all differ pays per row.
Reading a layout area belongs to the same shape. A record's layout areas live in its active revision, which is loaded the first time an area is read, so a card that prints $post->getAreaText('excerpt') adds queries per row too. That one is usually worth paying, since the excerpt is the point of the card; the relations are the ones to question.
Recognise it by: a get…() call that returns a record or a list, inside a {foreach}, and more so a {foreach} over such a call inside another one.
2. A lookup helper called inside a loop
A helper that queries every time it is called, used once per row:
{% for card in page.getAreaCollectionElements('cards') %}
{% set category = _query.findOneBy('Common\\Entities\\Category', {'slug': card.getAreaText('category')}) %}
{% if category %}
<a class="card" href="/products/{{ category.getSlug() }}">{{ card.getAreaText('title')|e }}</a>
{% endif %}
{% endfor %}
Every $_query call is a query, every time. So is $_search->search(), which is a request to the search engine, and so are $_globalComponents->get() and $_menu->get(): neither remembers an earlier answer in the same render, and a global component lookup is two queries when the site has no component of that identifier and the platform looks for a shared one. They are written in the loop because that is where the value is needed, and the loop multiplies them.
Recognise it by: any underscore helper ($_query, $_search, $_globalComponents, $_menu) inside a {foreach}, or inside a component template that is used in a collection, which is the same loop seen from the other side.
3. A nested component tree resolving the same thing at every level
A page built from components inside components: a section holds columns, a column holds cards, a card holds buttons. Each component renders its own template and reads its own areas, so the tree costs at least one load per component, and that part is the price of the design (The cost of nesting). It becomes query-per-row when each component also fetches something that belongs to the page as a whole. A component template receives the page's variables, $page included, so this is easy to write:
Template codetemplates/components/card.html
{* used inside every column of every section *}
{$theme = $page->getAreaText('theme')}
{$cta = $_globalComponents->get('primary-cta')}
<div class="card card--{$theme|escape}">
<h3>{$component->getAreaText('heading')|escape}</h3>
{foreach $component->getAreaCollectionElements('buttons') as $button}{$button->getHtml()}{/foreach}
{if $cta}{$cta->getHtml()}{/if}
</div>
{# used inside every column of every section #}
{% set theme = page.getAreaText('theme') %}
{% set cta = _globalComponents.get('primary-cta') %}
<div class="card card--{{ theme|e }}">
<h3>{{ component.getAreaText('heading')|e }}</h3>
{% for button in component.getAreaCollectionElements('buttons') %}{{ button.getHtml() }}{% endfor %}
{% if cta %}{{ cta.getHtml() }}{% endif %}
</div>
The global component lookup runs once for every card on the page, and the page area is resolved in every card although its answer never changes. With four sections of three columns of four cards, that is 48 lookups for one value.
Recognise it by: a component template that reads $page, a global component or any lookup helper. Ask whether the value differs per component. If it does not, it belongs at the top of the tree.
A review checklist
For every {foreach}, list the calls inside it that return records or call a helper, and multiply by the number of rows the loop can have on the real site, not on the test content.
Treat the template of every component used in a collection as the body of a loop.
Look at the largest listing on the site and the deepest component page first: that is where the count is highest.
Before changing anything, time the page (Measure first), so you know the change helped.