点一下就卡”的前端性能排查:为什么右侧卡片越多,侧边栏展开越卡?

5 阅读3分钟

标题

一次“点一下就卡”的前端性能排查:为什么右侧卡片越多,侧边栏展开越卡?(CSS 动画触发 Reflow)


背景与现象

在一个 Next.js + React 的页面里,左侧有一个侧边栏(展开/收起按钮),右侧是工具卡片列表。

  • 当右侧只有少量卡片时:点击展开/收起非常流畅
  • 当右侧有大量卡片(上千)时:点击展开/收起出现明显卡顿(INP 上升、主线程被占用)

表面看起来是“按钮点击导致卡顿”,但真正原因往往不在 React 逻辑,而在 CSS 布局动画。


先讲结论(最重要的一句话)

对 width/height/left/top 这类“布局属性”做 transition,会在动画每一帧触发布局计算(reflow/layout)。右侧 DOM 越多,每帧越贵,于是交互就卡。


为什么右侧内容越多越卡?

浏览器渲染大致分几步:

  1. 计算样式(Recalculate Style)
  1. 计算布局(Layout / Reflow)
  1. 绘制(Paint)
  1. 合成(Composite)

当你做这样的 CSS 动画:

.sidebar {
  width76px;
  transition: width 0.3s ease;

}
.sidebar.open {
  width236px;
}

动画的 0.3s 不是“一次变化”,而是几十帧连续变化(接近 60fps 时约 18 帧)。每一帧 width 都在变 → Layout 每一帧都要重算。右侧卡片越多,意味着:

  • DOM 节点更多
  • 布局树更复杂
  • 可能还有更多文字、图片、阴影等绘制成本

所以单次 Layout/Paint 的成本更高;当它被重复几十次,就会把主线程占满,导致点击响应变慢(INP 变差)。


如何快速确认“是不是布局动画导致的卡顿”?

用 Chrome DevTools:

1) Performance 录制

  • 打开 DevTools → Performance
  • 点击 Record
  • 点击侧边栏展开/收起按钮
  • 停止 Record

如果你看到时间轴中出现明显、连续的 Rendering(尤其 Layout)条段,并贯穿动画持续时间,那基本就坐实了。

2) 直接排查 CSS

搜索组件样式里是否存在:

  • transition: width ... / transition: height ...
  • 或 transition: all ...(最危险,可能把布局属性也动画化)
  • left/top/margin/padding 等属性动画

解决原则:把“布局动画”换成“合成动画”

浏览器对 transform / opacity 的优化最好,很多时候能走合成层,不触发布局重排。

推荐做法

  • 宽度切换:可以变,但不要做 transition(瞬间切换)
  • 视觉动效交给:
  • transform: translate/scale/rotate
  • opacity

示例(侧边栏宽度瞬切 + 内容淡入/位移):


/* 不要对 width 做 transition */

.sidebar {
  width4.75rem;
  transition: none;
}

.sidebar.open {
  width14.75rem;
}

/* 动效交给 transform/opacity(不会触发布局重排) */

.titleText {
  opacity0;
  transformtranslateX(-0.5rem);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.sidebar.open .titleText {
  opacity1;
  transformtranslateX(0);
}

/* 箭头旋转同理 */

.toggleIcon {
  transition: transform 0.2s ease;
}

.sidebar.open .toggleIcon {
  transformrotate(180deg);
}

额外优化:尊重“减少动态效果”

对偏好减少动画的用户,也能避免一些设备掉帧:


@media (prefers-reduced-motion: reduce) {
  .sidebar,
  .titleText,
  .toggleIcon {
    transition: none !important;
    animation: none !important;
  }
}


你可能会踩的坑

  • 误以为是 React 重新渲染导致卡:实际上 React 渲染可能很少,真正耗时在 Layout/Paint
  • transition: all:很容易把布局属性也带进动画,导致“看似小改动,性能大翻车”
  • 右侧少量卡片很流畅:只是没到临界点,并不代表实现没问题

验收标准(怎么判断修复成功)

修复后重新录制 Performance,你应该看到:

  • 点击展开/收起时,Layout 不再连续爆发
  • 主线程空闲时间增加
  • INP 更稳定
  • 即使右侧卡片很多,点击也更“跟手”

总结

这类问题的本质是:一次点击触发了“持续的布局动画”,页面越复杂,动画每帧越贵。解决思路不是“让 React 更快”,而是把动画从 layout 迁移到 compositor-friendly(transform/opacity)。如果你准备把这篇文章发到掘金/公众号,我建议再补两张图:

  • Performance 录制的“修复前 vs 修复后”对比(Layout 条段变化非常直观)
  • 对应 CSS diff(从 transition: width → transition: none + transform/opacity)