mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
perf(front,back): 查询缓存持久化 + staleTime 分层 — 重开应用秒出上次数据, 不再全量重拉
- 新增 lib/queryPersist.ts: PersistQueryClientProvider 接入 localStorage 持久化, 白名单 13 个慢变查询族 (设置/偏好/能力矩阵/自选清单与分组/策略池/regime 全家/自定义信号), 键首段精确匹配不误伤 watchlist-enriched 等实时行情族; 24h maxAge + 版本 buster; 快变数据 (实时价/分钟K/告警) 刻意不持久化, 避免陈旧价格误导 - staleTime 分层: settings/preferences 30s, capability-matrix 5min, 自选清单 30s, 策略池 60s, 自定义信号 60s — 切页不再对慢变数据反复请求 - regime_builder.load_regime_history 加 (path,mtime,size) 签名缓存: Regime 页 三个端点原先每请求各自全量重读同一 parquet, 现共享一份, 写入后 mtime 失效 验证: pnpm build 通过; 浏览器实测持久化恢复路径 (注入缓存刷新后守卫直接放行, 不再整屏 loading); regime 相关 pytest 22 passed; ruff 零新增; git diff --check 干净
This commit is contained in:
@@ -512,13 +512,27 @@ def regime_path(data_dir: Path) -> Path:
|
||||
return data_dir / REGIME_DIR / "part.parquet"
|
||||
|
||||
|
||||
# (path, mtime_ns, size) 签名缓存 — regime 三个端点每请求各读一次全量 parquet,
|
||||
# 重算/导入改变文件后 mtime 变化自动失效; Polars 帧由调用方只读 (操作均为函数式)
|
||||
_REGIME_HISTORY_CACHE: pl.DataFrame | None = None
|
||||
_REGIME_HISTORY_SIG: tuple[str, int, int] | None = None
|
||||
|
||||
|
||||
def load_regime_history(data_dir: Path) -> pl.DataFrame:
|
||||
"""读取全部 regime 时序; 不存在返回空 DataFrame。"""
|
||||
"""读取全部 regime 时序; 不存在返回空 DataFrame (带 mtime 签名缓存)。"""
|
||||
global _REGIME_HISTORY_CACHE, _REGIME_HISTORY_SIG
|
||||
p = regime_path(data_dir)
|
||||
if not p.exists():
|
||||
return pl.DataFrame()
|
||||
try:
|
||||
return pl.read_parquet(p)
|
||||
sig = (str(p), p.stat().st_mtime_ns, p.stat().st_size)
|
||||
except OSError:
|
||||
return pl.DataFrame()
|
||||
if _REGIME_HISTORY_CACHE is not None and sig == _REGIME_HISTORY_SIG:
|
||||
return _REGIME_HISTORY_CACHE
|
||||
try:
|
||||
df = pl.read_parquet(p)
|
||||
_REGIME_HISTORY_CACHE = df
|
||||
_REGIME_HISTORY_SIG = sig
|
||||
return df
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("load_regime_history failed: %s", e)
|
||||
return pl.DataFrame()
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource/inter": "^5.3.0",
|
||||
"@fontsource/jetbrains-mono": "^5.3.0",
|
||||
"@tanstack/query-sync-storage-persister": "^5.104.1",
|
||||
"@tanstack/react-query": "^5.55.0",
|
||||
"@tanstack/react-query-persist-client": "^5.104.1",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"clsx": "^2.1.1",
|
||||
"echarts": "^5.5.0",
|
||||
|
||||
Generated
+38
@@ -23,9 +23,15 @@ importers:
|
||||
'@fontsource/jetbrains-mono':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
'@tanstack/query-sync-storage-persister':
|
||||
specifier: ^5.104.1
|
||||
version: 5.104.1
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.55.0
|
||||
version: 5.100.11(react@18.3.1)
|
||||
'@tanstack/react-query-persist-client':
|
||||
specifier: ^5.104.1
|
||||
version: 5.104.1(@tanstack/react-query@5.100.11(react@18.3.1))(react@18.3.1)
|
||||
'@tanstack/react-virtual':
|
||||
specifier: ^3.13.12
|
||||
version: 3.14.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -559,6 +565,21 @@ packages:
|
||||
'@tanstack/query-core@5.100.11':
|
||||
resolution: {integrity: sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw==}
|
||||
|
||||
'@tanstack/query-core@5.104.1':
|
||||
resolution: {integrity: sha512-TiRghcrGkUTE+M4JBxvpPSpBhxvIzQairw+cksrHYGr+2uwC77yW1SY9nYxnYRYOppvucf8rEg6b5byoz4rTpw==}
|
||||
|
||||
'@tanstack/query-persist-client-core@5.104.1':
|
||||
resolution: {integrity: sha512-ONiadXqe56iNrw6RESqKILpTIVG9ZMI/DageTQnrBDt/nZ6WiP4G13iIiMwJEsiO12gQ3Gychx8tcFIPw4k25g==}
|
||||
|
||||
'@tanstack/query-sync-storage-persister@5.104.1':
|
||||
resolution: {integrity: sha512-eDsBMNds48+sjXILn0dQKu+uYwfh9tGIB+Rb0cKRdoxD22HHBMzJes5DJOhJVLUhBQbxtoFq+Hpha/Vohh14NQ==}
|
||||
|
||||
'@tanstack/react-query-persist-client@5.104.1':
|
||||
resolution: {integrity: sha512-kU3YaqjkwEUzUUfPF0JVg56d6gJKWHaFVqOlQztwc4C2IT0P5sv/23orrSoQqjxKaUe6Ejw5IIQtA/G12uNmow==}
|
||||
peerDependencies:
|
||||
'@tanstack/react-query': ^5.104.1
|
||||
react: ^18 || ^19
|
||||
|
||||
'@tanstack/react-query@5.100.11':
|
||||
resolution: {integrity: sha512-J0f9s5x3LE1450nNNfYx+e/n0DMa0uOBdFJUy5r0RvmsXd4nB/n0rbHtHI1vYXhikNFan+wf51p6Tmp4c8ucrg==}
|
||||
peerDependencies:
|
||||
@@ -1765,6 +1786,23 @@ snapshots:
|
||||
|
||||
'@tanstack/query-core@5.100.11': {}
|
||||
|
||||
'@tanstack/query-core@5.104.1': {}
|
||||
|
||||
'@tanstack/query-persist-client-core@5.104.1':
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.104.1
|
||||
|
||||
'@tanstack/query-sync-storage-persister@5.104.1':
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.104.1
|
||||
'@tanstack/query-persist-client-core': 5.104.1
|
||||
|
||||
'@tanstack/react-query-persist-client@5.104.1(@tanstack/react-query@5.100.11(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@tanstack/query-persist-client-core': 5.104.1
|
||||
'@tanstack/react-query': 5.100.11(react@18.3.1)
|
||||
react: 18.3.1
|
||||
|
||||
'@tanstack/react-query@5.100.11(react@18.3.1)':
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.100.11
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 查询缓存持久化 — 解决「每次打开应用都全量重新拉数据、等很久」。
|
||||
*
|
||||
* 仅白名单内的慢变查询族落地 localStorage: 重开应用时先渲染上一次的数据
|
||||
* (SWR: 挂载后仍按各自 staleTime 后台刷新), 秒开有内容。快变行情类
|
||||
* (watchlist-enriched 实时价 / 分钟K / 告警 / overview-market 等) 不持久化,
|
||||
* 避免陈旧价格误导。白名单外的查询仍是纯内存缓存, 行为与原先完全一致。
|
||||
*
|
||||
* 键首段精确匹配 (不是前缀匹配): 'watchlist' 不会误命中 'watchlist-enriched'。
|
||||
*/
|
||||
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
|
||||
|
||||
/** 持久化结构版本: 序列化结构变化时递增, 旧缓存整体作废 */
|
||||
export const PERSIST_BUSTER = 'v1'
|
||||
|
||||
/** 允许持久化的查询键首段 (对齐 lib/queryKeys.ts 各工厂首段与个别内联键) */
|
||||
const PERSISTABLE_KEY_ROOTS = new Set([
|
||||
'settings', // 应用设置 (OnboardingGuard 依赖; 恢复后打开不再整屏 logo)
|
||||
'preferences', // 用户偏好
|
||||
'capability-matrix', // 能力路由矩阵 (慢变)
|
||||
'watchlist', // 自选清单 (不含 enriched/quotes 等行情族)
|
||||
'watchlist-groups',
|
||||
'screener-strategies', // 策略池清单
|
||||
'regime-history', // 以下 regime 族一天最多变一次
|
||||
'regime-states',
|
||||
'regime-coverage',
|
||||
'regime-phases',
|
||||
'regime-mainline',
|
||||
'custom-signals', // 自定义信号库
|
||||
'custom-signals-options',
|
||||
])
|
||||
|
||||
function isPersistableQueryKey(key: readonly unknown[]): boolean {
|
||||
return typeof key[0] === 'string' && PERSISTABLE_KEY_ROOTS.has(key[0])
|
||||
}
|
||||
|
||||
/** dehydrate 过滤: 只落地白名单族的成功查询 (默认规则已排除 pending/error) */
|
||||
export function shouldPersistQuery(query: { queryKey: readonly unknown[]; state: { status: string } }): boolean {
|
||||
return query.state.status === 'success' && isPersistableQueryKey(query.queryKey)
|
||||
}
|
||||
|
||||
/** localStorage 同步 persister (throttle 2s 合并高频写) */
|
||||
export function createAppPersister() {
|
||||
return createSyncStoragePersister({
|
||||
storage: window.localStorage,
|
||||
key: 'tf-query-cache',
|
||||
throttleTime: 2_000,
|
||||
})
|
||||
}
|
||||
@@ -28,6 +28,8 @@ export function useCapabilityMatrix() {
|
||||
return useQuery({
|
||||
queryKey: QK.capabilityMatrix,
|
||||
queryFn: api.capabilityMatrix,
|
||||
// 矩阵仅在路由偏好/插件装卸时变化 (设置页会失效): 5 分钟新鲜度减少切页重拉
|
||||
staleTime: 5 * 60_000,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,6 +38,7 @@ export function useSettings() {
|
||||
return useQuery({
|
||||
queryKey: QK.settings,
|
||||
queryFn: api.settings,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,6 +47,7 @@ export function usePreferences() {
|
||||
return useQuery({
|
||||
queryKey: QK.preferences,
|
||||
queryFn: api.preferences,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+14
-3
@@ -1,8 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { RouterProvider } from 'react-router-dom'
|
||||
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query'
|
||||
import { QueryClient, QueryCache } from '@tanstack/react-query'
|
||||
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
|
||||
import { initializeFrontendExtensions } from './extensions/bootstrap'
|
||||
import { createAppPersister, shouldPersistQuery, PERSIST_BUSTER } from './lib/queryPersist'
|
||||
// 字体自托管 (@fontsource): 替代 rsms.me / Google Fonts 渲染阻塞外链,
|
||||
// 内网/离线部署不再白屏等字体。权重覆盖 tailwind 全部用量 (300-900)。
|
||||
import '@fontsource/inter/300.css'
|
||||
@@ -59,9 +61,18 @@ async function bootstrap() {
|
||||
const { router } = await import('./router')
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PersistQueryClientProvider
|
||||
client={queryClient}
|
||||
persistOptions={{
|
||||
persister: createAppPersister(),
|
||||
buster: PERSIST_BUSTER,
|
||||
// 恢复超过 1 天的缓存直接丢弃 (慢变族一天内必然后台刷新过)
|
||||
maxAge: 24 * 60 * 60 * 1000,
|
||||
dehydrateOptions: { shouldDehydrateQuery: shouldPersistQuery },
|
||||
}}
|
||||
>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</PersistQueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -644,6 +644,7 @@ function DeviationView({ onPreview }: {
|
||||
queryKey: QK.watchlist,
|
||||
queryFn: api.watchlistList,
|
||||
enabled: watchlistOnly,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const toggleEnabled = (v: boolean) => {
|
||||
|
||||
@@ -156,6 +156,7 @@ export function Screener() {
|
||||
const strategies = useQuery({
|
||||
queryKey: [...QK.screenerStrategies('all'), 'all'],
|
||||
queryFn: () => api.screenerStrategies(undefined, 'all'),
|
||||
staleTime: 60_000, // 策略池清单慢变 (与 Paper 同款), 减少切页重拉
|
||||
})
|
||||
|
||||
// 激活策略自身的执行周期 (决定走缓存还是分钟实时跑)。
|
||||
@@ -646,6 +647,7 @@ export function Screener() {
|
||||
const watchlist = useQuery({
|
||||
queryKey: QK.watchlist,
|
||||
queryFn: api.watchlistList,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
const watchlistSet = useMemo(() => {
|
||||
const symbols = watchlist.data?.symbols ?? []
|
||||
|
||||
@@ -38,8 +38,8 @@ export function Signals() {
|
||||
|
||||
function SignalsBody({ highlight }: { highlight: string }) {
|
||||
const qc = useQueryClient()
|
||||
const list = useQuery({ queryKey: QK.customSignals, queryFn: api.customSignalsList })
|
||||
const options = useQuery({ queryKey: QK.customSignalsOptions, queryFn: api.customSignalsOptions })
|
||||
const list = useQuery({ queryKey: QK.customSignals, queryFn: api.customSignalsList, staleTime: 60_000 })
|
||||
const options = useQuery({ queryKey: QK.customSignalsOptions, queryFn: api.customSignalsOptions, staleTime: 60_000 })
|
||||
|
||||
const [activeSection, setActiveSection] = useState<SignalSection>('custom')
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
|
||||
@@ -785,6 +785,7 @@ export function Watchlist() {
|
||||
const list = useQuery({
|
||||
queryKey: QK.watchlist,
|
||||
queryFn: api.watchlistList,
|
||||
staleTime: 30_000, // 清单慢变 (增删后 mutation 会失效), 减少切页重拉
|
||||
})
|
||||
|
||||
const groupList = useQuery({
|
||||
|
||||
Reference in New Issue
Block a user