Adventures in CSS and SVG filters and browser performance
I was working on my last blog post and wanted to go all out with some cool new UI elements. One of things I made were these stickers:
I was using the stickers service on my iPhone to generate “stickers” of the food I ate, the problem was my photos kinda sucked and didn’t isolate the image and edges were kinda blurry.
I’ll update that article eventually with good pics, but one idea that I thought of while trying to figure out how to programmatically smooth edges was to use CSS filters.
I realized after some playing around, we could add a nice looking sticker border with the code below:
filter:
drop-shadow( 5px 0 0 white)
drop-shadow(-5px 0 0 white)
drop-shadow( 0 5px 0 white)
drop-shadow( 0 -5px 0 white)
drop-shadow( 1px 0 0 #999)
drop-shadow(-1px 0 0 #999)
drop-shadow( 0 1px 0 #999)
drop-shadow( 0 -1px 0 #999);
I had to do this 8 times because we would need to create a shadow in each direction (4) and we have two kinds of "shadows", the white and the small grey outline.
The awesome thing about filters is that you can easily apply a filter to a PNG and it will only apply to non-transparent boundaries.
The next thing I wanted to do then is add some animations, a few hovers and a few transforms…but wait, everything is now extremely slow, am I running something in the background? No Hmm.
It turns out that drop-shadow and blur are performance intensive operations and now I need to do it with 10+ images on my page, it makes the startup time a bit slow.
From our code above, I have 8 operations going on per sticker for drop shadow, in most browsers every time you invoke drop-shadow, it happens linearly, as the resulting image output needs to be reprocessed for the next drop-shadow, meaning it would need to be done 8 times.
On chromium this involves using this 3rd party library called Skia to process the image array every time, performing a stacked transformation.
Let’s now look into svg filters instead. SVG filters are a lot more complicated and are usually much less performant than CSS, but the reason we're going for this feature instead is that it gives us some new tools that can help us create a more optimized sticker.
<svg width="0" height="0" aria-hidden="true" style="position: absolute;">
<filter id="sticker-outline" x="-50%" y="-50%"
width="200%" height="200%" color-interpolation-filters="sRGB">
<feMorphology in="SourceAlpha" operator="dilate" radius="5" result="border" />
<feMorphology in="border" operator="dilate" radius="1" result="outline" />
<feFlood flood-color="#999" result="outlineColor" />
<feComposite in="outlineColor" in2="outline" operator="in" result="coloredOutline" />
<feFlood flood-color="#fff" result="borderColor" />
<feComposite in="borderColor" in2="border" operator="in" result="coloredBorder" />
<feMerge>
<feMergeNode in="coloredOutline" />
<feMergeNode in="coloredBorder" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</svg>
Instead of needing to invoke drop-shadow, we first use feMorphology to expand the image by 5 pixels and give it the name border, then expanding it one more pixel for our outline. We then flood the whole area with our grey border, then composite that fill only to be on our 1px outline. We do it again with white but this time composite that onto our 5px border. Then we finally do the feMerge where we stack our three items on top of each other, the original image SouceGraphic and then our two added elements.
And in our CSS all we would need to do is link it up like we do below. You do need to have this SVG exist somewhere in your HTML for it to be referenced.
.sticker { filter: url(#sticker-outline); }
A lot more verbiage, but these operations are much less intensive, all we're doing is defining fills and defining additional padding around our non-transparent area; This should be less heavy than needing to do a shadow calculation 8 times over our stickers.
And now we can get some buttery smooth animations!!