perf(webui): 动画卡顿三处治理 + 覆盖列表/日历

用户:「动画卡顿严重,且绝大部分场景还是没有流畅的动画」。

① 提层:`html.view-switch .rise-in` 里加 will-change: transform, opacity。
   这一档只动 opacity+transform,但没提层时浏览器不保证合成器接管。
   写在 view-switch 窗口里(只在切换的 ~400ms 内有效),不是写在元素上 ——
   常驻 will-change 会把每个面板都变成常驻图层,白吃内存。

② 去掉触发动画时那次强制同步重排:App.tsx 原来用 `void root.offsetWidth`
   让 remove/add 分属两帧,代价是每次切视图都逼浏览器把整个文档布局算一遍,
   而这笔账正好落在动画第一帧上。改成两次 rAF,同样分两帧,不付重排的钱。

③ 覆盖:列表栏与日历根节点也穿 rise-in ⇒ 切 通信/日历/工作列表 都有入场,
   仍只动"新出现的那一块",骨架不动(避免 09-14 那次"整屏闪"的老问题)。

判据(narrow-layout +3):will-change 必须在 view-switch 规则里;
   App.tsx 不得再有 `void root.offsetWidth`(且必须用 requestAnimationFrame);
   MailList/CalendarView 必须穿 rise-in。变异:抽掉 will-change → 红;
   把强制重排放回去 → 红;复原 → 77 通过 0 失败。

★ 顺带纠错:上一轮我报"整页 146 个元素带 backdrop-filter / 动画子树 76 个全带模糊"
   是我探针自己的 bug(`webkitBackdropFilter` 取到 undefined,`undefined !== 'none'` 恒真,
   连 <path>/<META> 都被算进去)。修正后实测:真模糊 0 个、同时动画 1 个、
   帧间隔中位/最长 17ms(60fps)。所以"卡顿"不是我能在本机复现的形态 ——
   需要知道你看的是哪一端/什么状态(见回信)。
This commit is contained in:
2026-09-15 07:57:04 +08:00
parent 6ee58fa28a
commit e3b7f8f421
5 changed files with 45 additions and 6 deletions

View File

@ -269,6 +269,22 @@ check(
/rise-in/.test(composeSrc) && (mailViewSrc.match(/rise-in/g) || []).length >= 2,
'三处目标面没都穿上 rise-in(回复框与转发面板在 MailView 里,必须各有一处)'
);
check(
'动画走合成器(view-switch 窗口内提层)',
/html\.view-switch \.rise-in\s*\{[\s\S]*?will-change:\s*transform,\s*opacity/.test(css),
'缺少 will-change —— 没提层时 opacity/transform 仍可能在主线程上抖'
);
const appMotionSrc = code('src/App.tsx');
check(
'切视图不再用强制重排触发动画',
!/void\s+root\.offsetWidth/.test(appMotionSrc) && /requestAnimationFrame/.test(appMotionSrc),
'触发动画还在读 offsetWidth(同步强制布局)—— 这笔记账正好落在动画第一帧上,就是卡顿的来源之一'
);
check(
'主要视图也穿 rise-in(列表/日历)',
/rise-in/.test(code('src/components/MailList.tsx')) && /rise-in/.test(code('src/components/CalendarView.tsx')),
'切到列表/日历时没有入场动画 —— "绝大部分场景还是没有流畅的动画"就是这一条'
);
check(
'rise-in 也受 prefers-reduced-motion 管',
reducedBlocks.some(b => /view-switch \.rise-in/.test(b)),