Vuex状态管理实战技巧与项目应用经验分享

上官梓涵 框架 阅读 2,929
赞 9 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

项目上线三个月,用户反馈越来越多,最常听到的就是“点不动”“转圈半天”“手机上直接卡死”。我自己拿安卓机试了下,首页加载列表从5秒到8秒不等,滑动过程中 Vuex 状态更新一卡一卡的,甚至有时候点个按钮要等两秒才有反应。这哪是现代前端应用,简直是2003年的网页体验。

Vuex状态管理实战技巧与项目应用经验分享

一开始以为是接口慢,但查了下 network 面板,数据1秒内就回来了。问题显然不在后端。然后怀疑是组件渲染太多,拆了几个大组件也没见好转。最后把目光锁定了 Vuex —— 因为整个项目用了全局状态管理,所有列表、筛选、用户信息都塞在里面,store 文件都快3000行了。

找到瘼颈了!

我上了 Vue Devtools,切到 State 面板,发现每次点击一个筛选项,整个 store 树都会重新 computed 一遍,尤其是那个叫 getAllItems 的 getter,它依赖了一个巨大的 rawList,而这个 list 有上万条数据。更离谱的是,我在 mutations 里打了个 log,发现一个简单的 UI 切换(比如显示/隐藏弹窗)居然也触发了这个 getter —— 明明和它没关系。

这时候我意识到:不是 Vuex 慢,是我用得太糙了。getter 全是同步计算,没有缓存控制,组件还到处用 mapGetters 盲目绑定,导致一点点状态变动,全站响应式都在重算。

我还用了 Performance 面板录了一段操作:打开列表页 → 点击筛选 → 滑动页面。结果吓一跳:60% 的时间花在 JavaScript 上,其中超过一半是 getter 计算和组件 diff。这根本不是用户体验可接受的范围。

动手改:先砍掉无脑 computed

第一刀我砍向了那个万恶的 getAllItems getter。原来它是这样写的:

// 优化前
getters: {
  getAllItems: (state) => {
    return state.rawList
      .filter(item => item.status === 'active')
      .map(item => ({
        ...item,
        displayName: item.name || '未知'
      }))
      .sort((a, b) => a.sortIndex - b.sortIndex)
  }
}

问题在哪?每一条数据变动,哪怕只是某个 item 的 status 改了,整个数组都要重新 filter、map、sort。上万条数据,浏览器直接拉爆。

我把它改成按需计算 + 缓存键控制:

// 优化后
getters: {
  getAllItems: (state) => {
    const cacheKey = ${state.filterStatus}-${state.sortBy}
    if (state._cache[cacheKey]) {
      return state._cache[cacheKey]
    }

    let result = state.rawList

    if (state.filterStatus) {
      result = result.filter(item => item.status === state.filterStatus)
    }

    result = result.map(item => ({
      ...item,
      displayName: item.name || '未知'
    }))

    result.sort((a, b) => a[state.sortBy] - b[state.sortBy])

    // 缓存最多两个 key,避免内存爆炸
    state._cache = { [cacheKey]: result }
    return result
  }
}

同时在对应的 mutation 中清缓存:

mutations: {
  SET_FILTER_STATUS(state, status) {
    state.filterStatus = status
    // 清缓存,下一次调用 getter 会重建
    state._cache = {}
  },
  SET_SORT_BY(state, field) {
    state.sortBy = field
    state._cache = {}
  }
}

注意这里我没有用 LRU 或其他复杂结构,因为场景简单,清掉就行。而且 state._cache 不会被持久化,不影响业务逻辑。

组件别再乱监听了

另一个大问题是:很多组件其实只关心是否 loading,却通过 mapGetters 绑定了整个列表。比如这个 Header 组件:

// 优化前
computed: {
  ...mapGetters(['getAllItems', 'getUserInfo', 'isInitialized'])
},
watch: {
  getAllItems() {
    this.updateCount()
  }
}

即使 getAllItems 变了,Header 也不需要刷新,但它被绑了,所以每次列表变化它都 rerender。解决办法很简单:只监听你需要的状态。

