Mike Fallows

Optimising CSS minification in Liquid

• 5 min

One of the mailing lists I subscribe to is I Only Speak Liquid, where they regularly publish emails from their stable of developers with some great insights into working on Shopify themes and more generally working as a freelancer/consultant in the Shopify ecosystem. If you’re looking for good content on those subjects, I definitely recommend subscribing.

One tip mentioned in an email by Billy Noyes was a way to use Liquid to minify CSS. The technique was provided by a community member in this forum post. It’s a great way to remove unnecessary characters (like whitespace and comments) from a CSS file without the need for an additional build tool, so it’s a low effort way to increase a site’s performance by reducing its page weight.

Original #

Here is the initial implementation (rewritten slightly to make comparison easier, and added some comments to explain what’s happening):

{% capture bloated %}
/* CSS ... */
{% endcapture %}

<!-- CSS {{ bloated.size }} chars -->
{%- liquid
  assign original = ''
  # remove line breaks,
  # multiple whitespace characters and
  # split on closing comment tags
  assign chunks = bloated | strip_newlines | split: ' ' | join: ' ' | split: '*/'
  # iterate over each chunk of CSS
  for chunk in chunks
    # remove comments and whitespace around syntax characters
    assign mini = chunk | split: '/*' | first | strip | replace: '; ', ';' | replace: '} ', '}' | replace: '{ ', '{' | replace: ' {', '{'
    assign original = original | append: mini
  endfor
  # calculate characters saved
  assign change = original.size | times: 1.0 | divided_by: bloated.size | times: 100.0 | round: 1
%}
<!-- original    -{{ bloated.size | minus: original.size }}   {{ 100 | minus: change }}%  -->

This alone was providing me with a 10% reduction across most of the CSS I tried it on. However, having looked at the code, I thought I could see an opportunity to improve on it very slightly, and wanted to see if there were any meaningful gains to be made. I noticed that the final semicolon in a ruleset was being preserved, but it can safely be removed. If you have only 100 rulesets that could still save up to 100 characters. That extra bit of code was replace: ';}', '}'.

Test #

I wanted to find out how much of a difference this made on a real codebase, and also test that nothing would break as a result, so I set up the following test. I used a 5,000+ lines CSS file from one of my projects to test the results.

{% capture bloated %}
// 5000+ lines of CSS
{% endcapture %}

<!-- CSS {{ bloated.size }} chars -->
{%- liquid
  assign original = ''
  assign chunks = bloated | strip_newlines | split: ' ' | join: ' ' | split: '*/'
  for chunk in chunks
    assign mini = chunk | split: '/*' | first | strip | replace: '; ', ';' | replace: '} ', '}' | replace: '{ ', '{' | replace: ' {', '{'
    assign original = original | append: mini
  endfor
  assign change = original.size | times: 1.0 | divided_by: bloated.size | times: 100.0 | round: 1
%}
<!-- original    -{{ bloated.size | minus: original.size }}   {{ 100 | minus: change }}%  -->

{%- liquid
  assign optimised = ''
  assign chunks = bloated | strip_newlines | split: ' ' | join: ' ' | split: '*/'
  for chunk in chunks
    assign mini = chunk | split: '/*' | first | strip | replace: ': ', ':' | replace: '; ', ';' | replace: '} ', '}' | replace: '{ ', '{' | replace: ' {', '{' | replace: ';}', '}'
    assign optimised = optimised | append: mini
  endfor
  assign change = optimised.size | times: 1.0 | divided_by: bloated.size | times: 100.0 | round: 1
%}
<!-- optimised   -{{ bloated.size | minus: optimised.size }}   {{ 100 | minus: change }}%  -->

Results #

Here are the results:

<!-- CSS 121066 chars -->
<!-- original    -12829   10.6%  -->
<!-- optimised   -16091   13.3%  -->

Nice! An extra 300+ characters removed equating to over 2.5% more of a reduction. The output CSS worked perfectly still. For very little effort or intervention, a 10%+ reduction is a great result, and will benefit almost any project.

Extracting to a snippet #

Finally to wrap it up I created a snippet called minify-css.liquid so that I can easily add this feature to any large CSS files as well as to snippets and sections that may also contain large chunks of CSS that could benefit from minification.

{%- liquid
  assign chunks = input | strip_newlines | split: ' ' | join: ' ' | split: '*/'
  for chunk in chunks
    assign mini = chunk | split: '/*' | first | strip | replace: ': ', ':' | replace: '; ', ';' | replace: '} ', '}' | replace: '{ ', '{' | replace: ' {', '{' | replace: ';}', '}'
    echo mini
  endfor
%}

So it can even be used like this:

<style>
{%- capture bloated %}
// Unminified CSS
{%- endcapture %}

{%- render 'minify-css', input: bloated -%}
</style>

Now I can maintain beautiful, well documented, human-readable CSS code across an entire theme and still have it optimised for performance without any additional build tools.

Tagged • shopify • css • testing