As already implied by Michael Mullany
SVG filters are notorious for negatively affecting rendering performance.
Workarounds to mitigate jittery animations/transitions
- apply the filter only to certain elements - instead of the whole parent svg.
- specify or limit the css properties, that should be transitioned like so:
transition: transform 800ms;
svg {
width: 100px;
height: 100px;
padding: 1px;
transition: transform 800ms;
overflow: visible;
}
svg:hover {
transform: rotate(90deg);
}
.filterOutline {
filter: url("#outline");
}
.wrapper {
position: absolute;
padding: 10px;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: black;
}
<div class="wrapper">
<svg width="100%" height="100%">
<defs>
<mask id="mask">
<image
x="0"
y="0"
width="100%"
height="100%"
xlink:href="https://i.ibb.co/pL9J807/test.png"
/>
</mask>
<filter id="outline" color-interpolation-filters="sRGB">
<feDropShadow
dx="0"
dy="0"
stdDeviation="1.25"
in="SourceGraphic"
flood-color="white"
result="blur"
flood-opacity="10"
></feDropShadow>
<feColorMatrix
mode="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 55 -5"
in="blur"
></feColorMatrix>
</filter>
</defs>
<g class="filterOutline">
<rect
width="100%"
height="100%"
mask="url('#mask')"
fill="red"
/>
</g>
</svg>
</div>
Maybe vectorize mask images provided by users?
Applying a <mask>
is usually more expensive than applying a <clip-path>
since masks also support semi-transparent areas introducing a more complex alpha transparency rendering context.
Provided all user uploads are "high-contrast" or perfect black and white images (... or will be converted to these) – you might even try to vectorize these files via something like potrace (available for pretty much any language – here's just a javaScript example).
Trace/vectorize by potrace
Ideally you could convert the uploaded images server-side.
Provided, all images are similarly simple and clean as the example png, this might be a valid alternative.
The advantage of this approach – you'll get a svg already including the desired transparency. So there's no need to clip or mask anything.
Besides you could apply regular fill and stroke properties – no need to emulate fills or strokes with a filter.

// draw png to canvas
let canvas = document.createElement("canvas");
canvasWrp.appendChild(canvas);
let ctx = canvas.getContext("2d");
let scale = 2;
//let imgUrl = 'https://i.ibb.co/pL9J807/test.png';
let imgUrl ="data:image/png;base64,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";
var img = new Image();
img.src = imgUrl;
img.crossOrigin = "anonymous";
img.onload = function() {
let width = img.naturalWidth * scale;
let height = img.naturalHeight * scale;
canvas.width = width;
canvas.height = height;
// invert
ctx.filter = 'invert(1)';
ctx.drawImage(img, 0, 0, width, height);
let dataUrl = canvas.toDataURL();
/**
* trace/vectorize
* adjust parameters to get desired curve smoothing
* See: https://github.com/kilobtye/potrace/blob/master/potrace.js#LC7
*/
Potrace.setParameter({
turnpolicy: 'minority',
turdsize: 2,
optcurve: true,
alphamax: 1,
opttolerance: 0.75
});
// load
Potrace.loadImageFromUrl(dataUrl);
let opt_type = 'curve';
Potrace.process(() => {
let svg = new DOMParser().parseFromString(Potrace.getSVG(1), 'image/svg+xml').querySelector('svg');
let path = svg.querySelector('path');
path.removeAttribute('fill');
path.removeAttribute('fill-rule');
path.removeAttribute('stroke');
let [w, h] = [svg.width.baseVal.value, svg.height.baseVal.value];
svg.setAttribute('viewBox', [0, 0, w, h].join(' '))
svg.removeAttribute('id');
svg.removeAttribute('version');
svg.removeAttribute('width');
svg.removeAttribute('height');
svgdiv.appendChild(svg)
// clone
let svgClone = svg.cloneNode(true)
svgdivAni.appendChild(svgClone)
});
};
body {
margin: 30px;
background-color: #fff;
background-image: url("data:image/svg+xml, %3Csvg viewBox='0 0 10 9' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath d='M0 0 L10 0 L10 9' stroke-width='1' fill='none' stroke='%23eee' /%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 20px;
}
article {
display: flex;
gap: 2em;
}
article>* {
flex: 1;
width: 100%;
height: 100%;
}
.imgWrp {
width: 50%;
}
canvas,
svg {
width: 100%
}
.svgdiv path:hover {
fill: #000;
stroke-width: 0.5%;
stroke: transparent;
marker-start: url(#markerStart);
marker-mid: url(#markerRound);
}
svg {
overflow: visible;
}
.svgdivAni path {
fill: red;
stroke: #000;
stroke-width: 5;
paint-order: fill;
transition: 0.3s;
transform-origin: center;
}
.svgdivAni:hover path {
transform: rotate(180deg);
}
<article>
<div class="imgWrp" id="canvasWrp"> </div>
<div class="imgWrp svgdiv" id="svgdiv"> </div>
<div class="imgWrp svgdivAni" id="svgdivAni"> </div>
</article>
<!-- just to illustrate the number of commands according to curretn smoothing parameters -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
<defs>
<marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth"
markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="5" fill="green"></circle>
</marker>
<marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth"
markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="2.5" fill="red"></circle>
</marker>
</defs>
</svg>
<script src="https://cdn.jsdelivr.net/gh/kilobtye/potrace@master/potrace.js"></script>