从卡顿到丝滑:3 个实战场景教你搞定代码性能优化

发布于:2025-09-04 ⋅ 阅读:(15) ⋅ 点赞:(0)

作为程序员,我们每天都在和代码打交道 —— 实现功能、修复 BUG、迭代需求,但往往容易忽略一个关键问题:代码能跑通,不代表跑得好。当用户反馈 “页面加载要等 3 秒”“数据导出时浏览器直接卡死”“接口高峰期频繁超时”,这些看似偶然的问题,背后往往藏着性能优化的漏洞。​

今天结合 3 个真实项目场景,带你拆解代码优化的思路、工具和避坑点,看完就能用到实际开发中。

一、场景 1:列表渲染卡顿 —— 从 “一次性渲染” 到 “虚拟列表”​

问题背景​

前段时间接手一个管理系统,用户需要查看上万条订单数据,原代码直接用v-for(Vue 项目)循环渲染所有数据,打开页面时浏览器直接卡顿 5 秒以上,甚至出现 “未响应” 提示。

<template>
  <!-- 错误示例:一次性渲染10000+条数据 -->
  <div class="order-item" v-for="item in allOrders" :key="item.id">
    {{ item.orderNo }} - {{ item.amount }}
  </div>
</template>
<script>
export default {
  data() {
    return {
      allOrders: [] // 存储10000+条订单数据
    }
  },
  mounted() {
    this.getAllOrders(); // 一次性请求所有数据
  },
  methods: {
    getAllOrders() {
      // 直接请求全量数据,未做分页或懒加载
      api.get('/orders/all').then(res => {
        this.allOrders = res.data;
      });
    }
  }
}
</script>

问题根源:DOM 节点数量过多(上万条数据对应上万个子节点),浏览器渲染时回流重绘成本极高,导致主线程阻塞。​

优化方案:虚拟列表​

核心思路:只渲染可视区域内的列表项,滚动时动态替换可视区域的内容,DOM 节点数量始终保持在几十到上百个(取决于可视区域高度)。​

优化后代码(基于vue-virtual-scroller)​

  1. 安装依赖:​
    npm install vue-virtual-scroller --save
  2. 组件中使用:
    <template>
      <virtual-scroller
        class="order-list"
        :items="allOrders"
        :item-height="60" // 每个列表项的固定高度
        key-field="id"
      >
        <template v-slot="{ item }">
          <div class="order-item">
            {{ item.orderNo }} - {{ item.amount }}
          </div>
        </template>
      </virtual-scroller>
    </template>
    <script>
    import { VirtualScroller } from 'vue-virtual-scroller';
    import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
    
    export default {
      components: { VirtualScroller },
      data() {
        return {
          allOrders: []
        }
      },
      mounted() {
        this.getAllOrders();
      },
      methods: {
        getAllOrders() {
          api.get('/orders/all').then(res => {
            this.allOrders = res.data;
            // 优化点:如果数据量超10万,可配合后端做“滚动加载”
            // (即滚动到底部时请求下一页数据,避免一次性加载过多)
          });
        }
      }
    }
    </script>
    <style scoped>
    .order-list {
      height: 600px; /* 固定列表容器高度,确保可视区域可控 */
      overflow-y: auto;
    }
    .order-item {
      height: 60px;
      line-height: 60px;
      border-bottom: 1px solid #eee;
    }
    </style>

    优化效果​

    1. 页面加载时间从 5.2 秒降至 0.3 秒​
    2. DOM 节点数量从 12000 + 降至 80+​
    3. 滚动时无卡顿,丝滑度提升明显​

二、场景 2:接口超时 —— 从 “串行请求” 到 “并行 + 缓存”​

问题背景​

一个用户中心页面,需要加载用户基本信息、订单统计、收藏列表、消息通知 4 个接口数据,原代码用串行方式请求,总耗时 = 接口 1 耗时 + 接口 2 耗时 + 接口 3 耗时 + 接口 4 耗时,高峰期总耗时超 8 秒,触发接口超时。​

原代码痛点​

