From 170ee2bfeb85f3e3dc74b304754eca03511e3ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:24:14 +0900 Subject: [PATCH] =?UTF-8?q?perf(frontend):=20=E3=82=BF=E3=83=96=E3=81=8C?= =?UTF-8?q?=E3=83=90=E3=83=83=E3=82=AF=E3=82=B0=E3=83=A9=E3=82=A6=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=81=AA=E9=96=93=E3=81=AEsetInterval=E3=82=92?= =?UTF-8?q?=E5=81=9C=E6=AD=A2=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=20(#17769)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + packages/frontend-shared/js/interval.ts | 59 +++++++++++++++++++ packages/frontend-shared/js/use-interval.ts | 39 ++++++++---- .../src/components/MkChatHistories.vue | 2 +- .../src/composables/use-lowres-time.ts | 3 +- .../src/composables/use-note-capture.ts | 5 +- .../frontend/src/pages/reversi/game.board.vue | 6 +- packages/frontend/src/pages/reversi/index.vue | 6 +- packages/frontend/src/preferences.ts | 5 +- packages/frontend/src/widgets/WidgetRss.vue | 10 ++-- .../frontend/src/widgets/WidgetRssTicker.vue | 10 ++-- .../frontend/src/widgets/WidgetUnixClock.vue | 12 ++-- 12 files changed, 120 insertions(+), 38 deletions(-) create mode 100644 packages/frontend-shared/js/interval.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 826b0b0cff..d3edf8874c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - Fix: 一部の画像のみセンシティブなとき、ビューワー内で画像を切り替えるとセンシティブな画像がそのまま表示される問題を修正 - Fix: 一部の画像をビューワーで読み込んだ際に正しく表示されない問題を修正 - Enhance: タイムラインの読み込みパフォーマンスを改善 +- Enhance: タブがバックグラウンドの間は必要ない定期更新処理を停止するように - Fix: 「画像を新しいタブで開く」が機能しなくなっていた問題を修正 - Fix: デバイスタイプをスマートフォンに固定している状態で画面幅が広いとき、画面左上のアイコンが表示されない問題を修正 - Fix: チャットでIMEの変換を確定するEnterでメッセージが送信されてしまうことがある問題を修正 diff --git a/packages/frontend-shared/js/interval.ts b/packages/frontend-shared/js/interval.ts new file mode 100644 index 0000000000..37abf8bb51 --- /dev/null +++ b/packages/frontend-shared/js/interval.ts @@ -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(); + }; +} diff --git a/packages/frontend-shared/js/use-interval.ts b/packages/frontend-shared/js/use-interval.ts index b50e78c3cc..d9d00450e0 100644 --- a/packages/frontend-shared/js/use-interval.ts +++ b/packages/frontend-shared/js/use-interval.ts @@ -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(() => { diff --git a/packages/frontend/src/components/MkChatHistories.vue b/packages/frontend/src/components/MkChatHistories.vue index b33ed428c7..fb07faded2 100644 --- a/packages/frontend/src/components/MkChatHistories.vue +++ b/packages/frontend/src/components/MkChatHistories.vue @@ -87,7 +87,7 @@ onDeactivated(() => { useInterval(() => { // TODO: DOM的にバックグラウンドになっていないかどうかも考慮する - if (!window.document.hidden && isActivated) { + if (isActivated) { fetchHistory(); } }, 1000 * 10, { diff --git a/packages/frontend/src/composables/use-lowres-time.ts b/packages/frontend/src/composables/use-lowres-time.ts index 3c5b561f51..6371fa65ad 100644 --- a/packages/frontend/src/composables/use-lowres-time.ts +++ b/packages/frontend/src/composables/use-lowres-time.ts @@ -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); diff --git a/packages/frontend/src/composables/use-note-capture.ts b/packages/frontend/src/composables/use-note-capture.ts index 25a9383cd5..6d2b81c751 100644 --- a/packages/frontend/src/composables/use-note-capture.ts +++ b/packages/frontend/src/composables/use-note-capture.ts @@ -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', { diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 5988604652..9b0394c0b6 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -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 }) { diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index 9a737e93ac..de52684bf3 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -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 => { diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts index 7b03823407..91866f1fbd 100644 --- a/packages/frontend/src/preferences.ts +++ b/packages/frontend/src/preferences.ts @@ -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(() => { diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index 4a9842d6e6..dae3b270d4 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -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, }); diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index 9ed21e6d00..246b96fca0 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -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, }); diff --git a/packages/frontend/src/widgets/WidgetUnixClock.vue b/packages/frontend/src/widgets/WidgetUnixClock.vue index 7100c55e17..ce96b29d5f 100644 --- a/packages/frontend/src/widgets/WidgetUnixClock.vue +++ b/packages/frontend/src/widgets/WidgetUnixClock.vue @@ -17,6 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only