Menu Search
Jump to the content X X
Smashing Conf Barcelona

You know, we use ad-blockers as well. We gotta keep those servers running though. Did you know that we publish useful books and run friendly conferences — crafted for pros like yourself? E.g. our upcoming SmashingConf Barcelona, dedicated to smart front-end techniques and design patterns.

Web Image Effects Performance Showdown

As browsers constantly improve their graphical rendering abilities, the ability to truly design within them is becoming more of a reality. A few lines of code can now have quick and dramatic visual impact, and allow for consistency without a lot of effort. And as with most things in web development, there are often many ways to achieve the same effect.

In this post, we’ll take a look at one of the most popular image effects, grayscale, and assess both the ease of implementation and performance implications of HTML canvas, SVG, CSS filters, and CSS blend modes. Which one will win?

Further Reading on SmashingMag: Link

Filters on the web work like a lens laid over an image. They are applied to the image after the browser renders5 layout and initial paint. In supporting browsers, filters can be applied6 individually or layered on top of one another. Because they can be applied as image modifications after the initial render, and are likely an enhancement, filters gracefully degrade by merely not being visible in browsers which do not support them.

CSS Filters Link

Let’s get started with the most straightforward method for producing a grayscale effect: the humble, yet powerful CSS filter.

unfiltered bird image7
Unfiltered image. (View large version8)

To achieve this effect, we add a single line of CSS: filter: grayscale(1). This filter desaturates the image and can be used with any numeric or percentage-based value between 0 and 1 (or 0% to 100%). Note: currently, filters for WebKit-based browsers must be prefixed with -webkit-. However, a solution such as Autoprefixer9 would eliminate the need to add them by hand.

Live Demo – CSS Filter Link