// 错误示例:串行请求,耗时叠加
async function loadUserPageData(userId) {
  // 1. 请求用户基本信息(耗时2.5秒)
  const userInfo = await api.get(`/user/${userId}/info`);
  // 2. 请求订单统计(耗时3秒)
  const orderStats = await api.get(`/user/${userId}/order-stats`);
  // 3. 请求收藏列表(耗时2秒)
  const collectList = await api.get(`/user/${userId}/collects`);
  // 4. 请求消息通知(耗时2.8秒)
  const notifications = await api.get(`/user/${userId}/notifications`);
  
  return { userInfo, orderStats, collectList, notifications };
}
// 总耗时:2.5+3+2+2.8=10.3秒(超时)

优化方案:并行请求 + 缓存复用​

核心思路:​

  1. 并行请求:4 个接口无依赖关系(不需要先拿到用户信息再请求订单),用Promise.all同时发起请求,总耗时 = 最长单个接口耗时​
  2. 缓存复用:用户基本信息、订单统计等数据短时间内不会变化,用localStorage或Vuex缓存,30 分钟内重复进入页面不重复请求​

优化后代码

// 优化后:并行请求+缓存
async function loadUserPageData(userId) {
  // 1. 定义缓存key和过期时间(30分钟)
  const CACHE_KEY = `userPageData_${userId}`;
  const CACHE_EXPIRE = 30 * 60 * 1000;
  
  // 2. 先查缓存,未过期则直接返回
  const cacheData = localStorage.getItem(CACHE_KEY);
  if (cacheData) {
    const { data, timestamp } = JSON.parse(cacheData);
    if (Date.now() - timestamp < CACHE_EXPIRE) {
      console.log('使用缓存数据');
      return data;
    }
  }
  
  // 3. 并行发起4个接口请求
  const [userInfoRes, orderStatsRes, collectListRes, notificationsRes] = await Promise.all([
    api.get(`/user/${userId}/info`),
    api.get(`/user/${userId}/order-stats`),
    api.get(`/user/${userId}/collects`),
    api.get(`/user/${userId}/notifications`)
  ]);
  
  // 4. 整理数据并缓存
  const result = {
    userInfo: userInfoRes.data,
    orderStats: orderStatsRes.data,
    collectList: collectListRes.data,
    notifications: notificationsRes.data
  };
  
  localStorage.setItem(CACHE_KEY, JSON.stringify({
    data: result,
    timestamp: Date.now()
  }));
  
  return result;
}
// 总耗时:取最长接口耗时(3秒),且重复进入页面时耗时≈0

额外优化点​

  • 如果某个接口(如消息通知)非核心,可做 “延迟加载”(页面加载完成后再请求)​
  • 用Promise.allSettled替代Promise.all,避免一个接口失败导致所有请求失效(非核心接口可容忍失败)​

三、场景 3:循环计算耗时 —— 从 “原生循环” 到 “Web Worker”​

问题背景​

一个数据可视化工具,需要对 10 万条用户行为数据做统计分析(计算留存率、转化率等),原代码在主线程中循环计算,导致页面卡顿 20 秒以上,期间无法点击任何按钮。​

原代码痛点

// 错误示例:主线程中处理大量计算
function calculateUserBehavior(data) {
  const result = {
    retentionRate: 0,
    conversionRate: 0,
    activeUsers: []
  };
  
  // 循环10万条数据做复杂计算(耗时20+秒)
  for (let i = 0; i < data.length; i++) {
    const user = data[i];
    // 1. 计算留存率(判断用户是否连续两天活跃)
    if (user.activeDays >= 2) {
      result.retentionRate += 1;
    }
    // 2. 计算转化率(判断用户是否完成付费)
    if (user.paid) {
      result.conversionRate += 1;
    }
    // 3. 筛选活跃用户(近7天活跃超3次)
    if (user.recentActiveDays >= 3) {
      result.activeUsers.push(user.id);
    }
  }
  
  // 计算最终比率
  result.retentionRate = (result.retentionRate / data.length * 100).toFixed(2) + '%';
  result.conversionRate = (result.conversionRate / data.length * 100).toFixed(2) + '%';
  
  return result;
}

