diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index 0c5ee06197..0d5bd78b09 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -8,7 +8,12 @@ import { markRaw } from 'vue';
 import { $i } from '@/account.js';
 import { wsOrigin } from '@/config.js';
 
+// heart beat interval in ms
+const HEART_BEAT_INTERVAL = 1000 * 60;
+
 let stream: Misskey.Stream | null = null;
+let timeoutHeartBeat: ReturnType<typeof setTimeout> | null = null;
+let lastHeartbeatCall = 0;
 
 export function useStream(): Misskey.Stream {
 	if (stream) return stream;
@@ -17,7 +22,18 @@ export function useStream(): Misskey.Stream {
 		token: $i.token,
 	} : null));
 
-	window.setTimeout(heartbeat, 1000 * 60);
+	if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat);
+	timeoutHeartBeat = window.setTimeout(heartbeat, HEART_BEAT_INTERVAL);
+
+	// send heartbeat right now when last send time is over HEART_BEAT_INTERVAL
+	document.addEventListener('visibilitychange', () => {
+		if (
+			!stream
+			|| document.visibilityState !== 'visible'
+			|| Date.now() - lastHeartbeatCall < HEART_BEAT_INTERVAL
+		) return;
+		heartbeat();
+	});
 
 	return stream;
 }
@@ -26,5 +42,7 @@ function heartbeat(): void {
 	if (stream != null && document.visibilityState === 'visible') {
 		stream.heartbeat();
 	}
-	window.setTimeout(heartbeat, 1000 * 60);
+	lastHeartbeatCall = Date.now();
+	if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat);
+	timeoutHeartBeat = window.setTimeout(heartbeat, HEART_BEAT_INTERVAL);
 }