Audio fadeout using exponentialRampToValueAtTime in Chrome or Firefox is not reliable

The following code respects the MDN documentation but results in an abrupt mute instead of a 2-second-long fadeout:

const audioContext = new window.AudioContext();
let oscillator;
let gainNode;
document.getElementById("playSweep").addEventListener("click", () => {
    oscillator = audioContext.createOscillator();
    oscillator.type = "sine"; // Sine wave
    oscillator.frequency = 200;
    gainNode = audioContext.createGain();
    gainNode.gain.setValueAtTime(1, audioContext.currentTime);
    oscillator.connect(gainNode);
    gainNode.connect(audioContext.destination);
    oscillator.start();
});
document.getElementById("fadeOut").addEventListener("click", () => {
    gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 2);
});
document.getElementById("fadeOut2").addEventListener("click", () => {
    gainNode.gain.linearRampToValueAtTime(0.001, audioContext.currentTime + 2);
});
<button id="playSweep">Play Frequency Sweep</button>
<button id="fadeOut">Exp Fade Out</button>
<button id="fadeOut2">Lin Fade Out</button>

Even with the linear version, we can hear a click, it’s not a clean fadeout.

How to do a proper fadeout in JS Web Audio API?