Template performance: How to write performant Liquid code

This page is dedicated to template performance, and how Liquid code and template/workflow architecture impacts template performance.

Architecture and dependencies

The most important factor that has by far the largest impact on template performance is dependencies. It is therefore key to think about an efficient dependency structure between templates.

In short, a dependency is any external data source that connects with a template. How dependencies are created and which side-effects they entail is documented here.

On the first time render of a template (e.g. when viewing the template in input mode or when workflow progress bar is loading), all underlying dependencies will be rendered as well e.g. to collect results from underlying templates or accounts. For subsequent renders, Silverfin limits the re-rendering of underlying templates to a maximum of three levels deep in the dependency chain (which then might result in cache differences for results as documented here).

So to maintain strong performance across templates and workflows, it is crucial to minimise dependencies across templates and periods, and only include those that are absolutely necessary.

Some tips manage the dependencies:

  • Map the dependency tree. Maintain a visual representation of the dependency structure of the workflow. This makes it easier to identify and reduce complex or unnecessary connections.
    In this regard, the debug panel is a valuable tool for visualising a template’s dependencies and dependency tree. However, do note that this list of dependencies and the dependency tree are company specific and conditional. In order to keep track of the whole dependencies flow we recommend keeping a blueprint in a separate tool.
  • Prefer database access over results. Access database data (like custom drops) instead of results where possible. Accessing the payload of a text property directly in the database does not require a template render and is therefore considered cheaper compared to fetching results which will require a template (re-)render.
  • Use shared parts to limit dependencies. Shared parts can help to reuse code without creating additional dependencies. We are currently preparing detailed guidelines on code architecture and design patterns, including best practices for shared parts - coming soon!
  • Be specific when accessing accounts. Narrow down the range of accounts as precise as possible and avoid accessing period.accounts multiple times in the template (see examples below as well).
  • Leverage copy data and rollforward functionality. Rely on copy data and the Silverfin rollforward functionality when using data from other periods instead of directly accessing data from that period.
  • Make use of the flame graph. The flame graph gives template authors necessary insights in the loading time of a certain template, which can provide some additional pointers as to where the render time is spent.

Code improvements

Below section lists some practical examples on how to write Liquid code as performance neutral as possible:

  • Avoid accessing the same drops multiple times in your code, for example prefer this:
{% assign period_accounts = period.accounts.include_zeros %}

{% assign accounts_1 = period_accounts | range:"1" %}  
{% assign accounts_2 = period_accounts | range:"2" %}

Over this:

{% assign accounts_1 = period.accounts.include_zeros | range:"1" %}  
{% assign accounts_2 = period.accounts.include_zeros | range:"2" %}
  • Related to this, try to centralise logic so you only need to access the accounts drop once. For example, prefer this:
{% for account in period.accounts %}  
  {% if account.value != 0 %}  
    ...  
  {% endif %}  
  ...  
  {% if account.results.name == "true" %}  
    ...  
  {% endif %}  
{% endfor %}

Over this:

{% for account in period.accounts %}  
  {% if account.value != 0 %}  
    ...  
  {% endif %}  
{% endfor %}

...

{% for account in period.accounts %}  
  {% if account.results.name == "true" %}  
    ...  
  {% endif %}  
{% endfor %}
  • Avoid unnecessary iterations in loops, e.g. make use of the break tag:
{% assign one_account_has_value_show_category = false %}  
{% for account in period.accounts %}  
  {% if account.value != 0 %}{% assign one_account_has_value_show_category = true %}{% break %}{% endif %}  
{% endfor %}
  • Rely on reconciliations and accounts drop (over reconciliation and account drop) if possible as accessing the reconciliations or accounts drop does not create a dependency.

For example when checking if another reconciliation is starred, prefer this:

{% for recon in period.reconciliations.starred %}  
  {% if recon.handle == "hello" %}  
    {% assign handle_hello_starred = true %}  
  {% endif %}  
{% endfor %}

Over this:

{% if period.reconciliations.hello.starred? == true %}
  {% assign handle_hello_starred = true %}  
{% endif %}

Same for when looking for asset accounts, filter on the accounts drop rather than on the account drop. Prefer this:

{% assign asset_accounts = period.accounts.assets %}  
{% for asset_acocunt in asset_accounts %}  
  {% capture account_id %}{{ account.id }}_asset{% endcapture %}  
  {% assign [account_id] = true %}  
{% endfor %}

Over this:

{% assign accounts = period.accounts %}  
{% for account in accounts %}  
  {% if account.asset? %}  
    {% capture account_id %}{{ account.id }}_asset{% endcapture %}  
    {% assign [account_id] = true %}  
  {% endif %}  
{% endfor %}
  • Make arrays and ranges as precise as possible, e.g.:
{% assign starred_accounts = period.accounts.starred | range"613" %}  
{% assign starred_reconciliations = period.reconciliations.starred | range:"hello" %}