Skip to content

Commit 3d3a7f7

Browse files
committed
perf(transition): replace spring with CSS transition for page entrance and preload all chunks
- Replace Framer Motion spring animations in FunctionalOverlay with CSS transitions (opacity 300ms ease-out, transform 300ms ease-out). CSS transitions run on the GPU compositor thread with zero JS overhead, eliminating spring's per-frame differential equation computation. - Defer backdrop-blur-2xl by 300ms (setTimeout in useEffect) to prevent the GPU compositing layer creation from competing with the entrance transition animation. Blur appears smoothly after the panel settles. - Preload ALL 19 page chunks immediately at idle time (requestIdleCallback) instead of staggered 7-page prefetch with 2s delay. Eliminates chunk loading wait during page transitions. Framer Motion spring is retained for gesture interactions (drag, layoutId) where its physics model is genuinely needed.
1 parent 2a5cf31 commit 3d3a7f7

2 files changed

Lines changed: 55 additions & 35 deletions

File tree

‎client/src/components/overlay/FunctionalOverlay.tsx‎

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
* 透明度与交互性(而非卸载),使页面状态跨 Esc/重入周期保留,避免动画重播。
77
* 隐藏时设置 inert 防止焦点落入不可见页面(React 18 不支持 inert prop,直接操作 DOM)。
88
*
9+
* 动画策略:页面切换的 opacity/transform 入场出场使用 CSS transition
10+
* (零 JS 开销,运行在 GPU 合成线程),手势/布局动画保留 Framer Motion。
11+
*
912
* @ai-context: 浮层/弹窗组件:FunctionalOverlay。
1013
*/
11-
import { useEffect, useRef } from 'react';
12-
import { motion } from 'framer-motion';
14+
import { useEffect, useRef, useState } from 'react';
1315
import { cn } from '@/lib/utils';
1416

