How to use a simple SVG filter on Safari with acceptable performance and without crashing?
Asked Answered
S

2

11

Disclaimer: This post is half question and half report of my experiments while trying to find a solution.

The Task: Simple SVG Filter on a Monochrome Rectangle

Using a filter to change or modify the color of a monochrome shape in SVG is pretty straightforward. Here is how it can be done:

<svg viewBox="0 0 460 130">
  <defs>
    <filter id="filter1">
      <feColorMatrix values="0.5 0 0 0 0 0 0.5 0 0 0 0 0 0.5 0 0 0 0 0 1 0" />
    </filter>
  </defs>
  <g transform="translate(20, 0)">
    <text x="0" y="35">Original</text>
    <rect x="0" y="50" width="200" height="80" fill="red" />
  </g>
  <g transform="translate(240, 0)">
    <text x="0" y="35">Filtered</text>  
    <rect x="0" y="50" width="200" height="80" fill="red" filter="url(#filter1)" />
  </g>
</svg>

 

The Problem: It is Slow on Safari and Crashes on Safari Mobile

The little problem with this is that it is really slow on Safari. In the snippet below a new filtered rect is added to the SVG every half a second. This will eventually crash on the Safari mobile after 10-80 cycles, depending on your device. Also, it is unbearably slow. For comparison, on an entry level Android phone running Chrome this runs almost forever with 50 fps at the beginning.

var rectCount = 1;
var svgRectElem = document.getElementById('svg-rect');
var rectCountElem = document.getElementById('rect-count');
var fpsElem = document.getElementById('fps');

(function insertRect() {
  window.requestAnimationFrame(function() {
    var start = new Date().getTime();
  
    var clonedSvgRectElem = svgRectElem.cloneNode();

    // insert cloned rect SVG element
    svgRectElem.parentNode.appendChild(clonedSvgRectElem);

    rectCountElem.textContent = ++rectCount;

    window.requestAnimationFrame(function() {
      var fps = Math.round(100000 / (new Date().getTime() - start)) / 100;
      fpsElem.textContent = fps;
      window.setTimeout(insertRect, 500);
    }, 100);
  });
})();
<p>SVG Rect Count: <span id="rect-count">1</span></p>
<p>fps: <span id="fps"></span></p>

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512" viewBox="0,0,1024,1024">
  <defs>
    <filter id="filter-1">
      <feColorMatrix values="0.5 0 0 0 0 0 0.5 0 0 0 0 0 0.5 0 0 0 0 0 1 0" />
    </filter>
  </defs>
  <rect id="svg-rect" fill="red" filter="url(#filter-1)" x="0" y="0" width="1024" height="1024" />
</svg>

    Snippet on CodePen

 

Possible Solutions: What I Came up with so far

I tried several approaches, but as you will see. As you will see, some of them work for Safari, each one has a catch.

 

1. Wait for a fix by the WebKit team

tl;dr Maybe works, maybe not

That would be the most convenient option, but if you consider that SVG filters were introduced 15 years ago, I would not hold my breath for this issue to be fixed anytime soon. And yes, I have written a bug report.

 

2. Use filterRes

tl;dr Works, but only until the next Safari release

In SVG 1.1 the attribute filterRes is defined. This specifies the resolution of the filter. A value of 1 means that the source for the filter will be treated as a single pixel. So instead of running over all pixels, the filter has to be applied only to one pixel. Since we are using a single color rectangle as the input image, treating the whole image as one pixel is ok in our case.

In our example the filter will look like this:

<filter id="filter1" filterRes="1">
  <feColorMatrix values="0.5 0 0 0 0 0 0.5 0 0 0 0 0 0.5 0 0 0 0 0 1 0" />
</filter>

Snippet on CodePen

This actually improves performance a lot and no crashes occur!

The sad news is that filterRes has been removed from the SVG standard in version 2.0. WebKit was very fast to adapt this part of SVG 2.0 and decided to remove it in the next version. If we rely on filterRes, the same problem will appear again soon.

 

3. Filter a small rect, then scale up

tl;dr Does not work

The approach above shows that applying the filter on a small regions can improve performance a lot. There we apply the filter on a small rect, and then scale the result up to the desired size. For scaling we use the transform attribute. If we set the rect to 1/64 of the desired size we will then scale it with transform="scale(64)" to achieve a final size of 1024.

Snippet on CodePen

Unfortunately this does not work at all. Performance is unchanged and it still crashes on the Safari mobile. Interestingly this cannot be attributed to the filter or transform alone. Only in combination this gets slow and crashes.

 

4 Filter a small rect, then use feTile to fill the Target rect

tl;dr Works a bit, but artefacts on Chrome

This apporach is similar to the previous. We apply the filter to a small rectangle first, and then use a second filter with feTile to fill the target rectangle.

Snippet on CodePen

This works on Safari mobile. Not as good as filterRes, but there is quite an improvement.