.cssfilter-gray {
  -webkit-filter: grayscale(1);
  background: url('img/bird.jpg');
  filter: grayscale(1);

Background Blend Mode Link

CSS blend modes provide an endless variety of options for image effect combinations. There are two ways to use blend modes: the mix-blend-mode property and the background-blend-mode property.

  • mix-blend-mode is the property which describes how the element will blend with the content behind it.
  • background-blend-mode is used for elements with multiple backgrounds, and describes the relationship between these backgrounds.

We’ll use the background-blend-mode: luminosity to pull luminosity channels over a gray background in our example, resulting in a grayscale image. One thing to note is that background order is constant: background images must always be ordered before solid colors or gradient backgrounds for effects to render properly. Color must be the last background layer10. This is also a safeguard against older browsers which do not support background-blend-mode – the image will still render without the effect.

The only difference with blend modes and the CSS filter is that we now have multiple backgrounds, and are using background-blend-mode: luminosity, which grabs the luminosity values from the top image (the bird tester) and layers those brightness values over a gray second background.

Live Demo – Background Blend Mode Link

.cssfilter-gray {
  background: url('img/bird.jpg'), gray;
  background-blend-mode: luminosity;

At the moment, this is the newest and thus least supported11 option, though it still works well in Chrome and Firefox, and has partial Safari support. Note: Safari supports all blend modes except for the HSL-based blend modes: hue, saturation, color, and luminosity.

HTML5 Canvas Link

HTML5 <canvas> allows for a ton of flexibility when it comes to image manipulation, because we have access to each individual pixel’s data (specifically through canvasContext.getImageData) and can manipulate each one through JavaScript. This method, however, is the most complex and comes with the most overhead. It also has a few nuances in cross-origin issues due to security concerns.

To fix the cross-origin error in Chrome, your image will need to be hosted on a cross-origin resource sharing (CORS) friendly site like GitHub Pages or Dropbox, and specify crossOrigin="Anonymous". See the live example for a demonstration12.

The way to achieve a grayscale effect with <canvas> is to strip the red, green and blue components from any outlying value in the pixel value while maintaining its luminosity level (brightness). One way to do this13 is to average the RGB values like so: grayscale = (red + green + blue) / 3;.

In the example below, we are using the RGBa values in the format (R,G,B,a) in the image data; the red channel is data[0], the green channel is data[1], and so on. We then get the luminosity level of each of these channels (the brightness) and average them to turn the image grayscale.

Live Demo – HTML5 Canvas Link

SVG Filter Link

SVG filters have the widest support14 (even in Internet Explorer and Edge!) and are also (almost) just as easy to use as CSS filters directly. You can use them with the same filter property. In fact, CSS filters stemmed out of SVG filters15. As with canvas, SVG filters allow you to transcend the flat plane of two-dimensional effects, as you can leverage WebGL shading to create even more complex results.

There are a few ways to apply an SVG filter, but in this case we will still use the filter property on the background image, just like the CSS filter example for consistency. The biggest difference with SVG filters is that we need to be careful to include and link to this filter with the proper path. This means we need to import the SVG on the page above the element in which we are using it (which can be made easier by using templating engine and include statements).

Live Demo – SVG Filter Link

  <filter id="grayscale-filter">
    <feColorMatrix type="saturate" values="0"/>
.svgfilter-gray {
  background: url('img/bird.jpg');
  -webkit-filter: url(#grayscale-filter);
  filter: url(#grayscale-filter);

Filter Performance Link

So how do these stack up when it comes to initial render performance? I made a test page for each and used the WebPagetest16 comparison feature in Chrome 47. Keeping in mind that each test gave slightly different results, the overall trend can be summed up as follows:

The CSS filter, SVG filter and CSS blend mode methods all loaded in relatively similar time frames. Sometimes the SVG filter was faster than the CSS blend mode (but always barely) and vice versa. The CSS filter was generally among the fastest to load, and <canvas> was always the slowest. This is the most significant insight gleaned. <canvas> was regularly lagging behind the other methods in rendering the image.

For fairness sake, I wanted to also compare load time for multiple images. I created ten renditions of each (instead of just one) and ran the tests again:

The results were similar (keep in mind there were slight variations in each test). The CSS filter was 0.1ms slower in this case, showing that between CSS filters, blend modes and SVG filters, the results are inconclusive for the speediest method. However, HTML5 <canvas> lagged noticeably in comparison.

Taking a look deeper into page load time via JavaScript render and paint render time, you can see this trend continuing.

(View large version18)
Filter Type Time to Render Time to Paint
CSS Filter 12.94ms 4.28ms
CSS Blend Mode 12.10ms 4.45ms
SVG Filter 14.77ms 5.80ms
Canvas Filter 15.23ms 10.73ms

Again, <canvas> took the longest time to render and longest time to paint, while the two CSS options were the speediest, SVG coming in the middle.

These results make sense, because <canvas> is taking each individual pixel and performing an operation on it before we are ever able to see any image at all. This takes a lot of processing power at render time. While normally SVGs are used for vector graphics, I would still highly recommend them over <canvas> when dealing with raster image effects. Not only is SVG faster, but it is also much easier to deal with and more flexible within the DOM19. Generally, CSS filters are even more optimized than SVG filters, as historically they are shortcuts emerging out of SVG filters and, thus, optimized in browsers.

#nofilter Link

What about using no filter? I compared our overall speediest method (adding a CSS filter) to editing your image in photo editing software before uploading it (I used Preview on Mac OS X to remove saturation). When preediting the image, I found a consistent 0.1ms performance improvement in my tests:


Conclusion Link

Image filters are a fun and effective way to provide visual unity and aesthetic appeal on the web. Keep in mind that they do come with a slight performance hit, but also with the benefits of speedy design in the browser and the opportunity to design interactions with.

For simple image effects use CSS filters, as they have the widest support and simplest usage. For more complex image effects, check out SVG filters or CSS blend modes. SVG filter effects are particularly nice because of their channel manipulation capabilities and feColorMatrix21. CSS blend modes also offer some really nice visual effects with overlapping elements on the page. You can use similar blend modes within SVG (such as feBlend), though they are akin to CSS background-blend-mode in the sense that the interaction pertains to the SVG itself and not with surrounding elements, like mix-blend-mode allows. Just don't use <canvas> for filters.

(rb, og, il)

Footnotes Link

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21

↑ Back to top Tweet itShare on Facebook

Una is a UI Engineer at DigitalOcean. She writes technical articles around the web and on her website, is a Sass community organizer, and cohosts the Toolsday podcast. Una frequently contributes to the open source community, being a core member of the Open Design Foundation and creator of CSSgram.

  1. 1

    As a big fan of yours, it’s great to see you in Smashing Magazines with a great article!
    And it really surprised me to see CSS filters perform better than SVG ones. I guess it is because it has to be referenced from elsewhere?

  2. 2

    What a great blog, interesting that CSS performs better than SVG. The visual effect of SVG is definitely the most striking. I’d say the aesthetic of the filter is worth the minor performance hit if it contributes substantially to the unity of the page’s overall aesthetic. In the least they’re fun to play around with!

  3. 3

    Interesting comparison. I do though think that WebPageTest isn’t the right tool in this case to compare the various techniques if you want to zoom in on rendering performance for the specific filters. WebPageTest mainly looks at (and is affected by) what happens on the network and is in a cached situation (repeated view) affected as well by disk I/O for retrieving the cache. I think it would be better to purely look at timeline data and run extensive comparisons based on that info.

  4. 4

    Why are the prefix-variants of the CSS rules after the standard definitions? This is a really bad example!

  5. 6

    Gunnar Bittersmann

    May 21, 2016 7:52 pm

    “One way to do this is to average the RGB values like so: grayscale = (red + green + blue) / 3;.”

    That’s not the best way. Because the human eye is not the same sensible for red, green and blue wavelengths, you should not use the average, but the weighted average. Something like
    grayscale = (0.299 * red + 0.587 * green + 0.114 * blue) / 3;

    • 7

      Gunnar Bittersmann

      May 21, 2016 7:53 pm

      Correction: no ‘/ 3’, just grayscale = (0.299 * red + 0.587 * green + 0.114 * blue);

      Argl, I cannot edit my comment?

  6. 8

    James Edward Lewis II

    May 22, 2016 7:40 pm

    The arithmetic average of the RGB components is woefully inaccurate (in the canvas example); a somewhat more accurate method is getting the Y value of the YIQ representation:

    var Y = (299 * R + 587 * G + 114 * B) / 1000;

    An even more accurate method first converts the RGB values, which are usually gamma-corrected, into linear RGB values:

    The linear versions of R, G, and B, are respectively r, g, and b, with a range between 0 and 1:

    function gammaDeCorrect(n) {
    'use strict';
    if (typeof n !== 'number' || isNaN(n)) throw new TypeError('argument must be a number');
    if (n 255) throw new RangeError('argument must be between 0 and 255.');
    if (n > 10.31475) return Math.pow((n/255+0.055)/1.055, 2.4);
    else return n/3294.6;
    var r = gammaDeCorrect(R), g = gammaDeCorrect(G), b = gammaDeCorrect(B);

    Then find the linear y component of the xyz space:

    var y = 0.2126 * r + 0.7152 * g + 0.0722 * b;

    Finally, gamma-correct y to Y and scale to the range 0-255:

    var Y = (y > 0.0031308) ? 255 * (1.055 * Math.pow(y, 1 / 2.4) - 0.055) : 3294.6 * y;

    and in either this case or YIQ, round to the nearest integer.

    The key point is that people’s eyes are much more sensitive to the green component than to the red, and much more sensitive to the red component than to the blue, and the simplistic averaging algorithm fails to account for that, making green and yellow things look too dark, and deep blue and purple things look too bright.

    • 9

      James Edward Lewis II

      May 22, 2016 10:31 pm

      Oh, I thought this comment had been removed by the moderators, because the code wasn’t formatted very well, and the angle brackets inside the code tags were swallowed as if they made a tag by themselves; the comment below it links to code that actually works and shows what I was talking about.

  7. 10

    James Edward Lewis II

    May 22, 2016 10:29 pm

    This JSFiddle shows why averaging the RGB components (from the canvas example) doesn’t work well:

    Notice that yellow and green colors are made too dark, and purple and deep blue colors too light, by averaging, compared to taking the Y component from the YIQ or XYZ transformations.

    The best choice is probably the YIQ choice, which is not much more complicated than a simple average, and which (to my eyes at least) does seem to fit better than XYZ.

    The YIQ formula I used came straight from the W3C:

    var Y = (299 * R + 587 * G + 114 * B) / 1000;

  8. 11

    Tim Ferguson

    May 24, 2016 9:16 am

    FYI the live svg filter demo didn’t work on my phone. Android browser via the Feedly app.

  9. 12

    Tim Ferguson

    May 24, 2016 9:18 am

    Didn’t work in mobile version of Chrome either.

  10. 13

    The live SVG filter demo doesn’t work in Firefox, either.
    Made a few screenshots and combined them for better comparison:

    Full version:

    System: Firefox 46.0.1
    OS: Linux Mint 17.3 / Ubuntu 14.04.4 LTS / x86_64

    cu, w0lf.

  11. 14


    May 27, 2016 12:24 pm

    Wow! very nice and thanks for this code. This is such a great website. I learned many things from here and I have bookmarked it on my browser. Thanks admin for this great website and this post. Thanks again and again. –visual design

  12. 15

    Agustin Amenabar L.

    June 12, 2016 10:10 am

    The problem with the test is that all these filters are fairly trivial calculations. I bet they would differ vastly if you did the experiment with Blur, or even better Gaussian Blur, which is usually a rendering bottle neck even in box-shadows.


↑ Back to top