frames

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Frames

帧

This skill measures whether an interaction stays smooth. It records the interaction on a throttled CPU, traces each slow frame to its cause and proves the fix with a second recording.
该 skill 用于衡量一次交互是否保持流畅。它在 CPU 节流环境下记录交互,将每个慢帧追溯到具体原因,并通过第二次录制证明修复效果。

A trace, never a feeling

必须是追踪,不能凭感觉

The budgets are exact. Every frame lands within one refresh interval, 16.7ms at 60Hz, and no animation frame runs past 50ms while something moves. Every interaction paints its response within 200ms. "Feels smooth" with no numbers is a finding.
What a motion's values should be belongs to
motion
. Taking a motion apart frame by frame belongs to
deconstruct
. Opening the running result belongs to
verify
.
这些预算要求是精确的。每个帧都应在一个刷新周期内完成(60Hz 下为 16.7ms),且当有内容移动时,任何动画帧的运行时长都不得超过 50ms。每次交互都必须在 200ms 内完成响应绘制。没有数据支撑的“感觉流畅”本身就是一项问题。
某个 motion 的数值应是多少属于
motion
。逐帧拆解某个 motion 属于
deconstruct
。打开运行结果属于
verify
。

1. Name the interaction

1. 明确交互

Write the route, the steps and the moment that matters, such as "open the command palette and type three letters". Throttle the CPU 4× through the browser's emulation call, which stands in for a mid-range phone. A trace on an unthrottled laptop is
frames-throttle-the-cpu
.
写出 route、操作步骤以及关键时刻,例如“打开 command palette 并输入三个字母”。通过浏览器的 emulation 调用将 CPU 节流至 4×,以模拟中端手机。在未节流的笔记本电脑上得到的追踪结果属于
frames-throttle-the-cpu
。

2. Record it

2. 记录交互

Run observe.js through the browser's evaluate call. Call
window.__frames.start()
, perform the interaction three times, then call
window.__frames.stop()
. When a long frame has no named script, record a full performance profile too. Reporting smoothness without a recording is
frames-trace-not-feel
.
通过浏览器的 evaluate 调用运行 observe.js。调用
window.__frames.start()
,将交互执行三次,然后调用
window.__frames.stop()
。如果某个长帧无法归因到具体脚本,还要录制完整的 performance profile。在没有录制记录的情况下报告流畅度,属于
frames-trace-not-feel
。

3. Read the budget

3. 检查预算

Compare the recording against the budgets. Dropped frames or a long animation frame while something moves is
frames-hold-the-budget
. An interaction whose
duration
passes 200ms is
frames-answer-in-200ms
. Name whether the time went to input delay, processing or presentation.
将录制结果与预算进行比较。当有内容移动时出现掉帧或长动画帧,属于
frames-hold-the-budget
。交互的
duration
超过 200ms,属于
frames-answer-in-200ms
。说明耗时发生在输入延迟、处理还是呈现阶段。

4. Find the cause

4. 找出原因

Read each long frame's scripts and their forced layout time, then the code they point to.
A layout property animated across a large subtree reflows every frame.
top
or
width
animated on a full-page panel is
frames-compositor-properties
. Move it to
transform
and
opacity
.
Reading layout after writing a style forces the browser to lay out mid-frame.
offsetHeight
read inside a loop that sets
style.height
is
frames-no-forced-layout
. Read every value first, then write.
A state update on every frame re-renders the component tree each frame.
setState
inside a
requestAnimationFrame
loop or a scroll handler is
frames-animate-outside-render
. Drive the value through a motion value, a CSS variable or a ref.
A large surface repainted each frame costs more than the motion is worth.
backdrop-filter
on a full-screen layer that moves is
frames-small-paint
. Shrink the painted area or fade a static copy.
查看每个长帧的脚本及其强制布局时间,再查看这些脚本指向的代码。
在大型子树中动画一个布局属性会导致每帧都发生重排。在全页面面板上动画
top
或
width
,属于
frames-compositor-properties
。改用
transform
和
opacity
来实现动画。
写入样式后读取布局值,会迫使浏览器在帧中途执行布局。在设置
style.height
的循环内读取
offsetHeight
,属于
frames-no-forced-layout
。先读取所有值,再统一写入。
每一帧都进行状态更新,会导致组件树每帧重新渲染。在
requestAnimationFrame
循环或 scroll 事件处理器中调用
setState
,属于
frames-animate-outside-render
。通过 motion value、CSS 变量或 ref 驱动该值。
每帧重绘大面积区域所付出的成本高于 motion 本身带来的价值。在移动中的全屏图层上使用
backdrop-filter
,属于
frames-small-paint
。缩小绘制区域,或对静态副本做淡化处理。