But for this approach Chrome is the party pooper. If you scale zoom in and out on Chrome, in some zoom levels a raster is displayed. Here is how it can look (correct would be one large square):

enter image description here

 

5 Use huge values for the filter's width and height

tl;dr Works, but a hack and causes issues on Internet Explorer

This is a very counter intuitive approach. Setting the width and height of the filter region to very large values should make the filter much slower, because zillions of pixels will have to be processed. But actually the opposite is the case. The filter speeds up dramatically on WebKit and no crashing occurs.

Here is how to implement this in our example:

<filter id="filter-1" x="0" y="0" width="102400" height="102400" filterUnits="userSpaceOnUse">
  <feColorMatrix values="0.5 0 0 0 0 0 0.5 0 0 0 0 0 0.5 0 0 0 0 0 1 0" />
</filter>

Snippet on CodePen

I think what is happening here is that filter first scales down the original filter region, then processes the part that will be visible in the SVG's viewBox (which is a tiny region after downscaling), and then scales that region up again. And this appears to be fast.

Unfortunately this is quite a hack and we do not now how future browser versions will handle SVGs with such large values. Also, this causes issues on Internet Explorer, where the document's scrolling area will become HUGE (probably because the filter's bounds are calculated into the size), making scrolling almost impossible. And if you select the values too small, Internet Explorer will actually display the rect in the size of the filter's bounds.

 

What is the solution?

Actually, I do not know. There is an easy solution using filterRes, but this will expire soon (it is already not supported in WebKit technology preview), so this is not an option. All other approaches cause issues on other browsers.

Can you think of any other approach that I could try? I cannot believe that cross browser use of SVG filters fifteen years after the SVG 1.1 spec was released is such an adventure.

Surgeonfish answered 4/11, 2018 at 21:54 Comment(7)
A variant of option 1 would be download the source to webkit and fix the bug yourself or pay someone else to do that.Khotan
Yes, Apple would sure be happy about that option. But who would make sure that the change makes it into Safari and when will that be?Surgeonfish
You'd have to research Safari's release schedule etc. I'm only really familar with that of Firefox.Khotan
@RobertLongson BTW, Firefox did not even blink on all the bizarre tests that I conducted during the last days. Thanks for all your work.Surgeonfish
Random ideas: You might try adding the required type="matrix" attribute and see if that makes things better. Add a x="0%", y="0%" width="100%" and height="100%" in case it's screwing up the filter dimension calculation. Also make sure that you have no leading spaces in your values string and that it doesn't contain any line-breaks. Final idea - move the filter to an outermost g wrapper in case the intermediate transforms are causing problems.Rienzi
Why do you need a filter to modify the colour, just set another colour for the shape directly.Khotan
@RobertLongson Because in the real scenario the color can be set freely via CSS and I want the other colors to adapt automatically. It works really well, this Safari bug is the last hurdle.Surgeonfish
T
0

Try this:

Put all the rectangles into one group and apply the filter to that group instead.

var rectCount = 1;
var svgRectElem = document.getElementById('svg-rect');
var rectCountElem = document.getElementById('rect-count');
var fpsElem = document.getElementById('fps');

(function insertRect() {
  window.requestAnimationFrame(function() {
    var start = new Date().getTime();
  
    var clonedSvgRectElem = svgRectElem.cloneNode();

    // insert cloned rect SVG element
    svgRectElem.parentNode.appendChild(clonedSvgRectElem);

    rectCountElem.textContent = ++rectCount;

    window.requestAnimationFrame(function() {
      var fps = Math.round(100000 / (new Date().getTime() - start)) / 100;
      fpsElem.textContent = fps;
      window.setTimeout(insertRect, 500);
    }, 100);
  });
})();
<p>SVG Rect Count: <span id="rect-count">1</span></p>
<p>fps: <span id="fps"></span></p>

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512" viewBox="0,0,1024,1024">
  <defs>
    <filter id="filter-1">
      <feColorMatrix values="0.5 0 0 0 0 0 0.5 0 0 0 0 0 0.5 0 0 0 0 0 1 0" />
    </filter>
  </defs>
  <g filter="url(#filter-1)">
    <rect id="svg-rect" fill="red" x="0" y="0" width="1024" height="1024" />
  </g>
</svg>
Translucent answered 4/11, 2018 at 22:0 Comment(1)
Thanks, this works in the examples, but in the actual scenario the filters are all different. I decided to use the same filter in the examples for simplification. Also, the problems occur if you display multiple SVGs, each containing only one filtered rect.Surgeonfish
R
0

You should try adding color-interpolation-filters="sRGB" to the filter element -> the default color space for SVG filters is linearRGB, but the low level graphics libraries that support this seem to be very neglected in the browsers - so adding this as boilerplate to switch over to the better maintained sRGB codebase seems to fix a lot of performance issues with many filters.

Rienzi answered 11/6, 2023 at 9:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.