Git Reflog 使用指南:找回丢失的提交与分支恢复技巧
优化前:卡得不行
上周上线一个新功能,用户反馈“点一下要等好几秒才反应”,我一开始以为是后端接口慢,结果打开 DevTools 一看,Network 里 API 响应才 200ms,但整个页面从点击到渲染完成花了快 5 秒。这哪能忍?尤其在低端机上,直接卡成 PPT。
这个功能的核心是展示 Git 的 reflog 记录——就是 git reflog 那一坨东西。数据量其实不大,也就几百条,但每条都带 commit hash、时间戳、操作类型、引用名,还要支持搜索和按分支筛选。问题就出在这:每次筛选或搜索,整个列表全量重绘,React 组件没做任何 memo,状态更新一触发,几百个子组件全 rerender,FPS 直接掉到个位数。
找到瓶颈了!
先用 Chrome Performance 面板录了一次交互,发现 scripting 时间爆高,主要耗在 React 的 reconciler 上。再切到 React DevTools 的 Profiler,果然看到 ReflogList 和它的子项 ReflogItem 每次都在重新 mount/unmount。
翻了下代码,罪魁祸首是这个:
function ReflogList({ logs, filter }) {
const filteredLogs = logs.filter(log =>
log.ref.includes(filter.branch) &&
log.message.includes(filter.keyword)
);
return (
<div>
{filteredLogs.map(log => (
<ReflogItem key={log.id} log={log} />
))}
</div>
);
}
看起来没啥问题?但注意:filteredLogs 是每次 render 时重新计算的数组,哪怕内容一样,引用也变了。React 看到 props 引用变化,就认为子组件需要更新。再加上 ReflogItem 本身没用 React.memo,每次父组件更新,它就跟着跑一遍 render 生命周期。
更坑的是,logs 数据是从 Redux store 里拿的,而筛选条件一变,dispatch 一个 action 更新 filter state,store 变了,整个组件树 re-render。典型的“状态抖动”+“无 memo 保护”组合拳。
核心优化:三板斧搞定
折腾了半天,最后靠三个改动把性能拉回来了,亲测有效。
第一板斧:缓存过滤结果
用 useMemo 把过滤后的数组缓存住,依赖项只放 logs 和 filter 对象。但注意!filter 如果是每次重新创建的对象(比如从 URL query 解析来的),那依赖项永远在变。所以得确保 filter 是 stable 的——要么用 useCallback 包装更新函数,要么用 useReducer 管理状态。
// 优化后
function ReflogList({ logs, filter }) {
const filteredLogs = useMemo(() => {
return logs.filter(log =>
log.ref.includes(filter.branch) &&
log.message.includes(filter.keyword)
);
}, [logs, filter]); // 确保 filter 是同一个引用
return (
<div>
{filteredLogs.map(log => (
<ReflogItem key={log.id} log={log} />
))}
</div>
);
}
第二板斧:给子组件加 memo
给 ReflogItem 套上 React.memo,并且自定义比较函数,只当 log.id 或关键字段变化时才更新。因为 reflog 条目一旦生成就不会变,所以 id 不变就不用重绘。
const ReflogItem = React.memo(({ log }) => {
return (
<div className="reflog-item">
<span>{log.hash}</span>
<span>{log.timestamp}</span>
<span>{log.operation}</span>
</div>
);
}, (prevProps, nextProps) => {
return prevProps.log.id === nextProps.log.id;
});
第三板斧:虚拟滚动(可选但强力)
虽然几百条不算多,但在移动端还是有点压力。后来加了个轻量级虚拟滚动库 react-window,只渲染可视区域内的 10-15 条,内存和 CPU 占用直接降了一半。代码改动也不大:
import { FixedSizeList as List } from 'react-window';
function ReflogList({ logs, filter }) {
const filteredLogs = useMemo(() => {
return logs.filter(log =>
log.ref.includes(filter.branch) &&
log.message.includes(filter.keyword)
);
}, [logs, filter]);
const Row = ({ index, style }) => (
<div style={style}>
<ReflogItem log={filteredLogs[index]} />
</div>
);
return (
<List
height={600}
itemCount={filteredLogs.length}
itemSize={60}
width="100%"
>
{Row}
</List>
);
}
这里注意我踩过好几次坑:别忘了给 List 的容器设固定高度,否则滚动失效;另外 itemSize 要和实际 DOM 高度一致,不然会出现空白或重叠。
性能数据对比
本地测试环境(MacBook Pro + Chrome),模拟低端机(4x CPU slowdown):
- 优化前:筛选操作平均耗时 4.8s,FPS 最低 6
- 仅加 useMemo + React.memo:降到 1.2s,FPS 稳定在 30+
- 再加虚拟滚动:进一步压到 800ms,FPS 50+,滚动丝滑
线上真实用户数据(通过 Performance API 采样)也验证了效果:95 分位的交互延迟从 4200ms 降到 780ms。虽然还有提升空间(比如 Web Worker 做过滤),但对当前需求来说,已经够用了。
一点不完美的细节
改完后其实还留了个小问题:如果用户快速连续输入搜索关键词,可能会触发多次过滤计算。理想情况应该加防抖,但考虑到 reflog 数据量不大,而且用户一般不会疯狂打字,我就没加——毕竟“过早优化是万恶之源”。如果后续数据量涨到上千条,再补上 useDeferredValue 或 Web Worker 也不迟。
另外,虚拟滚动在动态高度场景下会麻烦些,但我们的 reflog 条目高度固定,所以省事了。要是你的列表项高度不一,建议用 react-virtualized-auto-sizer 配合 VariableSizeList。
以上是我这次 reflog 列表性能优化的完整过程,核心就是三点:缓存计算结果、避免无效重绘、按需渲染。有更优的实现方式欢迎评论区交流,比如有没有人试过用 SolidJS 或 Svelte 做类似功能?性能会不会天生就好很多?
