🤯 50% Off! 700+ courses, assessments, and books

Caching Values from Sass Mixins

Kitty Giraudel
Share

Let’s back up a little bit so I can explain the context of this post. In recent weeks I have been actively comparing rendering output between mixins and placeholders. As a result, the more I think about it, the more I believe using mixins and placeholders as they are described in the Sass docs is simply not enough.

Let’s focus on the main goal here: CSS output. In this whole experiment, I am not considering code complexity or code quality. I’m only considering final CSS output. The point is to have CSS output that is as light and lean as possible.

If you’ve read my previous SitePoint articles covering placeholders vs mixins and @extend directives, you would be aware that there are a couple of drawbacks with both features. Mixins do not merge selectors while @extend directives are restricted to their scope, hence they are basically unusable in a responsive context with @media blocks (which I tackled and partially solved in my article on cross-scope @extend).

Today, I would like to deal with another issue: Mixin grouping.

What is Mixin Grouping?

Mixins are really handy when you want to create shortcuts in your variable code. For instance, you could have a mixin accepting two arguments to output both width and height. Or a mixin for offset positioning. That’s nice.

The problem with mixins is every time you call them, they dump CSS rule sets. If you call them twice with the same arguments from 2 different selectors, they won’t be merged into a single rule set. Instead, they will dump the same rule set twice — which kind of sucks.

For this article, we’ll deal with a very simple use case: A size mixin to assign width and height. Here is what it looks like:

// Helper mixin to define dimensions
// ---
// @param [number] $width: width
// @param [number] $height ($width): height
// ---
@mixin size($width, $height: $width) {
  width: $width;
  height: $height;
}

Using it is pretty easy.

.element {
  @include size(25%, 5em);
}

This will result in the following CSS:

.element {
  width: 25%;
  height: 5em;
}

Perfect, isn’t it? Now let’s say we have another piece of code in a different place in our project that should also be styled with those same dimensions.

.other-element {
  @include size(25%, 5em);
}

No problem so far. But what’s the final output?

.element {
  width: 25%;
  height: 5em;
}

/* [...] */

.other-element {
  width: 25%;
  height: 5em;
}

Now you can see the problem. Since those two rule sets are exactly the same, they should be merged. Having the same rule sets like that is pretty lame, isn’t it?

Over-Engineering Things

Based on this problem, I thought of a solution to keep the mixin usage as is but use the power of placeholders in the background. Basically it would be a mixin extending a placeholder generated on the fly. Sounds like a risky bet but you’ll see it’s actually pretty hot.

First of all, we need a list to store our values. Actually, we need more than a list — we need a map. This is strictly internal; the developer does not have to know about it, and neither should he need to do anything with it manually.

$cache: (
  'width'  : (),
  'height' : ()
);

Now, let’s get back to our mixin, which has no signature change.

@mixin size($width, $height: $width) {
  // Width
  $stored-widths: map-get($cache, 'width');
  @if not index($stored-widths, $width) {
    $cache-size: map-merge($cache, ('width': append($stored-widths, $width)));
    @at-root %width-#{length($stored-widths) + 1} {
      width: $width;
    }
  }

  // Height
  $stored-heights: map-get($cache-size, 'height');
  @if not index($stored-heights, $height) {
    $cache-size: map-merge($cache, ('height': append($stored-heights, $height)));
    @at-root %height-#{length($stored-heights) + 1} {
      height: $height;
    }
  }

  // Actually applying values
  @extend %width-#{index(map-get($cache, 'width'), $width)};
  @extend %height-#{index(map-get($cache, 'height'), $height)};
}

I have to concede that this code is pretty harsh.

Here is what’s going on: first, we get the stored widths in the $stored-widths variable (line 3). If the $width value is found in the list, we do nothing. If it hasn’t been found (line 4), not only do we append $width to the list (line 5), but we also generate a placeholder at root level (line 6), named after the freshly added index.

Then we do the exact same thing with $height (lines 12 to 15). Once we’re done, we have brand new generated placeholders to extend, so we extend them (lines 21 and 22). End of story.

If we get back to our original example, here is what would happen the first time we call the mixin (to produce the first rule set):

  1. .element calls size() with a width of 25% and a height of 5em.
  2. 25% is not found in the stored widths so it gets stored and a %width-1 placeholder is generated at root level.
  3. 5em is not found in the stored heights so it gets stored and a %height-1 placeholder is generated at root level.
  4. Both placeholders get extended, resulting in the following CSS:
.element { 
  width: 25% 
}

.element { 
  height: 5em 
}

Now when size() gets called for the second time with the same arguments (which is supposed to produce the 2nd rule set, but we’ll see why it doesn’t):

  1. .other-element calls size() with a width of 25% and a height of 5em.
  2. 25% is found at index 1 in our stored widths so %width-1 gets extended, appending .other-element to the existing .element { width: 25% } selector.
  3. 5em is found at index 1 in our stored heights so %height-1 gets extended, appending .other-element to the existing .element { height: 5em } selector.
.element, .other-element { 
  width: 25% 
}

.element, .other-element { 
  height: 5em 
}

True, there are only two calls here, which isn’t a big deal. But imagine a large project with dozens and dozens of calls to the same mixin, with common values like 100% or 1px. No more extra lines dumped and all selectors are regrouped whenever possible.

Automating Things with a Cache Mixin

While what we did is cool, it involves code repetition and can quickly become annoying to set up for all mixins, especially those with quite a lot of CSS output. What if we made a small cache() mixin that deals with this for us?

The idea is to pass a collection of properties/values to our mixin, so it can cache those values in a global cache map. Thus, our mixin needs only one parameter to work: A map of CSS declarations.

// Global cache map
$cache: ();

// Cache mixin
@mixin cache($declarations) {
  /* Sass magic */
}

Now, all we need to do is loop through all declarations in the map. For each declaration:

  • We check if the value exists in the list of stored values for the given property.
  • If it doesn’t, we add it to the list and create a placeholder at root level.
  • We extend the already existing or freshly created placeholder.
@mixin cache($declarations) {
  // Looping through all properties/values from map
  @each $property, $value in $declarations {
    // Get the stored values for the current property
    $stored-values: map-get($cache, $property);

    // If the value doesn't exist in stored values
    @if not index($stored-values, $value) {
      // Add it
      $cache: map-merge($cache, ($property: append($stored-values or (), $value))) !global;
      // And create a placeholder at root level
      @at-root %#{$property}-#{length(map-get($cache, $property))} {
        #{$property}: $value;
      }
    }

    // Extend the placeholder
    @extend %#{$property}-#{index(map-get($cache, $property), $value)};
  }
}

There you go. A very clean and neat cache mixin. Now, if we go back to our initial example:

@mixin size($width, $height: $width) {
  @include cache((
    width: $width,
    height: $height
  ));
}

Pretty simple, isn’t it? In the end, it looks very clean (and speaks for itself when read out loud), it caches the content so it gets the best out of Sass placeholders, and we avoid content repetition. Who could ask for more?

Is This Useful?

Hard to tell. On a medium to large scale project, I can see the benefit of doing something like this with all mixins. We used a simple example but you could apply this technique to all mixins; you just need to add keys to the cache map (e.g. padding, margin, color…).

Is there any performance benefit? Probably. I have not done any in-depth tests but I don’t see why there wouldn’t be. In the end, we are reducing the amount of generated CSS from mixins by grouping selectors when it’s possible. It certainly can’t be bad.

That being said, Gzip does some astonishing work on repeated strings, so at the end of the day I am not sure you’ll actually be able to see a major difference.

Feel free to have a look at the fully commented code on SassMeister:

Play with this gist on SassMeister.