Git Reflog 使用指南:找回丢失的提交与分支恢复技巧

令狐思晨 工具 阅读 2,087
赞 40 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上周上线一个新功能,用户反馈“点一下要等好几秒才反应”,我一开始以为是后端接口慢,结果打开 DevTools 一看,Network 里 API 响应才 200ms,但整个页面从点击到渲染完成花了快 5 秒。这哪能忍?尤其在低端机上,直接卡成 PPT。

Git Reflog 使用指南:找回丢失的提交与分支恢复技巧

这个功能的核心是展示 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 把过滤后的数组缓存住,依赖项只放 logsfilter 对象。但注意!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 做类似功能?性能会不会天生就好很多?

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论
诸葛成立
这篇文章让我感受到了技术分享的价值,以后我也想尝试分享自己的经验。
点赞
2026-02-28 13:25