5. Trace the fix

5. 追踪修复效果

Record again with the same steps and the same throttle. Report both recordings side by side. "Should be faster now" is
frames-before-and-after
. Clear the CPU throttle when done.
使用相同的步骤和相同的节流设置再次录制。并排报告两次录制结果。“现在应该更快了”属于
frames-before-and-after
。完成后清除 CPU 节流。

The pair

成对呈现

Every check is a pair. The broken line sits beside the line that replaces it.
RuleBrokenReplacement
frames-throttle-the-cpu
Traced on an unthrottled laptopTraced at 4× CPU throttle
frames-trace-not-feel
"The drawer feels smooth""Drawer open: 0 dropped frames, worst gap 17ms at 4×"
frames-hold-the-budget
14 dropped frames while the sheet opens0 dropped, no animation frame over 50ms
frames-answer-in-200ms
A keystroke painting after 340ms, 280ms of it processing90ms, with the filter moved off the keystroke
frames-compositor-properties
animate={{ width: 320 }}
on the full-page sidebar
animate={{ x: 0 }}
with the width fixed
frames-no-forced-layout
el.style.height = el.scrollHeight + "px"
in a loop over rows
All heights read first, then all written
frames-animate-outside-render
setProgress(p)
on every frame
progress.set(p)
on a motion value
frames-small-paint
backdrop-filter: blur(20px)
on a moving full-screen layer
Blur on the static backdrop, the panel moving above it
frames-before-and-after
"Should be faster now"Before 14 dropped, after 0, same steps at 4×
每次检查都由一对结果组成。有问题的方案与替代方案并排展示。
规则有问题的方案替代方案
frames-throttle-the-cpu
在未节流的笔记本电脑上追踪在 4× CPU 节流下追踪
frames-trace-not-feel
“drawer 感觉流畅”“drawer 打开:4× 下掉帧 0 个,最差间隔 17ms”
frames-hold-the-budget
sheet 打开时掉帧 14 个掉帧 0 个,无动画帧超过 50ms
frames-answer-in-200ms
按键操作在 340ms 后才绘制,其中 280ms 用于处理90ms,将 filter 移出按键处理关键路径
frames-compositor-properties
在全页面 sidebar 上使用
animate={{ width: 320 }}
固定宽度并使用
animate={{ x: 0 }}
frames-no-forced-layout
在遍历各行的循环中使用
el.style.height = el.scrollHeight + "px"
先读取所有高度,再统一写入
frames-animate-outside-render
每帧调用
setProgress(p)
在 motion value 上调用
progress.set(p)
frames-small-paint
在移动的全屏图层上使用
backdrop-filter: blur(20px)
在静态背景上应用 blur,并在其上方移动 panel
frames-before-and-after
“现在应该更快了”修复前掉帧 14 个,修复后 0 个,4× 下执行相同步骤

Reporting

报告

Severity.
HIGH
is an interaction over 200ms at the throttle, or a report of smoothness with no recording.
MEDIUM
is dropped frames during motion, a layout property animated on a large subtree, forced layout or a state update per frame. It is also a trace taken without a throttle.
LOW
is a large repaint that still holds the budget, or a fix reported without a second recording.
Format. One finding per root cause, with the measured numbers:
1. file:line - [rule-id] what is wrong (dropped, worst gap, duration)
Then a
Rule | Count | Severity
summary.
Block
when any
HIGH
remains,
Approve
otherwise. Mark a browser without long animation frame support as
Not verified
for script attribution.
严重程度。
HIGH
表示在节流条件下交互超过 200ms,或在没有录制记录的情况下仅报告“感觉流畅”。
MEDIUM
表示运动期间掉帧、在大型子树上动画布局属性、发生强制布局,或每帧进行状态更新;未加节流就进行追踪也属于
MEDIUM
。
LOW
表示大面积重绘但仍满足预算,或仅报告修复却没有第二次录制。
格式。 每个根本原因对应一条发现,并附上实测数据:
1. file:line - [rule-id] what is wrong (dropped, worst gap, duration)
然后提供
Rule | Count | Severity
汇总。存在任何
HIGH
时标记为
Block
,否则标记为
Approve
。如果浏览器不支持 long animation frame,将脚本归因状态标记为
Not verified
。