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>
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>
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.
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.
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):
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>
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.