前端性能优化实践经验总结

throttle与debounce

  • 一般直接使用现有的lodash库,如果只是单纯希望实现throttle和debounce的简易功能,不需要引入其他的一些工具,为了打包结果更小可以采用自己写函数的方式

throttle: 同时间段内多次触发只有第一次生效

常见应用场景:

  • 防止button被频繁点击,触发多个弹层功能
  • 滚动加载更多场景

debounce:同时间段内多次触发只有最后一次生效

常见应用场景:

  • 输入框,一般希望用户输入完成之后再触发搜索功能
  • 窗口拖拽场景,resize之后只在最后实现实际函数

除此之外还可以在某些场景用于请求控制,典型场景:

  • 同时监听tab和下拉框选中信息以及搜索框的信息,用于更新表格信息。如果搜索有值的情况希望在切换Tab的情况清空对应的搜索值,返回新tab页新下拉框选中的信息数据,这个时候其实会发送两次请求:
  1. 切换tab,获取新的下拉框信息,并由此获取列表数据,并清空对应搜索项
  2. 监听搜索项变化,获取新的列表数据

实际只需要获得最后一次的请求数据,但是因为需求问题,有时候不可避免要多个watch监听不同的条件,导致同时触发多个请求,请求因为时延问题,不一定会按照发送顺序接收,最终导致有用的信息被后续到的请求覆盖 ,这个时候因为请求发送的时机是可控的(接收时机受后端和网络时延往往不可控),可以使用throttle和debounce进行请求控制

请求竞态AbortController

结合请求拦截器和 AbortController 处理请求竞态,可以实现 全局统一管理重复请求 的效果:当相同的请求(如同一接口、同一参数)重复发送时,自动取消前序未完成的请求,只保留最后一次请求。这种方式无需在每个请求处单独处理,更适合大型项目。

js 复制代码
import axios from 'axios';

// 1. 创建Axios实例
const instance = axios.create({
  baseURL: '/api',
  timeout: 5000
});

// 2. 存储当前活跃的请求控制器:key为请求唯一标识,value为AbortController实例
const requestControllers = new Map();

/**
 * 生成请求唯一标识(确保相同请求生成相同key)
 * @param {Object} config - Axios请求配置
 * @returns {string} 唯一标识字符串
 */
const generateRequestKey = (config) => {
  const { url, method, params, data } = config;
  // 组合URL、方法、参数生成key(忽略无关配置,确保相同请求生成相同key)
  return [
    url,
    method?.toUpperCase(),
    JSON.stringify(params || {}),
    JSON.stringify(data || {})
  ].join('&');
};

