Blackfire on Blackfire: Twig loops and escaping
How we optimized our Twig templates usages thanks to Blackfire.
The first principle we obey at Blackfire is to use our own product on a daily basis. As developers we are de-facto beta testers and reviewers of our own work.
Recently, I’ve been working on optimizing Twig and I’d like to share my findings about loops and escaping.
As you might know, Blackfire is built with PHP and uses Twig as its template engine. In the early days of Blackfire, we had some issues about memory consumption and intense CPU usage when displaying a profile graph page.
The graph view needs a lot of data, some are dynamically created by the browser based on a JSON file while some others are rendered server-side by Twig.
Here’s what we were doing:
{% extends "layout.html.twig" %}
{% block body_container %}
<table>
{% for key, call in graph.nodes %}
{{ block('functioncall') }}
{% endfor %}
</table>
{% endblock %}
{% block functioncall %}
{% spaceless %}
<tr>
<td title="{{ call.nodeId }}">
<a href="#call={{ call.nodeId }}">{{ call.nodeId }}</a>
</td>
{% for dimension, data in graph.dimensions %}
<td class="focus-{{ dimension }}">
<div class="hidden val-focus-{{ dimension }} val-self">
{{ call.exclusive_percentage[dimension]|abs }}
</div>
<div class="progress" data-dimension="{{ dimension }}" data-call-name="{{ call.nodeId }}">
<div class="progress-bar progress-bar-danger"
style="width: {{ call.exclusive_percentage[dimension]|abs }}%;">
</div>
<div class="progress-bar progress-bar-warning"
style="width: {{ call.inclusive_percentage[dimension]|abs }}%;">
</div>
</div>
</td>
<td class="hidden focus-{{ dimension }}">
<div class="val-focus-{{ dimension }} val-incl">
{{ call.inclusive_percentage[dimension]|abs }}
</div>
</td>
{% endfor %}
</tr>
{% endspaceless %}
{% endblock functioncall %}
Notice anything special? Personally, I’ve already seen such code and I’m okay with it. However, passing this template to Twig results in the following graph:
Observations
Some observations about this graph first:
- The whole page takes 260ms to render;
- We spend nearly 80ms in the
functioncallTwig block, more than 30ms exclusively, for 275 calls; twig_escape_filterconsumes more than 30ms and is called nearly 15,000 times;twig_template_get_attributesis called 15700 times using up to 7ms (and we are using the Twig C PHP extension);- The spaceless twig tag implies a
preg_replacecall that cost 5ms.
Improving the performance
In our case, we are absolutely confident about the data we manipulate. All of them have been prepared server side and none of them can contain user data. This eliminates XSS injection vulnerabilities that twig_escape_filter is used for.
twig_template_get_attributes is called many times, but if you read carefully, the same attribute is fetched multiple times from the call variable.
First, let’s avoid that on the data we’re sure by using the Twig autoescape tag. Then, let’s cache twig_template_get_attributes calls by setting a variable within the loop. Finally, let’s remove the spaceless tag from the loop and let’s use it at higher level.
{% block body_container %}
+ {% spaceless %}
<table>
{% for key, call in graph.nodes %}
{{ block('functioncall') }}
{% endfor %}
</table>
+ {% endspaceless %}
{% endblock %}
{% block functioncall %}
- {% spaceless %}
+ {% set nodeId = call.nodeId %}
<tr>
+ {% autoescape false %}
- <td title="{{ call.nodeId }}">
- <a href="#call={{ call.nodeId }}">{{ call.nodeId }}</a>
+ <td title="{{ nodeId }}">
+ <a href="#call={{ nodeId }}">{{ nodeId }}</a>
</td>
{% for dimension, data in graph.dimensions %}
+ {% set inclusive_dim = call.inclusive_percentage[dimension]|abs %}
+ {% set exclusive_dim = call.exclusive_percentage[dimension]|abs %}
<td class="focus-{{ dimension }}">
<div class="hidden val-focus-{{ dimension }} val-self">
- {{ call.exclusive_percentage[dimension]|abs }}
+ {{ exclusive_dim }}
</div>
- <div class="progress" data-dimension="{{ dimension }}" data-call-name="{{ call.nodeId }}">
+ <div class="progress" data-dimension="{{ dimension }}" data-call-name="{{ nodeId }}">
<div class="progress-bar progress-bar-danger"
- style="width: {{ call.exclusive_percentage[dimension]|abs }}%;">
+ style="width: {{ exclusive_dim }}%;">
</div>
<div class="progress-bar progress-bar-warning"
- style="width: {{ call.inclusive_percentage[dimension]|abs }}%;">
+ style="width: {{ inclusive_dim }}%;">
</div>
</div>
</td>
- {{ call.inclusive_percentage[dimension]|abs }}
+ {{ inclusive_dim }}
</div>
</td>
{% endfor %}
+ {% endautoescape %}
</tr>
- {% endspaceless %}
{% endblock functioncall %}
Comparison
We saved 53ms for the page rendering:
twig_escape_filterhas now disappeared and that saves 33ms;twig_template_get_attributesis now less than 5ms;preg_replaceis now called once instead of 255 times and is not noticeable anymore;- The
functioncallblock is now faster but use more than 1MB of memory by storing more variables; - The page now takes 206ms to be rendered.
That’s quite an improvement!
Being an expert in a language or framework does not mean you control everything that happen at runtime. Blackfire is definitely my new glasses that help me spot what’s important to tweak before delivering it in production.
Enjoy these glasses, it’s free 🙂

