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

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

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

一、场景 1:列表渲染卡顿 ------ 从 "一次性渲染" 到 "虚拟列表"​

问题背景​

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

html 复制代码
<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. 安装依赖:

    javascript 复制代码
    npm install vue-virtual-scroller --save
  2. 组件中使用:

    html 复制代码
    <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 秒,触发接口超时。​

原代码痛点​

javascript 复制代码
// 错误示例:串行请求,耗时叠加
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 分钟内重复进入页面不重复请求

优化后代码

javascript 复制代码
// 优化后:并行请求+缓存
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 秒以上,期间无法点击任何按钮。​

原代码痛点

javascript 复制代码
// 错误示例:主线程中处理大量计算
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)

    javascript 复制代码
    // 子线程:处理计算逻辑,不操作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

    javascript 复制代码
    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)跟踪性能指标,一旦发现异常及时优化。如果大家有其他优化场景或问题,欢迎在评论区交流~

相关推荐
Zhencode7 小时前
Vue3 响应式依赖收集与更新之effect
前端·vue.js
天下代码客7 小时前
使用electronc框架调用dll动态链接库流程和避坑
前端·javascript·vue.js·electron·node.js
冰暮流星8 小时前
javascript之数组
java·前端·javascript
l1t8 小时前
DeepSeek总结的PostgreSQL解码GIF文件SQL移植到DuckDB的性能优化方法
sql·postgresql·性能优化
weixin79893765432...8 小时前
Vue 渲染体系“三件套”(template 模板语法、h 函数和 JSX 语法)
vue.js·h函数·template 模板·jsx 语法
xkxnq8 小时前
第五阶段:Vue3核心深度深挖(第74天)(Vue3计算属性进阶)
前端·javascript·vue.js
三小河9 小时前
Agent Skill与Rules的区别——以Cursor为例
前端·javascript·后端
Hilaku9 小时前
不要在简历上写精通 Vue3?来自面试官的真实劝退
前端·javascript·vue.js
三小河9 小时前
前端视角详解 Agent Skill
前端·javascript·后端
数据知道9 小时前
PostgreSQL 性能优化:分区表实战
数据库·postgresql·性能优化