// 3. 请求拦截器:处理重复请求,取消前序请求
instance.interceptors.request.use(
  (config) => {
    // 生成当前请求的唯一标识
    const requestKey = generateRequestKey(config);

    // 若存在前序相同请求的控制器,取消它
    if (requestControllers.has(requestKey)) {
      const prevController = requestControllers.get(requestKey);
      prevController.abort(); // 取消前序请求
      requestControllers.delete(requestKey); // 移除旧控制器
      console.log(`已取消重复请求: ${requestKey}`);
    }

    // 创建新的AbortController,关联当前请求
    const controller = new AbortController();
    config.signal = controller.signal; // 将signal加入请求配置

    // 存储新控制器
    requestControllers.set(requestKey, controller);
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 4. 响应拦截器:清理控制器(无论成功/失败)
instance.interceptors.response.use(
  (response) => {
    const requestKey = generateRequestKey(response.config);
    requestControllers.delete(requestKey); // 请求完成,移除控制器
    return response;
  },
  (error) => {
    // 处理取消请求的错误(不视为业务错误)
    if (error.name === 'CanceledError') {
      console.log('请求已被取消(重复请求)');
      return Promise.resolve(null); // 或根据业务需求处理
    }

    // 其他错误:清理控制器并抛出
    if (error.config) {
      const requestKey = generateRequestKey(error.config);
      requestControllers.delete(requestKey);
    }
    return Promise.reject(error);
  }
);

如果某些接口需要允许并发(不取消前序请求),可通过请求配置中的 allowConcurrent 字段例外处理

arduino 复制代码
// 修改请求拦截器逻辑,增加例外判断
instance.interceptors.request.use((config) => {
  // 若配置 allowConcurrent: true,则不取消前序请求
  if (config.allowConcurrent) {
    return config;
  }

  // 否则执行原逻辑(取消重复请求)
  const requestKey = generateRequestKey(config);
  // ... 剩余逻辑不变
});

// 使用时允许并发
fetchData(1, { allowConcurrent: true }); // 此请求不会被后续相同请求取消

Q: 按照上述代码,响应拦截器的时候没办法删除map内的key,显示没有这个key

A: 出现这种情况通常是因为 请求拦截器和响应拦截器中生成的 requestKey 不一致 导致的(例如请求配置在发送过程中被修改)。解决方法是在请求拦截器中生成 requestKey 后直接存储到 config 中,在响应拦截器中直接使用该 key 而不是重新生成

ini 复制代码
instance.interceptors.request.use(
  (config) => {
    const requestKey = generateRequestKey(config);
    // 关键:将key存储到config中,确保响应时能获取到相同的key
    config.__requestKey = requestKey;

    if (requestControllers.has(requestKey)) {
      const prevController = requestControllers.get(requestKey);
      prevController.abort();
      requestControllers.delete(requestKey);
    }

    const controller = new AbortController();
    config.signal = controller.signal;
    requestControllers.set(requestKey, controller);

    return config;
  },
  (error) => Promise.reject(error)
);

instance.interceptors.response.use(
  (response) => {
    // 关键:直接使用请求时存储的key
    const requestKey = response.config.__requestKey;
    if (requestKey && requestControllers.has(requestKey)) {
      requestControllers.delete(requestKey);
    }
    return response;
  },
  (error) => {
    if (error.config && error.config.__requestKey) {
      const requestKey = error.config.__requestKey;
      if (requestControllers.has(requestKey)) {
        requestControllers.delete(requestKey);
      }
    }

    if (error.name === 'CanceledError') {
      return Promise.resolve(null);
    }

    return Promise.reject(error);
  }
);

Tailwindcss原子化CSS

css类原子化,使得公用的css代码不用再次生成,可以导致最终生产环境打包的css代码体积减小

useMemo && useCallback && react.memo

  • 静态组件(指只收到外部输入props影响变更的组件,不受ref/useState等影响),可以使用react.memo包裹,去提高渲染的复用率
  • 静态函数如果需要缓存结果或者函数本身则可以使用useMemo和useCallback处理
  • 一般常见于react,vue一般不需要使用以上方式
    • 因为react的渲染机制影响,一旦父组件更新,内部全部子组件也会重渲染 ,这导致了为了优化渲染性能,会使用以上方式缓存不要变化的子组件;但是vue的渲染机制追踪的粒度更细,能够索引到对应的响应式数据上,进行局部变化,父组件某个变量更新并不会导致整个子组件重渲染
    • vue在某些特殊场景下:大量纯数据渲染列表组件(渲染耗时长,变化不频繁),可以使用v-memo去实现包裹

懒加载

  • 路由懒加载,基本是所有项目都会用的手段
  • 其他包括图片懒加载等,都可以提高对应的性能

优化项目代码结构

  • 比如监听watch多个变量,都需要触发同一个函数,并且多个变量之间会有触发重合的现象,就可以使用一个watch监听触发该函数,这样可以减少函数的多次不必要请求
js 复制代码
const handleChange = debounce(()=>{/.../},100)

watch([tabname,index,title],()=>{
    handleChange()
})

watch(title,()=>{
    handleValue()
})
相关推荐
90后的晨仔2 小时前
Vue 插槽(Slots)全面解析与实战指南
前端·vue.js
golang学习记2 小时前
从0死磕全栈之Next.js API 路由实战:不用后端,前端也能写接口!
前端
Nathan202406162 小时前
Kotlin-Sealed与Open的使用
android·前端·面试
RoyLin3 小时前
SurrealDB - 统一数据基础设施
前端·后端·typescript
longlongago~~3 小时前
富文本编辑器Tinymce的使用、行内富文本编辑器工具栏自定义class、katex渲染数学公式
前端·javascript·vue.js
2501_915921433 小时前
前端用什么开发工具?常用前端开发工具推荐与不同阶段的选择指南
android·前端·ios·小程序·uni-app·iphone·webview
aixfe3 小时前
BiomeJS 2.0 忽略目录配置方法
前端
Mintopia3 小时前
Cesium-kit 又发新玩意儿了:CameraControl 相机控制组件全解析
前端·three.js·cesium
ssshooter3 小时前
shader更换后,数据需要重新加载吗?
前端