mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-25 07:25:04 +02:00
perf(frontend): タブがバックグラウンドな間のsetIntervalを停止するように (#17769)
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
- Fix: 一部の画像のみセンシティブなとき、ビューワー内で画像を切り替えるとセンシティブな画像がそのまま表示される問題を修正
|
||||
- Fix: 一部の画像をビューワーで読み込んだ際に正しく表示されない問題を修正
|
||||
- Enhance: タイムラインの読み込みパフォーマンスを改善
|
||||
- Enhance: タブがバックグラウンドの間は必要ない定期更新処理を停止するように
|
||||
- Fix: 「画像を新しいタブで開く」が機能しなくなっていた問題を修正
|
||||
- Fix: デバイスタイプをスマートフォンに固定している状態で画面幅が広いとき、画面左上のアイコンが表示されない問題を修正
|
||||
- Fix: チャットでIMEの変換を確定するEnterでメッセージが送信されてしまうことがある問題を修正
|
||||
|
||||
59
packages/frontend-shared/js/interval.ts
Normal file
59
packages/frontend-shared/js/interval.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/** ドキュメントがアクティブになっていない間は実行を止めるsetInterval。戻り値の関数でdisposeする */
|
||||
export function createVisibilityAwareInterval(fn: () => void, interval: number, options: {
|
||||
immediate?: boolean;
|
||||
} = {}): () => void {
|
||||
let intervalId: number | null = null;
|
||||
let lastCalledAt: number | null = null;
|
||||
|
||||
const tick = () => {
|
||||
lastCalledAt = Date.now();
|
||||
fn();
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (intervalId != null) return;
|
||||
if (lastCalledAt == null) {
|
||||
if (options.immediate) {
|
||||
tick();
|
||||
} else {
|
||||
lastCalledAt = Date.now();
|
||||
}
|
||||
} else if (Date.now() - lastCalledAt >= interval) {
|
||||
// もし前回の呼び出しからinterval以上の時間が経過していたら、即座に呼び出す
|
||||
// (非アクティブ→アクティブに戻った場合など)
|
||||
tick();
|
||||
}
|
||||
intervalId = window.setInterval(tick, interval);
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
if (intervalId != null) {
|
||||
window.clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const onVisibilityChange = () => {
|
||||
if (window.document.visibilityState === 'visible') {
|
||||
start();
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
};
|
||||
|
||||
window.document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
|
||||
if (window.document.visibilityState === 'visible') {
|
||||
start();
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
stop();
|
||||
};
|
||||
}
|
||||
@@ -4,34 +4,47 @@
|
||||
*/
|
||||
|
||||
import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
|
||||
import { createVisibilityAwareInterval } from './interval.js';
|
||||
|
||||
export function useInterval(fn: () => void, interval: number, options: {
|
||||
immediate: boolean;
|
||||
afterMounted: boolean;
|
||||
keepRunningWhenHidden?: boolean;
|
||||
}): (() => void) | undefined {
|
||||
if (Number.isNaN(interval)) return;
|
||||
|
||||
let intervalId: number | null = null;
|
||||
let disposer: (() => void) | null = null;
|
||||
|
||||
const start = () => {
|
||||
if (options.keepRunningWhenHidden) {
|
||||
if (options.immediate) fn();
|
||||
const intervalId = window.setInterval(fn, interval);
|
||||
disposer = () => {
|
||||
window.clearInterval(intervalId);
|
||||
};
|
||||
} else {
|
||||
disposer = createVisibilityAwareInterval(fn, interval, { immediate: options.immediate });
|
||||
}
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
if (disposer) {
|
||||
disposer();
|
||||
disposer = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (options.afterMounted) {
|
||||
onMounted(() => {
|
||||
if (options.immediate) fn();
|
||||
intervalId = window.setInterval(fn, interval);
|
||||
start();
|
||||
});
|
||||
} else {
|
||||
if (options.immediate) fn();
|
||||
intervalId = window.setInterval(fn, interval);
|
||||
start();
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
if (intervalId) window.clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
};
|
||||
|
||||
onActivated(() => {
|
||||
if (intervalId) return;
|
||||
if (options.immediate) fn();
|
||||
intervalId = window.setInterval(fn, interval);
|
||||
if (disposer) return;
|
||||
start();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
|
||||
@@ -87,7 +87,7 @@ onDeactivated(() => {
|
||||
|
||||
useInterval(() => {
|
||||
// TODO: DOM的にバックグラウンドになっていないかどうかも考慮する
|
||||
if (!window.document.hidden && isActivated) {
|
||||
if (isActivated) {
|
||||
fetchHistory();
|
||||
}
|
||||
}, 1000 * 10, {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ref, readonly, computed } from 'vue';
|
||||
import { createVisibilityAwareInterval } from '@@/js/interval.js';
|
||||
|
||||
const time = ref(Date.now());
|
||||
|
||||
@@ -29,6 +30,6 @@ export function useLowresTime() {
|
||||
return computed(() => Math.max(time.value, now));
|
||||
}
|
||||
|
||||
window.setInterval(() => {
|
||||
createVisibilityAwareInterval(() => {
|
||||
time.value = Date.now();
|
||||
}, TIME_UPDATE_INTERVAL);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { onUnmounted, reactive } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { createVisibilityAwareInterval } from '@@/js/interval.js';
|
||||
import type { Reactive } from 'vue';
|
||||
import type { NoteUpdatedEvent } from 'misskey-js/streaming.types.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
@@ -68,7 +69,8 @@ const POLLING_INTERVAL =
|
||||
prefer.s.pollingInterval === 3 ? MIN_POLLING_INTERVAL :
|
||||
MIN_POLLING_INTERVAL;
|
||||
|
||||
window.setInterval(() => {
|
||||
// documentが非表示の間はポーリングを停止する
|
||||
createVisibilityAwareInterval(() => {
|
||||
const ids = [...pollingQueue.entries()]
|
||||
.filter(([k, v]) => Date.now() - v.lastAddedAt < 1000 * 60 * 5) // 追加されてから一定時間経過したものは省く
|
||||
.map(([k, v]) => k)
|
||||
@@ -76,7 +78,6 @@ window.setInterval(() => {
|
||||
.slice(0, CAPTURE_MAX);
|
||||
|
||||
if (ids.length === 0) return;
|
||||
if (window.document.hidden) return;
|
||||
|
||||
// まとめてリクエストするのではなく、個別にHTTPリクエスト投げてCDNにキャッシュさせた方がサーバーの負荷低減には良いかもしれない?
|
||||
misskeyApi('notes/show-partial-bulk', {
|
||||
|
||||
@@ -303,7 +303,11 @@ if (!props.game.isEnded) {
|
||||
props.connection!.send('claimTimeIsUp', {});
|
||||
}
|
||||
}
|
||||
}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
|
||||
}, TIMER_INTERVAL_SEC * 1000, {
|
||||
immediate: false,
|
||||
afterMounted: true,
|
||||
keepRunningWhenHidden: true, // 対局の制限時間管理のため、バックグラウンドでも止めない
|
||||
});
|
||||
}
|
||||
|
||||
async function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
||||
|
||||
@@ -248,7 +248,11 @@ async function accept(user: Misskey.entities.UserLite) {
|
||||
}
|
||||
}
|
||||
|
||||
useInterval(matchHeatbeat, 1000 * 5, { immediate: false, afterMounted: true });
|
||||
useInterval(matchHeatbeat, 1000 * 5, {
|
||||
immediate: false,
|
||||
afterMounted: true,
|
||||
keepRunningWhenHidden: true, // バックグラウンドタブでもマッチング待機を維持する必要がある
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
misskeyApi('reversi/invitations').then(_invitations => {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { BroadcastChannel } from 'broadcast-channel';
|
||||
import { createVisibilityAwareInterval } from '@@/js/interval.js';
|
||||
import type { StorageProvider } from '@/preferences/manager.js';
|
||||
import { cloudBackup } from '@/preferences/utility.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
@@ -143,10 +144,10 @@ preferencesChannel.addEventListener('message', (msg) => {
|
||||
//#region 定期クラウドバックアップ
|
||||
let latestBackupAt = 0;
|
||||
|
||||
window.setInterval(() => {
|
||||
// documentが非表示の間は実行されない (同期されていない古い値がバックアップされるのを防ぐ意味もある)
|
||||
createVisibilityAwareInterval(() => {
|
||||
if ($i == null) return;
|
||||
if (!store.s.enablePreferencesAutoCloudBackup) return;
|
||||
if (window.document.visibilityState !== 'visible') return; // 同期されていない古い値がバックアップされるのを防ぐ
|
||||
if (prefer.profile.modifiedAt <= latestBackupAt) return;
|
||||
|
||||
cloudBackup().then(() => {
|
||||
|
||||
@@ -75,11 +75,9 @@ const fetchEndpoint = computed(() => {
|
||||
url.searchParams.set('url', widgetProps.url);
|
||||
return url.toString();
|
||||
});
|
||||
const intervalClear = ref<(() => void) | undefined>();
|
||||
let intervalClear: (() => void) | null | undefined = null;
|
||||
|
||||
const tick = () => {
|
||||
if (window.document.visibilityState === 'hidden' && rawItems.value.length !== 0) return;
|
||||
|
||||
window.fetch(fetchEndpoint.value, {})
|
||||
.then(res => res.json())
|
||||
.then((feed: Misskey.entities.FetchRssResponse) => {
|
||||
@@ -90,10 +88,10 @@ const tick = () => {
|
||||
|
||||
watch(fetchEndpoint, tick);
|
||||
watch(() => widgetProps.refreshIntervalSec, () => {
|
||||
if (intervalClear.value) {
|
||||
intervalClear.value();
|
||||
if (intervalClear != null) {
|
||||
intervalClear();
|
||||
}
|
||||
intervalClear.value = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
|
||||
intervalClear = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
|
||||
immediate: true,
|
||||
afterMounted: true,
|
||||
});
|
||||
|
||||
@@ -113,13 +113,11 @@ const fetchEndpoint = computed(() => {
|
||||
url.searchParams.set('url', widgetProps.url);
|
||||
return url;
|
||||
});
|
||||
const intervalClear = ref<(() => void) | undefined>();
|
||||
let intervalClear: (() => void) | null | undefined = null;
|
||||
|
||||
const key = ref(0);
|
||||
|
||||
const tick = () => {
|
||||
if (window.document.visibilityState === 'hidden' && rawItems.value.length !== 0) return;
|
||||
|
||||
window.fetch(fetchEndpoint.value, {})
|
||||
.then(res => res.json())
|
||||
.then((feed: Misskey.entities.FetchRssResponse) => {
|
||||
@@ -131,10 +129,10 @@ const tick = () => {
|
||||
|
||||
watch(fetchEndpoint, tick);
|
||||
watch(() => widgetProps.refreshIntervalSec, () => {
|
||||
if (intervalClear.value) {
|
||||
intervalClear.value();
|
||||
if (intervalClear != null) {
|
||||
intervalClear();
|
||||
}
|
||||
intervalClear.value = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
|
||||
intervalClear = useInterval(tick, Math.max(10000, widgetProps.refreshIntervalSec * 1000), {
|
||||
immediate: true,
|
||||
afterMounted: true,
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted, ref, watch } from 'vue';
|
||||
import { createVisibilityAwareInterval } from '@@/js/interval.js';
|
||||
import { useWidgetPropsManager } from './widget.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||
@@ -59,7 +60,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
|
||||
emit,
|
||||
);
|
||||
|
||||
let intervalId: number | null = null;
|
||||
let disposeInterval: (() => void) | null = null;
|
||||
let rafRequestId: number | null = null;
|
||||
const ss = ref('');
|
||||
const ms = ref('');
|
||||
@@ -83,9 +84,9 @@ const tick = () => {
|
||||
};
|
||||
|
||||
const clearTimers = () => {
|
||||
if (intervalId) {
|
||||
window.clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
if (disposeInterval) {
|
||||
disposeInterval();
|
||||
disposeInterval = null;
|
||||
}
|
||||
if (rafRequestId) {
|
||||
window.cancelAnimationFrame(rafRequestId);
|
||||
@@ -99,12 +100,13 @@ watch(() => widgetProps.showMs, (to) => {
|
||||
clearTimers();
|
||||
|
||||
if (to) {
|
||||
// rafはdocumentが非表示の間はブラウザによって自動的に停止される
|
||||
rafRequestId = window.requestAnimationFrame(function loop() {
|
||||
tick();
|
||||
rafRequestId = window.requestAnimationFrame(loop);
|
||||
});
|
||||
} else {
|
||||
intervalId = window.setInterval(tick, 1000);
|
||||
disposeInterval = createVisibilityAwareInterval(tick, 1000);
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user