// 优化后
computed: {
  isInitialized() {
    return this.$store.state.isInitialized
  },
  // 不再引入 getAllItems
},
methods: {
  updateCount() {
    const count = this.$store.getters.getAllItems.length
    this.localCount = count
  }
},
created() {
  // 手动触发一次,之后靠事件通知或防抖更新
  this.updateCount()
}

或者更进一步,用 shouldComponentUpdate 思路(Vue 里是 watch + immediate 控制),防抖更新:

watch: {
  isInitialized: {
    handler() {
      this.debounceUpdate()
    },
    immediate: true
  }
},
created() {
  this.debounceUpdate = this._debounce(() => {
    this.updateCount()
  }, 300)
}

这里的 _debounce 是自己封装的简易防抖,你也可以用 lodash。

异步操作别堵在 action 里

还有一个隐藏坑:action 里堆了太多同步逻辑。比如一个初始化 action:

// 优化前
actions: {
  async initApp({ commit, dispatch }) {
    await dispatch('fetchUser')
    await dispatch('fetchConfig')
    await dispatch('fetchItemList') // 三者其实无依赖
    commit('setInitialized', true)
  }
}

这三个请求明明可以并行,却串行执行,白白多花了2秒。改成 Promise.all:

// 优化后
actions: {
  async initApp({ commit, dispatch }) {
    await Promise.all([
      dispatch('fetchUser'),
      dispatch('fetchConfig'),
      dispatch('fetchItemList')
    ])
    commit('setInitialized', true)
  }
}

接口总耗时从 4.2s 降到 1.8s,效果立竿见影。

持久化也得小心

我们用了 vuex-persistedstate 把部分数据存 localStorage。但问题在于,每次 state 更新都同步写入,导致 IO 阻塞。特别是频繁变更的字段,比如临时表单数据。

解决方案:过滤掉不需要持久化的字段,并加个 debounce:

// 优化后
import createPersistedState from 'vuex-persistedstate'
import { _debounce } from '@/utils'

const debouncedSave = _debounce((key, value) => {
  localStorage.setItem(key, value)
}, 500)

export default new Vuex.Store({
  // ...
  plugins: [
    createPersistedState({
      storage: {
        getItem: (key) => localStorage.getItem(key),
        setItem: (key, value) => {
          debouncedSave(key, value)
        },
        removeItem: (key) => localStorage.removeItem(key)
      },
      reducer: (state) => ({
        user: state.user,
        config: state.config,
        // 去掉 rawList、_cache 等大字段
      })
    })
  ]
})

这样既保证关键数据落地,又避免频繁写入拖慢主线程。

优化后:流畅多了

改完之后,我拿同一台安卓机再测:首页加载从平均 6.8s 降到 920ms,滑动过程帧率稳定在 50fps 以上,按钮点击响应几乎无延迟。Devtools 里 getter 计算次数减少了 80%,mutation 触发不再引发全量重算。

最明显的感受是:用户不再投诉卡顿了。产品经理甚至说“感觉像是换了框架”——其实我只是把原本该注意的地方补上了。

性能数据对比

  • 首屏加载时间:6.8s → 920ms(降了 86%)
  • JS 占比 CPU 时间:60% → 22%
  • Getter 重计算频率:每次 mutation 都触发 → 仅关键参数变更时触发
  • 内存占用峰值:380MB → 210MB
  • 用户主动上报卡顿数:日均17次 → 日均2次

这些数字背后是实实在在的用户体验提升。虽然还有一些小瑕疵,比如冷启动时 still 有点白屏感,但已经不影响核心流程了。

结语

以上就是我这次 Vuex 性能优化的全过程。没有上什么高大上的库,也没有重构架构,就是一点一点看瓶颈、改代码、测数据。中间踩过好几次坑,比如缓存没清导致数据不一致,debounce 没控好让状态延迟更新,都是线上灰度才发现的。

这个方案不是最优解,但最简单、最可控,适合中后台这种快速迭代的项目。如果你有更好的方式,比如用 Pinia 迁移或者局部状态提升,欢迎评论区交流。我也在考虑下一步要不要动这块。

开发就是这样,永远在修修补补中前进。这次优化完,终于能睡个安稳觉了。

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

暂无评论