diff --git a/src/main/index.ts b/src/main/index.ts index 8335615d0..90e85e736 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -497,6 +497,9 @@ if (shouldDisableMediaFeatures) { // https://github.com/electron/electron/issues/46538#issuecomment-2808806722 app.commandLine.appendSwitch('gtk-version', '3'); +// Enable garbage collection API +app.commandLine.appendSwitch('js-flags', '--expose-gc'); + // Must duplicate with the one in renderer process settings.store.ts enum BindingActions { GLOBAL_SEARCH = 'globalSearch', diff --git a/src/preload/utils.ts b/src/preload/utils.ts index f7c4228a3..a3f2ded02 100644 --- a/src/preload/utils.ts +++ b/src/preload/utils.ts @@ -1,4 +1,4 @@ -import { ipcRenderer, IpcRendererEvent } from 'electron'; +import { ipcRenderer, IpcRendererEvent, webFrame } from 'electron'; import { disableAutoUpdates, isLinux, isMacOS, isWindows } from '../main/utils'; @@ -39,9 +39,28 @@ const download = (url: string) => { ipcRenderer.send('download-url', url); }; +const forceGarbageCollection = (): boolean => { + try { + if (typeof global.gc === 'function') { + global.gc(); + webFrame.clearCache(); + return true; + } + if (typeof window.gc === 'function') { + window.gc(); + webFrame.clearCache(); + return true; + } + return false; + } catch { + return false; + } +}; + export const utils = { disableAutoUpdates, download, + forceGarbageCollection, isLinux, isMacOS, isWindows, diff --git a/src/renderer/hooks/use-garbage-collection.ts b/src/renderer/hooks/use-garbage-collection.ts new file mode 100644 index 000000000..291cbabae --- /dev/null +++ b/src/renderer/hooks/use-garbage-collection.ts @@ -0,0 +1,42 @@ +import isElectron from 'is-electron'; +import { useEffect, useRef } from 'react'; +import { useLocation } from 'react-router'; + +const GARBAGE_COLLECTION_INTERVAL = 1000 * 60 * 5; + +export const useGarbageCollection = () => { + const intervalIdRef = useRef(null); + + // Clear the cache on an interval + useEffect(() => { + if (!isElectron()) { + return; + } + + intervalIdRef.current = setInterval(() => { + window.api?.utils?.forceGarbageCollection?.(); + }, GARBAGE_COLLECTION_INTERVAL); + + return () => { + if (intervalIdRef.current) { + clearInterval(intervalIdRef.current); + } + }; + }, []); + + const location = useLocation(); + + // Clear the cache when the location changes + useEffect(() => { + if (!isElectron()) { + return; + } + + // Clear the interval when the location changes + if (intervalIdRef.current) { + clearInterval(intervalIdRef.current); + } + + window.api?.utils?.forceGarbageCollection?.(); + }, [location]); +}; diff --git a/src/renderer/layouts/responsive-layout.tsx b/src/renderer/layouts/responsive-layout.tsx index b92da9789..4afb06005 100644 --- a/src/renderer/layouts/responsive-layout.tsx +++ b/src/renderer/layouts/responsive-layout.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router'; import { useAppTracker } from '/@/renderer/features/analytics/hooks/use-app-tracker'; import { CommandPalette } from '/@/renderer/features/search/components/command-palette'; +import { useGarbageCollection } from '/@/renderer/hooks/use-garbage-collection'; import { useIsMobile } from '/@/renderer/hooks/use-is-mobile'; import { DefaultLayout } from '/@/renderer/layouts/default-layout'; import { MobileLayout } from '/@/renderer/layouts/mobile-layout/mobile-layout'; @@ -31,6 +32,7 @@ const ResponsiveLayoutBase = ({ shell }: ResponsiveLayoutProps) => { export const ResponsiveLayout = ({ shell }: ResponsiveLayoutProps) => { useAppTracker(); + useGarbageCollection(); return ( <>