Adjust the simulated latency for each address family and watch the race unfold. The algorithm fires IPv6 first, waits a short stagger delay (250 ms by default in RFC 8305), then fires IPv4. Whichever handshake completes first wins the socket.
<!-- Controls for latency and stagger delay -->
<div class="controls">
<div class="control-group">
<label for="ipv6-latency">IPv6 latency (ms)</label>
<select id="ipv6-latency">
<option value="50">50 ms</option>
<option value="100">100 ms</option>
<option value="200" selected>200 ms</option>
<option value="400">400 ms</option>
<option value="800">800 ms</option>
<option value="9999">timeout</option>
</select>
</div>
<div class="control-group">
<label for="ipv4-latency">IPv4 latency (ms)</label>
<select id="ipv4-latency">
<option value="50">50 ms</option>
<option value="100">100 ms</option>
<option value="200">200 ms</option>
<option value="400" selected>400 ms</option>
<option value="800">800 ms</option>
<option value="9999">timeout</option>
</select>
</div>
<div class="control-group">
<label for="stagger">Stagger delay (ms)</label>
<select id="stagger">
<option value="0">0 ms</option>
<option value="100">100 ms</option>
<option value="250" selected>250 ms</option>
<option value="500">500 ms</option>
</select>
</div>
</div>
<button id="btn-race" type="button">Race!</button>
<!-- Animated timeline bars -->
<div class="timeline-wrap">
<div class="timeline-label">IPv6</div>
<div class="bar-track"><div id="bar-v6" class="bar bar-v6"></div></div>
<div class="timeline-label">IPv4</div>
<div class="bar-track"><div id="bar-v4" class="bar bar-v4"></div></div>
</div>
<div id="status" class="status"></div>
<div id="detail" class="detail"></div>
/* Base layout and control styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-wrap: wrap; gap: .7rem; margin-bottom: .8rem; }
.control-group { display: flex; flex-direction: column; gap: .2rem; }
label { font-size: .8rem; font-weight: 600; color: #555; }
select { font-size: .9rem; padding: .3rem .5rem; border: 1px solid #ccc; border-radius: 6px;
background: #fff; cursor: pointer; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1.1rem;
background: #1d3557; color: #fff; border: none; border-radius: 8px; cursor: pointer; }
button:hover { background: #16304e; }
/* Animated bar track styles */
.timeline-wrap { margin: 1rem 0 .5rem; display: grid;
grid-template-columns: 3.5rem 1fr; gap: .35rem .5rem; align-items: center; }
.timeline-label { font-size: .85rem; font-weight: 700; color: #444; text-align: right; }
.bar-track { background: #eaeef2; border-radius: 6px; height: 22px; overflow: hidden; position: relative; }
.bar { height: 100%; width: 0; border-radius: 6px; transition: width .05s linear; }
.bar-v6 { background: #2196f3; }
.bar-v4 { background: #ff9800; }
.bar.winner { outline: 3px solid #0a7d33; }
.bar.loser { opacity: .45; }
.bar.timed-out { background: #ccc; }
/* Result status styles */
.status { font-size: 1rem; font-weight: 700; margin: .4rem 0; min-height: 1.4em; }
.status.win-v6 { color: #1565c0; }
.status.win-v4 { color: #e65100; }
.status.timeout { color: #b71c1c; }
.detail { font-size: .82rem; color: #666; min-height: 1.2em; }
/* Happy Eyeballs simulation -- race IPv6 and IPv4, use the first to finish */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const TIMEOUT_MS = 9999; /* sentinel value meaning the connection never arrives */
const MAX_DISPLAY_MS = 1200; /* cap the animation at this many ms for display purposes */
let animFrame = null;
/* Run one race: fire IPv6 immediately, stagger IPv4, pick the winner */
function runRace() {
if (animFrame !== null) cancelAnimationFrame(animFrame);
const v6ms = parseInt(document.getElementById('ipv6-latency').value, 10);
const v4ms = parseInt(document.getElementById('ipv4-latency').value, 10);
const stagger = parseInt(document.getElementById('stagger').value, 10);
const barV6 = document.getElementById('bar-v6');
const barV4 = document.getElementById('bar-v4');
const status = document.getElementById('status');
const detail = document.getElementById('detail');
/* Reset bars and status before each race */
barV6.style.width = '0';
barV4.style.width = '0';
barV6.className = 'bar bar-v6';
barV4.className = 'bar bar-v4';
status.textContent = '';
status.className = 'status';
detail.textContent = '';
const start = performance.now();
let resolved = false;
/* Animate progress bars at each animation frame */
function tick(now) {
const elapsed = now - start;
/* IPv6 bar progress */
const v6done = v6ms === TIMEOUT_MS ? false : elapsed >= v6ms;
const v6pct = v6ms === TIMEOUT_MS
? Math.min(elapsed / MAX_DISPLAY_MS * 0.7, 0.7)
: Math.min(elapsed / v6ms, 1);
/* IPv4 bar progress (starts after stagger delay) */
const v4started = elapsed >= stagger;
const v4effective = elapsed - stagger;
const v4done = v4ms === TIMEOUT_MS ? false : v4started && v4effective >= v4ms;
const v4pct = v4started
? (v4ms === TIMEOUT_MS
? Math.min(v4effective / MAX_DISPLAY_MS * 0.7, 0.7)
: Math.min(v4effective / v4ms, 1))
: 0;
barV6.style.width = (v6pct * 100) + '%';
barV4.style.width = (v4pct * 100) + '%';
if (!resolved) {
if (v6done) {
/* IPv6 finished first */
resolved = true;
barV6.className = 'bar bar-v6 winner';
barV4.className = 'bar bar-v4 loser';
status.textContent = 'IPv6 wins the socket!';
status.className = 'status win-v6';
detail.textContent = i18nFmt('Connected via IPv6 in %ms% ms.', { ms: v6ms });
} else if (v4done) {
/* IPv4 finished first (IPv6 was slower or timed out) */
resolved = true;
barV4.className = 'bar bar-v4 winner';
barV6.className = 'bar bar-v6 loser';
status.textContent = 'IPv4 wins the socket!';
status.className = 'status win-v4';
const saved = v6ms === TIMEOUT_MS
? i18nFmt('IPv6 timed out — IPv4 stepped in after the %stagger% ms stagger.', { stagger: stagger })
: i18nFmt('IPv6 was slower by %gap% ms net of stagger.', { gap: v6ms - v4ms - stagger });
detail.textContent = i18nFmt('Connected via IPv4 in %ms% ms.', { ms: stagger + v4ms }) + ' ' + saved;
}
}
/* If both addresses time out, show a failure state */
const bothTimedOut = v6ms === TIMEOUT_MS && v4ms === TIMEOUT_MS;
if (bothTimedOut && elapsed > MAX_DISPLAY_MS) {
if (!resolved) {
resolved = true;
barV6.className = 'bar bar-v6 timed-out';
barV4.className = 'bar bar-v4 timed-out';
status.textContent = 'Both addresses timed out — no connection.';
status.className = 'status timeout';
}
return;
}
if (!resolved || elapsed < MAX_DISPLAY_MS) {
animFrame = requestAnimationFrame(tick);
}
}
animFrame = requestAnimationFrame(tick);
}
document.getElementById('btn-race').addEventListener('click', runRace);
/* Run automatically on load so the demo is immediately visible */
runRace();
<!-- Controls for latency and stagger delay -->
<div class="controls">
<div class="control-group">
<label for="ipv6-latency">IPv6 latency (ms)</label>
<select id="ipv6-latency">
<option value="50">50 ms</option>
<option value="100">100 ms</option>
<option value="200" selected>200 ms</option>
<option value="400">400 ms</option>
<option value="800">800 ms</option>
<option value="9999">timeout</option>
</select>
</div>
<div class="control-group">
<label for="ipv4-latency">IPv4 latency (ms)</label>
<select id="ipv4-latency">
<option value="50">50 ms</option>
<option value="100">100 ms</option>
<option value="200">200 ms</option>
<option value="400" selected>400 ms</option>
<option value="800">800 ms</option>
<option value="9999">timeout</option>
</select>
</div>
<div class="control-group">
<label for="stagger">Stagger delay (ms)</label>
<select id="stagger">
<option value="0">0 ms</option>
<option value="100">100 ms</option>
<option value="250" selected>250 ms</option>
<option value="500">500 ms</option>
</select>
</div>
</div>
<button id="btn-race" type="button">Race!</button>
<!-- Animated timeline bars -->
<div class="timeline-wrap">
<div class="timeline-label">IPv6</div>
<div class="bar-track"><div id="bar-v6" class="bar bar-v6"></div></div>
<div class="timeline-label">IPv4</div>
<div class="bar-track"><div id="bar-v4" class="bar bar-v4"></div></div>
</div>
<div id="status" class="status"></div>
<div id="detail" class="detail"></div>
/* Base layout and control styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-wrap: wrap; gap: .7rem; margin-bottom: .8rem; }
.control-group { display: flex; flex-direction: column; gap: .2rem; }
label { font-size: .8rem; font-weight: 600; color: #555; }
select { font-size: .9rem; padding: .3rem .5rem; border: 1px solid #ccc; border-radius: 6px;
background: #fff; cursor: pointer; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1.1rem;
background: #1d3557; color: #fff; border: none; border-radius: 8px; cursor: pointer; }
button:hover { background: #16304e; }
/* Animated bar track styles */
.timeline-wrap { margin: 1rem 0 .5rem; display: grid;
grid-template-columns: 3.5rem 1fr; gap: .35rem .5rem; align-items: center; }
.timeline-label { font-size: .85rem; font-weight: 700; color: #444; text-align: right; }
.bar-track { background: #eaeef2; border-radius: 6px; height: 22px; overflow: hidden; position: relative; }
.bar { height: 100%; width: 0; border-radius: 6px; transition: width .05s linear; }
.bar-v6 { background: #2196f3; }
.bar-v4 { background: #ff9800; }
.bar.winner { outline: 3px solid #0a7d33; }
.bar.loser { opacity: .45; }
.bar.timed-out { background: #ccc; }
/* Result status styles */
.status { font-size: 1rem; font-weight: 700; margin: .4rem 0; min-height: 1.4em; }
.status.win-v6 { color: #1565c0; }
.status.win-v4 { color: #e65100; }
.status.timeout { color: #b71c1c; }
.detail { font-size: .82rem; color: #666; min-height: 1.2em; }
/* Happy Eyeballs simulation -- race IPv6 and IPv4, use the first to finish */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const TIMEOUT_MS = 9999; /* sentinel value meaning the connection never arrives */
const MAX_DISPLAY_MS = 1200; /* cap the animation at this many ms for display purposes */
let animFrame = null;
/* Run one race: fire IPv6 immediately, stagger IPv4, pick the winner */
function runRace() {
if (animFrame !== null) cancelAnimationFrame(animFrame);
const v6ms = parseInt(document.getElementById('ipv6-latency').value, 10);
const v4ms = parseInt(document.getElementById('ipv4-latency').value, 10);
const stagger = parseInt(document.getElementById('stagger').value, 10);
const barV6 = document.getElementById('bar-v6');
const barV4 = document.getElementById('bar-v4');
const status = document.getElementById('status');
const detail = document.getElementById('detail');
/* Reset bars and status before each race */
barV6.style.width = '0';
barV4.style.width = '0';
barV6.className = 'bar bar-v6';
barV4.className = 'bar bar-v4';
status.textContent = '';
status.className = 'status';
detail.textContent = '';
const start = performance.now();
let resolved = false;
/* Animate progress bars at each animation frame */
function tick(now) {
const elapsed = now - start;
/* IPv6 bar progress */
const v6done = v6ms === TIMEOUT_MS ? false : elapsed >= v6ms;
const v6pct = v6ms === TIMEOUT_MS
? Math.min(elapsed / MAX_DISPLAY_MS * 0.7, 0.7)
: Math.min(elapsed / v6ms, 1);
/* IPv4 bar progress (starts after stagger delay) */
const v4started = elapsed >= stagger;
const v4effective = elapsed - stagger;
const v4done = v4ms === TIMEOUT_MS ? false : v4started && v4effective >= v4ms;
const v4pct = v4started
? (v4ms === TIMEOUT_MS
? Math.min(v4effective / MAX_DISPLAY_MS * 0.7, 0.7)
: Math.min(v4effective / v4ms, 1))
: 0;
barV6.style.width = (v6pct * 100) + '%';
barV4.style.width = (v4pct * 100) + '%';
if (!resolved) {
if (v6done) {
/* IPv6 finished first */
resolved = true;
barV6.className = 'bar bar-v6 winner';
barV4.className = 'bar bar-v4 loser';
status.textContent = 'IPv6 wins the socket!';
status.className = 'status win-v6';
detail.textContent = i18nFmt('Connected via IPv6 in %ms% ms.', { ms: v6ms });
} else if (v4done) {
/* IPv4 finished first (IPv6 was slower or timed out) */
resolved = true;
barV4.className = 'bar bar-v4 winner';
barV6.className = 'bar bar-v6 loser';
status.textContent = 'IPv4 wins the socket!';
status.className = 'status win-v4';
const saved = v6ms === TIMEOUT_MS
? i18nFmt('IPv6 timed out — IPv4 stepped in after the %stagger% ms stagger.', { stagger: stagger })
: i18nFmt('IPv6 was slower by %gap% ms net of stagger.', { gap: v6ms - v4ms - stagger });
detail.textContent = i18nFmt('Connected via IPv4 in %ms% ms.', { ms: stagger + v4ms }) + ' ' + saved;
}
}
/* If both addresses time out, show a failure state */
const bothTimedOut = v6ms === TIMEOUT_MS && v4ms === TIMEOUT_MS;
if (bothTimedOut && elapsed > MAX_DISPLAY_MS) {
if (!resolved) {
resolved = true;
barV6.className = 'bar bar-v6 timed-out';
barV4.className = 'bar bar-v4 timed-out';
status.textContent = 'Both addresses timed out — no connection.';
status.className = 'status timeout';
}
return;
}
if (!resolved || elapsed < MAX_DISPLAY_MS) {
animFrame = requestAnimationFrame(tick);
}
}
animFrame = requestAnimationFrame(tick);
}
document.getElementById('btn-race').addEventListener('click', runRace);
/* Run automatically on load so the demo is immediately visible */
runRace();
Notice what happens at the extremes. When IPv6 is much faster, it wins every time. When IPv6 is broken (set its latency to "timeout"), IPv4 takes over after a brief wait — still far faster than the old sequential fallback that could take 20+ seconds. The stagger delay is the key tuning knob: too short and you waste IPv4 capacity on races IPv6 would have won; too long and a broken IPv6 delays the user.
Comments
Loading comments...