I have two solutions for you:
filter:blur(0.2px) hack (don't ask me how it works)
.item img
{
transition: filter 250ms ease-in-out;
filter: blur(0.2px); /* the lowest value I could get on my machine: 0.12805650383234025436px */
image-rendering: -webkit-optimize-contrast; /* just in case quality degrades */
}
.item:hover img
{
filter: blur(2px);
}
Jokes aside, this is probably caused by floating number optimizations done by the Processing Unit, so by setting blur to .2px I am not animating blur(0px), but instead start from another value and instead of calculating it like this (assume we have linear easing):
frame1: 0, frame2: .1, frame3: .2, frame4: .3, ...
it calculates it like that:
frame1: .2, frame2: .2666, frame3: .3332, ...
So the incremental value has changed and no longer causes that misposition. Of course there's no proper math here (this is especially tricky with easings), but you get the idea.
This also skips the first frame with jiggle as well.
Duplicate blurred image and toggle between them (also the most performant way)
<div class="item">
<img class="blurred" src="https://www.wikihow.com/images/thumb/2/25/Collect-Bodily-Fluid-Samples-from-a-Cat-Step-16.jpg/aid8673811-v4-728px-Collect-Bodily-Fluid-Samples-from-a-Cat-Step-16.jpg" alt="">
<img class="original" src="https://www.wikihow.com/images/thumb/2/25/Collect-Bodily-Fluid-Samples-from-a-Cat-Step-16.jpg/aid8673811-v4-728px-Collect-Bodily-Fluid-Samples-from-a-Cat-Step-16.jpg" alt="">
</div>
.item
{
position: relative;
}
.item img
{
max-width: 300px;
transition: opacity 250ms ease-in-out;
will-change: opacity;
}
.item .original
{
transition-delay: 0;
}
.item .blurred
{
position: absolute;
filter: blur(5px);
opacity: 0;
transition-delay: .1s;
}
.item:hover .original
{
opacity: 0;
transition-delay: .2s;
}
.item:hover .blurred
{
opacity: 1;
transition-delay: .1s;
}