// vite.config.ts
import { defineConfig } from 'vite'
import { hmrVisibility } from './vite-plugin-hmr-visibility'
// https://vite.dev/config/
export default defineConfig({
plugins: [
hmrVisibility({
disconnectDelay: 2000, // Wait 2 seconds before disconnecting
debug: true, // Enable debug logging
}),
],
server: {
// Prevent Vite from aggressive reconnection attempts
hmr: {
overlay: false, // Disable error overlay which can trigger reconnections
},
},
})
// vite-plugin-hmr-visibility.ts
/**
* Vite Plugin: HMR Visibility Manager
* Automatically disconnects/reconnects HMR websocket based on tab visibility
* to reduce serverless billing costs
*/
import type { Plugin } from 'vite'
export interface HMRVisibilityOptions {
/**
* Grace period before disconnecting (ms)
* @default 2000
*/
disconnectDelay?: number
/**
* Enable debug logging
* @default false
*/
debug?: boolean
}
export function hmrVisibility(options: HMRVisibilityOptions = {}): Plugin {
const {
disconnectDelay = 2000,
debug = false,
} = options
return {
name: 'vite-plugin-hmr-visibility',
apply: 'serve', // Only apply in dev mode
transformIndexHtml() {
// Inject the HMR visibility manager as an inline script
return [
{
tag: 'script',
attrs: { type: 'module' },
children: `
// HMR Visibility Manager - Disconnects HMR when tab is hidden
(function() {
const log = (...args) => {
if (${JSON.stringify(debug)}) {
console.log('[HMR Visibility]', ...args);
}
};
let disconnectTimer = null;
let isTabVisible = !document.hidden;
const DISCONNECT_DELAY = ${JSON.stringify(disconnectDelay)};
// Store reference to original functions
const OriginalWebSocket = window.WebSocket;
const OriginalSetTimeout = window.setTimeout;
const OriginalSetInterval = window.setInterval;
let hmrSocket = null;
let shouldBlockReconnect = false;
let blockedTimers = new Set();
// Patch setTimeout and setInterval to block Vite's reconnection timers
window.setTimeout = function(callback, delay, ...args) {
// Block timers when tab is hidden and it looks like a reconnection attempt
if (shouldBlockReconnect && delay && delay >= 500 && delay <= 5000) {
const callbackStr = callback.toString();
if (callbackStr.includes('connect') || callbackStr.includes('WebSocket') || callbackStr.includes('ws')) {
log('Blocked reconnection timer (setTimeout)');
const timerId = OriginalSetTimeout(() => {}, 999999999);
blockedTimers.add(timerId);
return timerId;
}
}
return OriginalSetTimeout.call(this, callback, delay, ...args);
};
window.setInterval = function(callback, delay, ...args) {
// Block intervals when tab is hidden
if (shouldBlockReconnect) {
const callbackStr = callback.toString();
if (callbackStr.includes('connect') || callbackStr.includes('WebSocket') || callbackStr.includes('ws')) {
log('Blocked reconnection interval (setInterval)');
const timerId = OriginalSetInterval(() => {}, 999999999);
blockedTimers.add(timerId);
return timerId;
}
}
return OriginalSetInterval.call(this, callback, delay, ...args);
};
// Patch WebSocket to track and block HMR connections
window.WebSocket = function(url, protocols) {
// Block new WebSocket connections when tab is hidden
if (shouldBlockReconnect) {
log('Blocked WebSocket connection attempt while tab is hidden:', url);
// Return a fake WebSocket that stays in CONNECTING state forever
const fakeWs = {
readyState: 0, // CONNECTING (keeps Vite waiting)
close: () => { log('Fake WebSocket close called'); },
send: () => { log('Fake WebSocket send called'); },
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
onopen: null,
onclose: null,
onerror: null,
onmessage: null,
};
return fakeWs;
}
const ws = new OriginalWebSocket(url, protocols);
// Track HMR WebSocket and intercept its event handlers
if (typeof url === 'string') {
hmrSocket = ws;
log('HMR WebSocket created and tracked');
// Intercept the onclose setter to control reconnection behavior
let userOnClose = null;
Object.defineProperty(ws, 'onclose', {
get() { return userOnClose; },
set(handler) {
userOnClose = function(event) {
// If we're blocking reconnect, don't call Vite's onclose handler
if (shouldBlockReconnect) {
log('Suppressed onclose handler - blocking reconnection');
return;
}
// Otherwise, call the original handler
if (handler) {
handler.call(this, event);
}
};
},
configurable: true
});
// Also intercept addEventListener for 'close' events
const originalAddEventListener = ws.addEventListener;
ws.addEventListener = function(type, listener, options) {
if (type === 'close') {
const wrappedListener = function(event) {
if (shouldBlockReconnect) {
log('Suppressed close event listener - blocking reconnection');
return;
}
listener.call(this, event);
};
return originalAddEventListener.call(this, type, wrappedListener, options);
}
return originalAddEventListener.call(this, type, listener, options);
};
}
return ws;
};
// Copy static properties from original WebSocket
Object.setPrototypeOf(window.WebSocket, OriginalWebSocket);
window.WebSocket.prototype = OriginalWebSocket.prototype;
// Copy static constants
Object.defineProperty(window.WebSocket, 'CONNECTING', { value: 0, enumerable: true });
Object.defineProperty(window.WebSocket, 'OPEN', { value: 1, enumerable: true });
Object.defineProperty(window.WebSocket, 'CLOSING', { value: 2, enumerable: true });
Object.defineProperty(window.WebSocket, 'CLOSED', { value: 3, enumerable: true });
const disconnectHMR = () => {
if (hmrSocket && (hmrSocket.readyState === 0 || hmrSocket.readyState === 1)) {
log('Disconnecting HMR websocket (tab hidden) - saving costs ✅');
hmrSocket.close(1000, 'Tab hidden');
hmrSocket = null;
}
shouldBlockReconnect = true;
};
const reconnectHMR = () => {
shouldBlockReconnect = false;
// Clear any blocked timers
blockedTimers.forEach(timerId => {
try {
clearTimeout(timerId);
clearInterval(timerId);
} catch (e) {}
});
blockedTimers.clear();
// If HMR was disconnected, reload to reconnect
if (!hmrSocket || hmrSocket.readyState === 3) {
log('Reloading page to restore HMR connection');
setTimeout(() => window.location.reload(), 100);
}
};
// Handle tab visibility changes
document.addEventListener('visibilitychange', () => {
isTabVisible = !document.hidden;
if (document.hidden) {
log('Tab hidden - will disconnect HMR in ' + DISCONNECT_DELAY + 'ms');
disconnectTimer = OriginalSetTimeout(() => {
if (document.hidden) {
disconnectHMR();
}
}, DISCONNECT_DELAY);
} else {
log('Tab visible - allowing HMR reconnection');
// Clear disconnect timer if tab becomes visible again
if (disconnectTimer) {
clearTimeout(disconnectTimer);
disconnectTimer = null;
}
OriginalSetTimeout(() => {
reconnectHMR();
}, 100);
}
});
// Handle page freeze events
window.addEventListener('freeze', () => {
log('Page freeze detected - disconnecting HMR immediately');
disconnectHMR();
}, { capture: true });
window.addEventListener('resume', () => {
log('Page resume detected - allowing HMR reconnection');
OriginalSetTimeout(() => {
reconnectHMR();
}, 100);
}, { capture: true });
log('HMR Visibility Manager initialized - will block reconnection attempts when tab is hidden');
})();
`,
injectTo: 'head-prepend',
},
]
},
}
}