// 调用后主线程阻塞
const behaviorData = await api.get('/user/behavior');
const result = calculateUserBehavior(behaviorData.data); // 页面卡顿20秒

优化方案:Web Worker​

核心思路:把复杂计算逻辑转移到子线程(Web Worker)中处理,主线程(UI 线程)保持空闲,用户可以正常操作页面,计算完成后通过事件通知主线程。​

优化后代码​

  1. 创建 Worker 文件(behavior-calculator.worker.js)
    // 子线程:处理计算逻辑,不操作DOM
    self.onmessage = function(e) {
      const data = e.data; // 接收主线程传递的数据
      const result = {
        retentionRate: 0,
        conversionRate: 0,
        activeUsers: []
      };
      
      // 10万条数据计算(在子线程中执行,不阻塞主线程)
      for (let i = 0; i < data.length; i++) {
        const user = data[i];
        if (user.activeDays >= 2) result.retentionRate += 1;
        if (user.paid) result.conversionRate += 1;
        if (user.recentActiveDays >= 3) result.activeUsers.push(user.id);
      }
      
      // 计算比率
      result.retentionRate = (result.retentionRate / data.length * 100).toFixed(2) + '%';
      result.conversionRate = (result.conversionRate / data.length * 100).toFixed(2) + '%';
      
      // 向主线程发送计算结果
      self.postMessage(result);
      // 关闭Worker(计算完成后释放资源)
      self.close();
    };

  2. 主线程中使用 Worker
    async function loadAndCalculateBehavior() {
      // 1. 请求数据
      const behaviorData = await api.get('/user/behavior');
      const rawData = behaviorData.data;
      
      // 2. 创建Worker(注意:本地开发需启动服务,直接打开HTML会跨域)
      const calculatorWorker = new Worker('./behavior-calculator.worker.js');
      
      // 3. 向Worker发送数据
      calculatorWorker.postMessage(rawData);
      
      // 4. 接收Worker返回的结果
      calculatorWorker.onmessage = function(e) {
        const result = e.data;
        console.log('计算完成', result);
        // 更新页面UI(主线程操作,安全)
        renderBehaviorResult(result);
      };
      
      // 5. 处理Worker错误
      calculatorWorker.onerror = function(error) {
        console.error('Worker计算出错', error);
        calculatorWorker.close();
      };
      
      // 优化点:如果用户在计算过程中离开页面,主动关闭Worker
      window.addEventListener('beforeunload', () => {
        calculatorWorker.close();
      });
    }
    
    // 调用后:页面可正常操作,计算在后台执行
    loadAndCalculateBehavior();

    优化效果​

    1. 页面卡顿消失,计算期间可正常点击、滚动​
    2. 计算总耗时略有增加(约 22 秒,因线程通信有少量开销),但用户体验大幅提升​
    3. 若使用SharedWorker,还可实现多标签页共享计算结果(适合多窗口场景)​

四、性能优化的 3 个核心原则​

通过以上 3 个场景,我们可以总结出代码优化的通用思路:​

  1. 定位瓶颈再优化:用 Chrome DevTools(Performance 面板)、Vue DevTools(性能面板)等工具找到卡顿 / 超时的具体原因,不要凭感觉优化(比如盲目用for循环替代forEach,实际性能提升微乎其微)。​
  2. 优先解决核心问题:先优化影响用户体验的关键场景(如页面加载、核心功能操作),再处理边缘场景。​
  3. 平衡优化成本与收益:不要为了 0.1 秒的性能提升,写出难以维护的代码(比如过度使用奇技淫巧的语法),可读性和可维护性同样重要。​

最后,性能优化不是一次性的工作,而是持续迭代的过程。建议大家在项目上线后,定期用监控工具(如 Sentry、阿里云 ARMS)跟踪性能指标,一旦发现异常及时优化。如果大家有其他优化场景或问题,欢迎在评论区交流~


网站公告

今日签到

点亮在社区的每一天
去签到