1517
interface FunctionalOverlayProps {
@@ -25,6 +27,16 @@ interface FunctionalOverlayProps {
2527

2628
export function FunctionalOverlay({ children, visible, className, panelClassName, maskClassName }: FunctionalOverlayProps) {
2729
const rootRef = useRef<HTMLDivElement>(null);
30+
// 延迟 backdrop-blur:动画期间(前 300ms)不加 blur,避免合成层与过渡动画竞争
31+
const [showBlur, setShowBlur] = useState(false);
32+
33+
useEffect(() => {
34+
if (visible) {
35+
const timer = setTimeout(() => setShowBlur(true), 300);
36+
return () => clearTimeout(timer);
37+
}
38+
setShowBlur(false);
39+
}, [visible]);
2840

2941
// 隐藏时设置 inert:阻断指针事件、焦点与辅助技术访问(React 18 无 inert prop)
3042
useEffect(() => {
@@ -37,59 +49,59 @@ export function FunctionalOverlay({ children, visible, className, panelClassName
3749
}
3850
}, [visible]);
3951

40-
// 已移除 aria-hidden:inert 属性已完整覆盖其语义(阻断辅助技术+焦点),
41-
// 同时避免 aria-hidden 同步生效而 inert 异步生效导致的焦点竞态警告
4252
return (
43-
<motion.div
53+
<div
4454
ref={rootRef}
4555
className={cn(
4656
"fixed inset-0 z-10 flex items-center justify-center",
4757
"p-2 sm:p-4 md:p-8",
48-
"pb-16 sm:pb-8 md:pb-8", // 移动端底部预留 BottomNav 空间
58+
"pb-16 sm:pb-8 md:pb-8",
59+
// CSS transition: opacity 300ms ease-out(合成器线程,零 JS 开销)
60+
"transition-opacity duration-300 ease-out",
61+
visible ? "opacity-100" : "opacity-0",
4962
className
5063
)}
51-
initial={{ opacity: 0 }}
52-
animate={{ opacity: visible ? 1 : 0 }}
53-
/* 修复:外层过渡统一为 spring 类型,与内层面板保持一致,避免 ease 与 spring 混用导致动画节奏不协调 */
54-
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
5564
style={{ pointerEvents: visible ? 'auto' : 'none' }}
5665
>
57-
{/* 半透明背景遮罩 — 隐藏时移除 backdrop-blur,避免 Electron 中 backdrop-filter 合成层遮挡下方 canvas */}
66+
{/* 半透明背景遮罩 — 延迟 backdrop-blur 避免动画期间合成层竞争 */}
5867
<div className={cn(
5968
"absolute inset-0 bg-black/20",
60-
visible && "backdrop-blur-sm",
69+
"transition-[backdrop-filter] duration-300 ease-out",
70+
showBlur && visible && "backdrop-blur-sm",
6171
maskClassName,
6272
visible ? "pointer-events-auto" : "pointer-events-none"
6373
)} />
6474

65-
{/* 功能面板 — 隐藏时移除 backdrop-blur,避免 backdrop-filter 合成层遮挡 canvas */}
66-
<motion.div
75+
{/* 功能面板 — CSS transition 处理 scale/y/opacity 入场出场 */}
76+
<div
6777
data-work-area="module-content"
6878
className={cn(
6979
"relative z-10 w-full",
7080
"max-w-5xl",
71-
"max-h-[calc(100vh-5rem)] sm:max-h-[85vh]", // 移动端适配底部导航
81+
"max-h-[calc(100vh-5rem)] sm:max-h-[85vh]",
7282
"overflow-y-auto",
73-
"rounded-2xl sm:rounded-[24px_12px_20px_16px]", // 移动端统一圆角
83+
"rounded-2xl sm:rounded-[24px_12px_20px_16px]",
7484
"bg-transparent",
75-
visible && "backdrop-blur-2xl",
85+
// CSS transition: 面板从 scale(0.9) + y(30px) 缩放入场
86+
"transition-all duration-300 ease-out",
87+
visible
88+
? "opacity-100 scale-100 translate-y-0"
89+
: "opacity-0 scale-90 translate-y-[30px]",
90+
// backdrop-blur 延迟 300ms 后生效,避免与入场动画竞争
91+
showBlur && visible && "backdrop-blur-2xl",
7692
"border border-white/20 dark:border-white/10",
7793
"shadow-[0_8px_40px_rgba(0,0,0,0.3)]",
7894
panelClassName,
7995
visible ? "pointer-events-auto" : "pointer-events-none",
8096
"p-3 sm:p-5 md:p-8"
8197
)}
82-
/* 修复:提示浏览器提前创建合成层,避免动画时触发重绘/重排,提升动画流畅度 */
8398
style={{
8499
willChange: 'transform, opacity',
85100
background: 'linear-gradient(180deg, var(--kb-dive-top) 0%, var(--kb-dive-mid) 42%, var(--kb-dive-bot) 100%)',
86101
}}
87-
initial={{ scale: 0.9, y: 30 }}
88-
animate={visible ? { scale: 1, y: 0 } : { scale: 0.9, y: 30 }}
89-
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
90102
>
91103
{children}
92-
</motion.div>
93-
</motion.div>
104+
</div>
105+
</div>
94106
);
95-
}
107+
}

‎client/src/routes/index.tsx‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,35 @@ import AppLayout from '@/components/layout/AppLayout';
88
import { AuthGuard } from '@/lib/auth/AuthGuard';
99
import { prefetchRoute } from '@/utils/scheduler';
1010

11-
// 在应用启动后空闲期预加载核心模块
12-
// P1-6 prefetch 错峰:7 个高频页面 chunk 每 300ms 拉取一个(而非一次性突发),
13-
// 避免启动 2s 后的网络/磁盘峰值影响首次交互帧
11+
// 应用启动后空闲期预加载所有页面 chunk,消除页面切换时的加载等待
12+
// 使用 requestIdleCallback 错峰,避免影响首屏交互帧
1413
if (typeof window !== 'undefined') {
15-
const HIGH_FREQUENCY_PAGES = [
14+
const ALL_PAGES = [
1615
() => import('@/features/dashboard/pages/DashboardPage'),
1716
() => import('@/features/notes/pages/NoteEditPage'),
1817
() => import('@/features/pomodoro/pages/PomodoroPage'),
19-
// 修复:增加高频页面预加载,减少模块切换时的加载等待与视觉跳变
2018
() => import('@/features/notes/pages/NotesPage'),
2119
() => import('@/features/flashcards/pages/FlashcardsPage'),
2220
() => import('@/features/feynman/pages/FeynmanPage'),
2321
() => import('@/pages/SettingsPage'),
22+
() => import('@/features/dashboard/pages/AnalyticsPage'),
23+
() => import('@/features/inspiration/pages/InspirationPage'),
24+
() => import('@/features/classroom/pages/ClassroomPage'),
25+
() => import('@/features/pomodoro/pages/PomodoroStatsPage'),
26+
() => import('@/features/pomodoro/pages/PomodoroSettingsPage'),
27+
() => import('@/features/notes/pages/NotesGraphPage'),
28+
() => import('@/features/flashcards/pages/DeckDetailPage'),
29+
() => import('@/features/flashcards/pages/StudySessionPage'),
30+
() => import('@/features/flashcards/pages/GenerativeReviewPage'),
31+
() => import('@/features/feynman/pages/FeynmanSessionPage'),
32+
() => import('@/features/feynman/pages/FeynmanGraphPage'),
33+
() => import('@/features/feynman/pages/SocraticSessionPage'),
2434
];
25-
const PREFETCH_STAGGER_MS = 300;
35+
const idle = window.requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 1000));
2636
window.addEventListener('load', () => {
27-
setTimeout(() => {
28-
HIGH_FREQUENCY_PAGES.forEach((loader, i) => {
29-
setTimeout(() => prefetchRoute(loader), i * PREFETCH_STAGGER_MS);
30-
});
31-
}, 2000); // 启动2秒后开始预加载
37+
idle(() => {
38+
ALL_PAGES.forEach((loader) => prefetchRoute(loader));
39+
});
3240
});
3341
}
3442

0 commit comments

Comments
 (0)