记一次密码过期问题的排查与自动化解决方案

UP主~瑞雪 安全 阅读 1,591
赞 21 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

说实话,这事儿我一开始真没当回事。用户登录后提示密码过期,点个按钮改一下嘛,能有多大事?但上线没两周,客服就炸了,一堆人投诉“改密码页面打不开”“卡死了”“转圈圈转了半分钟”。我去自己试了下,好家伙,从点击“修改密码”到页面加载出来,足足等了5秒多,期间整个界面卡住,输入框都点不动。这哪是用户体验,这是考验用户耐心。

记一次密码过期问题的排查与自动化解决方案

最离谱的是,我们系统里密码策略是90天过期,意味着每三个月就有一次这种大规模卡顿。运维说高峰期接口响应时间直接飙到4秒以上,监控显示CPU和内存都在正常范围,那问题出在哪?我第一反应:前端是不是哪里写得太重了?

找到瘼颈了!

我打开 Chrome DevTools,切到 Performance 面板,完整录了一段用户点击“修改密码”后的流程。分析下来发现,页面初始化阶段有三个明显问题:

  • 页面刚加载时,一口气发了7个请求,其中4个是重复校验当前密码状态的
  • 密码强度校验组件在 mount 时就加载了完整的规则引擎,而这个引擎居然还同步引入了一个中文语料库(后来才知道是某 npm 包自带的)
  • 最大的 CPU 占用来自一个叫 checkPasswordExpiry 的函数,它竟然在每次 render 时都重新计算所有日期逻辑,而且用了 moment.js 的深拷贝操作

尤其是最后一个,占了整整 1.8 秒的主线程阻塞时间。我翻代码一看,好家伙,每次调用都是:

// 优化前
function checkPasswordExpiry(lastChangeTime) {
  const now = moment();
  const last = moment(lastChangeTime);
  const diff = now.diff(last, 'days');
  return diff >= 90;
}

看着没问题对吧?但问题是这个函数被绑在了多个 useEffect、form validator、UI 状态判断里,每个地方都独立调用一次,完全没有缓存。更坑的是,moment() 在每次执行时都会创建新对象,高频调用下 GC 直接拉满。

我还发现,前端在进入页面时,不是先检查是否真的需要展示“密码即将过期”提示,而是不管三七二十一先把所有相关模块全 load 进来。有个同事为了“避免闪屏”,甚至用了 Suspense + lazy 同步等待,结果就是首屏渲染被硬生生拖慢。

动刀:这几个地方必须改

我定了三个优化方向:

  1. 减少重复计算,关键函数加缓存
  2. 延迟加载非必要模块
  3. 换掉 moment.js 这种重型依赖

先解决最痛的日期计算问题。我把 checkPasswordExpiry 改成了带记忆化的版本,并且把基础时间抽出来作为常量,避免频繁实例化:

// 优化后
const PASSWORD_EXPIRY_DAYS = 90;
const MS_PER_DAY = 1000 * 60 * 60 * 24;

// 简单记忆化,按输入字符串 key 缓存
const memoizedCheck = (() => {
  const cache = new Map();
  return (lastChangeTime) => {
    if (!lastChangeTime) return false;
    if (cache.has(lastChangeTime)) {
      return cache.get(lastChangeTime);
    }
    
    const last = new Date(lastChangeTime).getTime();
    const now = Date.now();
    const diffDays = Math.floor((now - last) / MS_PER_DAY);
    const isExpired = diffDays >= PASSWORD_EXPIRY_DAYS;
    
    cache.set(lastChangeTime, isExpired);
    return isExpired;
  };
})();

这里注意我踩过好几次坑:一开始用的是 WeakMap,想着自动回收,结果发现传进来的时间字符串每次都是新生成的,根本没法命中缓存。后来干脆用 Map,定期清空(比如用户登出时),简单粗暴但有效。

接着是模块拆分。原来整个“密码管理”功能被打包在一个 chunk 里,现在我把它拆成两个动态导入:

// 原来
import { PasswordExpiryBanner } from './components/PasswordExpiry';

// 现在
const PasswordExpiryBanner = React.lazy(() =>
  import('./components/PasswordExpiry').then(module => ({
    default: module.PasswordExpiryBanner
  }))
);

// 使用时配合 Suspense
<Suspense fallback={null}>
  <PasswordExpiryBanner />
</Suspense>

关键是 fallback 设成 null,而不是 loading 图标。因为这个组件只在特定条件下显示,没必要提前占位。实测首屏 JS 体积减少了 42KB,gzip 后约 15KB。

最后是干掉 moment.js。我们项目里其实只用了日期差计算和格式化,完全可以用原生 Date API 加上 dayjs 替代。dayjs 体积只有 2KB,API 几乎兼容:

// 安装
// npm install dayjs

// 替换
import dayjs from 'dayjs';
const diff = dayjs().diff(dayjs(lastChangeTime), 'day');

替换过程中唯一麻烦的是国际化,但我们系统只支持中文,直接引入 locale 就行:

import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');

优化后:流畅多了

改完之后我本地压测了一下,又让测试同学跑了几十遍真实场景。结果挺惊喜的:

  • 页面首次渲染时间从平均 5.2s 降到 800ms 左右
  • CPU 主线程占用峰值从 95%+ 降到 30% 以下
  • 内存泄漏问题消失,连续操作十次不再出现卡顿累积

线上灰度发布后,APM 监控数据显示,该页面的 FCP(First Contentful Paint)中位数下降了 76%,TBT(Total Blocking Time)从 680ms 降到 90ms。最重要的是,客服那边再也没收到相关投诉了。

不过也有个小遗憾:lazy load 的组件在弱网环境下会有轻微延迟显示,但我们评估后认为可以接受——毕竟比整个页面卡住强得多。后续打算加上一个轻量级的骨架屏,不增加体积的前提下改善感知体验。

性能数据对比

以下是优化前后关键指标对比(基于 100 次采样均值):

指标 优化前 优化后 提升幅度
首屏渲染时间 5200ms 800ms 84.6%
JS 执行时间 3100ms 450ms 85.5%
关键请求次数 7 3 57.1%
打包体积增量 +180KB +35KB 80.6%

其他细节顺手修了

顺带处理了一些边角问题:

  • 把密码过期检查接口从轮询改成登录时一次性获取,通过 JWT payload 携带过期时间戳
  • 前端存储这个时间戳到 sessionStorage,避免重复请求
  • 增加节流逻辑,同一个会话内最多每天检查一次本地状态

接口调用示例如下:

// 登录后获取用户信息
fetch('https://jztheme.com/api/user/profile')
  .then(res => res.json())
  .then(data => {
    const { lastPasswordChange } = data;
    sessionStorage.setItem('passwordLastChanged', lastPasswordChange);
  });

// 页面中使用缓存值
function shouldShowExpiryWarning() {
  const lastChanged = sessionStorage.getItem('passwordLastChanged');
  if (!lastChanged) return false;
  return memoizedCheck(lastChanged);
}

虽然看起来小题大做,但这类细节积少成多,对整体性能影响不小。

总结

这次优化折腾了半天才发现,问题根本不在于“密码过期”这个功能本身复杂,而是前期开发时图省事,把一堆本该懒加载、缓存、轻量化的逻辑全堆在主线程里跑。尤其是 moment.js 这种“老牌”库,在现代前端环境下真的要慎用。

核心经验就三点:

  1. 高频调用的纯函数一定要加缓存
  2. 非首屏内容果断 lazy load,fallback 能省则省
  3. 能用原生或轻量库的地方,别引入重型依赖

这个方案不是最优的,比如还可以进一步做服务端预判、CDN 缓存策略等,但对我们当前项目来说,改动最小、见效最快。

以上是我踩坑后的总结,希望对你有帮助。有更好的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论