The Unexpected @extended
Recently I ran into a nasty SCSS surprise. A single line of SCSS code that I inserted into my stylesheet bloated the output by almost 3x, taking it from 126kb to 379kb. Needless to say I was a bit shocked. A quick investigation showed my naive use of @extend
was at fault.
I've created this isolated example to demonstrate the issue. Let's say I need to style the following html blocks. This is very similar to a grid + column structure where we can have up to 12 children which need to evenly lay across the horizontal space.
<!-- simple case -->
<div class="item">...</div>
<!-- nested items in a 3-up column structure -->
<div class="item item--block item--block--3">
<div class="item">...</div>
<div class="item">...</div>
<div class="item">...</div>
</div>
<!-- nested items in a 12-up column structure -->
<div class="item item--block item--block--12">
<div class="item">...</div>
<div class="item">...</div>
<!-- ... more items here ... -->
<div class="item">...</div>
</div>
The Challenge
I don't want to specify the class as item item--block item--block--3
every time I create a 3-up layout. I'd like to simply use item--block--3
and have it include the styles of item
and item--block
.
So I wrote the following, naively extending .item
(view in sassmeister)
@mixin item-generator ($n, $i: 1) {
@while $i <= $n {
.item--block--#{$i} {
@extend .item;
.item {
width: (100% / $i);
}
}
$i: $i + 1;
}
}
.item {
padding: 12px;
& + .item {
padding-left: 0;
}
}
.item--block {
width: 100%;
display: inline-block;
}
@include item-generator(12);
Expectations
I expected the generated css to look something like this:
.item,
.item--block--1,
.item--block--2,
.item--block--3,
.item--block--4,
.item--block--5,
.item--block--6,
.item--block--7,
.item--block--8,
.item--block--9,
.item--block--10,
.item--block--11,
.item--block--12 {
padding: 12px;
}
.item + .item {
padding-left: 0;
}
.item--block {
width: 100%;
display: inline-block;
}
.item--block--1 .item {
width: 100%;
}
.item--block--2 .item {
width: 50%;
}
<!-- 9 blocks removed for brevity -->
.item--block--12 .item {
width: 8.33333%;
}
Actual Output
But what I got was this — all matching selectors merged together into this huge undesirable mess:
.item,
.item--block--1,
.item--block--2,
.item--block--3,
.item--block--4,
.item--block--5,
.item--block--6,
.item--block--7,
.item--block--8,
.item--block--9,
.item--block--10,
.item--block--11,
.item--block--12 {
padding: 12px;
}
.item + .item,
.item--block--1 + .item,
.item--block--2 + .item,
.item--block--3 + .item,
.item--block--4 + .item,
.item--block--5 + .item,
.item--block--6 + .item,
.item--block--7 + .item,
.item--block--8 + .item,
.item--block--9 + .item,
.item--block--10 + .item,
.item--block--11 + .item,
.item--block--12 + .item,
.item + .item--block--1,
.item--block--1 + .item--block--1,
.item--block--2 + .item--block--1,
.item--block--3 + .item--block--1,
.item--block--4 + .item--block--1,
.item--block--5 + .item--block--1,
.item--block--6 + .item--block--1,
.item--block--7 + .item--block--1,
.item--block--8 + .item--block--1,
.item--block--9 + .item--block--1,
.item--block--10 + .item--block--1,
.item--block--11 + .item--block--1,
.item--block--12 + .item--block--1,
.item + .item--block--2,
.item--block--1 + .item--block--2,
.item--block--2 + .item--block--2,
.item--block--3 + .item--block--2,
.item--block--4 + .item--block--2,
.item--block--5 + .item--block--2,
.item--block--6 + .item--block--2,
.item--block--7 + .item--block--2,
.item--block--8 + .item--block--2,
.item--block--9 + .item--block--2,
.item--block--10 + .item--block--2,
.item--block--11 + .item--block--2,
.item--block--12 + .item--block--2,
.item + .item--block--3,
.item--block--1 + .item--block--3,
.item--block--2 + .item--block--3,
.item--block--3 + .item--block--3,
.item--block--4 + .item--block--3,
.item--block--5 + .item--block--3,
.item--block--6 + .item--block--3,
.item--block--7 + .item--block--3,
.item--block--8 + .item--block--3,
.item--block--9 + .item--block--3,
.item--block--10 + .item--block--3,
.item--block--11 + .item--block--3,
.item--block--12 + .item--block--3,
.item + .item--block--4,
.item--block--1 + .item--block--4,
.item--block--2 + .item--block--4,
.item--block--3 + .item--block--4,
.item--block--4 + .item--block--4,
.item--block--5 + .item--block--4,
.item--block--6 + .item--block--4,
.item--block--7 + .item--block--4,
.item--block--8 + .item--block--4,
.item--block--9 + .item--block--4,
.item--block--10 + .item--block--4,
.item--block--11 + .item--block--4,
.item--block--12 + .item--block--4,
.item + .item--block--5,
.item--block--1 + .item--block--5,
.item--block--2 + .item--block--5,
.item--block--3 + .item--block--5,
.item--block--4 + .item--block--5,
.item--block--5 + .item--block--5,
.item--block--6 + .item--block--5,
.item--block--7 + .item--block--5,
.item--block--8 + .item--block--5,
.item--block--9 + .item--block--5,
.item--block--10 + .item--block--5,
.item--block--11 + .item--block--5,
.item--block--12 + .item--block--5,
.item + .item--block--6,
.item--block--1 + .item--block--6,
.item--block--2 + .item--block--6,
.item--block--3 + .item--block--6,
.item--block--4 + .item--block--6,
.item--block--5 + .item--block--6,
.item--block--6 + .item--block--6,
.item--block--7 + .item--block--6,
.item--block--8 + .item--block--6,
.item--block--9 + .item--block--6,
.item--block--10 + .item--block--6,
.item--block--11 + .item--block--6,
.item--block--12 + .item--block--6,
.item + .item--block--7,
.item--block--1 + .item--block--7,
.item--block--2 + .item--block--7,
.item--block--3 + .item--block--7,
.item--block--4 + .item--block--7,
.item--block--5 + .item--block--7,
.item--block--6 + .item--block--7,
.item--block--7 + .item--block--7,
.item--block--8 + .item--block--7,
.item--block--9 + .item--block--7,
.item--block--10 + .item--block--7,
.item--block--11 + .item--block--7,
.item--block--12 + .item--block--7,
.item + .item--block--8,
.item--block--1 + .item--block--8,
.item--block--2 + .item--block--8,
.item--block--3 + .item--block--8,
.item--block--4 + .item--block--8,
.item--block--5 + .item--block--8,
.item--block--6 + .item--block--8,
.item--block--7 + .item--block--8,
.item--block--8 + .item--block--8,
.item--block--9 + .item--block--8,
.item--block--10 + .item--block--8,
.item--block--11 + .item--block--8,
.item--block--12 + .item--block--8,
.item + .item--block--9,
.item--block--1 + .item--block--9,
.item--block--2 + .item--block--9,
.item--block--3 + .item--block--9,
.item--block--4 + .item--block--9,
.item--block--5 + .item--block--9,
.item--block--6 + .item--block--9,
.item--block--7 + .item--block--9,
.item--block--8 + .item--block--9,
.item--block--9 + .item--block--9,
.item--block--10 + .item--block--9,
.item--block--11 + .item--block--9,
.item--block--12 + .item--block--9,
.item + .item--block--10,
.item--block--1 + .item--block--10,
.item--block--2 + .item--block--10,
.item--block--3 + .item--block--10,
.item--block--4 + .item--block--10,
.item--block--5 + .item--block--10,
.item--block--6 + .item--block--10,
.item--block--7 + .item--block--10,
.item--block--8 + .item--block--10,
.item--block--9 + .item--block--10,
.item--block--10 + .item--block--10,
.item--block--11 + .item--block--10,
.item--block--12 + .item--block--10,
.item + .item--block--11,
.item--block--1 + .item--block--11,
.item--block--2 + .item--block--11,
.item--block--3 + .item--block--11,
.item--block--4 + .item--block--11,
.item--block--5 + .item--block--11,
.item--block--6 + .item--block--11,
.item--block--7 + .item--block--11,
.item--block--8 + .item--block--11,
.item--block--9 + .item--block--11,
.item--block--10 + .item--block--11,
.item--block--11 + .item--block--11,
.item--block--12 + .item--block--11,
.item + .item--block--12,
.item--block--1 + .item--block--12,
.item--block--2 + .item--block--12,
.item--block--3 + .item--block--12,
.item--block--4 + .item--block--12,
.item--block--5 + .item--block--12,
.item--block--6 + .item--block--12,
.item--block--7 + .item--block--12,
.item--block--8 + .item--block--12,
.item--block--9 + .item--block--12,
.item--block--10 + .item--block--12,
.item--block--11 + .item--block--12,
.item--block--12 + .item--block--12 {
padding-left: 0;
}
.item--block {
width: 100%;
display: inline-block;
}
.item--block--1 .item,
.item--block--1 .item--block--1,
.item--block--1 .item--block--2,
.item--block--1 .item--block--3,
.item--block--1 .item--block--4,
.item--block--1 .item--block--5,
.item--block--1 .item--block--6,
.item--block--1 .item--block--7,
.item--block--1 .item--block--8,
.item--block--1 .item--block--9,
.item--block--1 .item--block--10,
.item--block--1 .item--block--11,
.item--block--1 .item--block--12 {
width: 100%;
}
.item--block--2 .item,
.item--block--2 .item--block--1,
.item--block--2 .item--block--2,
.item--block--2 .item--block--3,
.item--block--2 .item--block--4,
.item--block--2 .item--block--5,
.item--block--2 .item--block--6,
.item--block--2 .item--block--7,
.item--block--2 .item--block--8,
.item--block--2 .item--block--9,
.item--block--2 .item--block--10,
.item--block--2 .item--block--11,
.item--block--2 .item--block--12 {
width: 50%;
}
<!-- 9 blocks removed for brevity -->
.item--block--12 .item,
.item--block--12 .item--block--1,
.item--block--12 .item--block--2,
.item--block--12 .item--block--3,
.item--block--12 .item--block--4,
.item--block--12 .item--block--5,
.item--block--12 .item--block--6,
.item--block--12 .item--block--7,
.item--block--12 .item--block--8,
.item--block--12 .item--block--9,
.item--block--12 .item--block--10,
.item--block--12 .item--block--11,
.item--block--12 .item--block--12 {
width: 8.33333%;
}
Woah! Not ideal.
What I had not anticipated was having @extend .item;
match against .item
and .item + .item
and .item--block--# .item
, effectively merging all of the selectors together.
Solution
While I did not expect it to match against every occurance of .item
within my stylesheet I now see the behavior is expected. It's called merging selector sequences.
If this merging is not desired then the solution is quite simple — don't use @extend
with real selectors. Use placeholder %
selectors instead.
Here is the fixed code rendering css as expected (view is sassmeister) thanks to scss placeholder selectors:
@mixin item-generator ($n, $i: 1) {
@while $i <= $n {
.item--block--#{$i} {
@extend %item;
.item {
width: (100% / $i);
}
}
$i: $i + 1;
}
}
%item {
padding: 12px;
}
.item {
@extend %item;
& + .item {
padding-left: 0;
}
}
.item--block {
width: 100%;
display: inline-block;
}
@include item-generator(12);
Postscript
I just want to note that the above code is not real scss from a project — I only created it to demonstrate the selector merging vs placeholder selector behavior. Please don't take this as a suggestion of how to write great scss.