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):
.element
callssize()
with a width of25%
and a height of5em
.25%
is not found in the stored widths so it gets stored and a%width-1
placeholder is generated at root level.5em
is not found in the stored heights so it gets stored and a%height-1
placeholder is generated at root level.- 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):
.other-element
callssize()
with a width of25%
and a height of5em
.25%
is found at index1
in our stored widths so%width-1
gets extended, appending.other-element
to the existing.element { width: 25% }
selector.5em
is found at index1
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.
Frequently Asked Questions (FAQs) about Caching Values in Sass Mixins
What is the purpose of caching values in Sass Mixins?
Caching values in Sass Mixins is a technique used to optimize the performance of your Sass code. By caching values, you can avoid unnecessary computations and make your code run faster. This is particularly useful when you have complex mixins that are used multiple times in your code. Instead of computing the same values over and over again, you can compute them once, cache the results, and reuse them whenever needed.
How do I cache values in Sass Mixins?
Caching values in Sass Mixins involves storing the results of computations in variables. These variables can then be used in place of the original computations, saving time and resources. Here’s a simple example:@mixin cache-example($value) {
$cached-value: $value * 2;
width: $cached-value;
height: $cached-value;
}
In this example, the computation $value * 2
is stored in the variable $cached-value
, which is then used to set the width
and height
properties.
Can I cache values from function calls in Sass Mixins?
Yes, you can cache values from function calls in Sass Mixins. This is done in the same way as caching values from computations. You simply store the result of the function call in a variable and use that variable in place of the function call. This can be particularly useful when you have functions that perform complex or time-consuming operations.
How does caching values in Sass Mixins improve performance?
Caching values in Sass Mixins improves performance by reducing the number of computations that need to be performed. When you cache a value, you compute it once and store the result in a variable. This variable can then be used in place of the original computation, saving time and resources. This is particularly beneficial when you have complex mixins that are used multiple times in your code.
Can I cache values in Sass Mixins that depend on arguments?
Yes, you can cache values in Sass Mixins that depend on arguments. The key is to ensure that the cached value is updated whenever the arguments change. This can be done by including the arguments in the computation that is being cached. Here’s an example:@mixin cache-example($value1, $value2) {
$cached-value: $value1 * $value2;
width: $cached-value;
height: $cached-value;
}
In this example, the computation $value1 * $value2
is stored in the variable $cached-value
, which is then used to set the width
and height
properties. If either $value1
or $value2
changes, the cached value will be updated accordingly.
Are there any limitations to caching values in Sass Mixins?
While caching values in Sass Mixins can greatly improve performance, it’s important to be aware of potential limitations. One limitation is that caching can increase the complexity of your code, making it harder to understand and maintain. Another limitation is that caching is not always possible or beneficial. For example, if a computation is very simple or is only used once, caching may not provide any performance benefits and could even slow down your code.
How can I determine if caching values in Sass Mixins is beneficial for my code?
Determining whether caching values in Sass Mixins is beneficial for your code can be a bit tricky. In general, caching is most beneficial when you have complex computations that are used multiple times. If a computation is very simple or is only used once, caching may not provide any performance benefits. To determine if caching is beneficial, you can try implementing it and then testing the performance of your code with and without caching.
Can I use caching in Sass Mixins with other Sass features?
Yes, you can use caching in Sass Mixins with other Sass features. For example, you can use caching with Sass functions, control directives, and other mixins. The key is to ensure that the cached values are updated whenever the relevant inputs change.
How can I debug issues with caching in Sass Mixins?
Debugging issues with caching in Sass Mixins can be challenging, as the issues may not be immediately obvious. One approach is to use Sass’s built-in debugging features, such as the @debug
directive, which can print values to the console. Another approach is to carefully review your code to ensure that cached values are being updated correctly.
Can I use caching in Sass Mixins in all browsers?
Yes, you can use caching in Sass Mixins in all browsers. This is because Sass is a preprocessor, which means it compiles your Sass code into CSS before it is sent to the browser. As a result, all of the caching happens during the compilation process, and the resulting CSS code can be used in any browser.
Non-binary trans accessibility & diversity advocate, frontend developer, author. Real life cat. She/her.