前端性能优化实用方案(二):并行加载让接口响应速度提升3倍

举报
Yeats_Liao 发表于 2025/11/13 17:00:24 2025/11/13
【摘要】 2 首屏速度优化的其他实用技巧前面我们讲了减少资源体积的方法,这次聊聊另外两个收效不错的优化方向。虽然效果可能没有减少体积那么立竿见影,但在特定场景下还是很有价值的,特别是在用户体验的细节打磨上。 2.1 数据并行加载:让接口请求不再排队等待数据加载的并行化和接口合并可以减少网络请求的延迟,提升首屏渲染速度。这个优化在数据量大、接口多的项目中效果比较明显。 并行数据加载很多时候我们习惯性地...

2 首屏速度优化的其他实用技巧

前面我们讲了减少资源体积的方法,这次聊聊另外两个收效不错的优化方向。虽然效果可能没有减少体积那么立竿见影,但在特定场景下还是很有价值的,特别是在用户体验的细节打磨上。

2.1 数据并行加载:让接口请求不再排队等待

数据加载的并行化和接口合并可以减少网络请求的延迟,提升首屏渲染速度。这个优化在数据量大、接口多的项目中效果比较明显。

并行数据加载

很多时候我们习惯性地串行加载数据,一个接口调完再调下一个。但其实很多数据之间并没有强依赖关系,完全可以同时请求。

// 传统串行加载(不推荐)
const loadDataSerial = async () => {
  try {
    const userInfo = await fetchUserInfo();
    const userPreferences = await fetchUserPreferences(userInfo.id);
    const notifications = await fetchNotifications(userInfo.id);
    const dashboardData = await fetchDashboardData(userInfo.id);
    
    return {
      userInfo,
      userPreferences,
      notifications,
      dashboardData
    };
  } catch (error) {
    console.error('数据加载失败:', error);
  }
};

// 并行加载(推荐)
const loadDataParallel = async () => {
  try {
    // 首先获取用户基础信息
    const userInfo = await fetchUserInfo();
    
    // 并行加载其他数据
    const [userPreferences, notifications, dashboardData] = await Promise.all([
      fetchUserPreferences(userInfo.id),
      fetchNotifications(userInfo.id),
      fetchDashboardData(userInfo.id)
    ]);
    
    return {
      userInfo,
      userPreferences,
      notifications,
      dashboardData
    };
  } catch (error) {
    console.error('数据加载失败:', error);
    // 处理部分失败的情况
    return handlePartialFailure(error);
  }
};

// 更进一步:完全并行加载(如果接口支持)
const loadDataFullParallel = async () => {
  try {
    const promises = {
      userInfo: fetchUserInfo(),
      userPreferences: fetchUserPreferences(),
      notifications: fetchNotifications(),
      dashboardData: fetchDashboardData()
    };
    
    // 使用Promise.allSettled处理部分失败
    const results = await Promise.allSettled(Object.values(promises));
    const keys = Object.keys(promises);
    
    const data = {};
    results.forEach((result, index) => {
      const key = keys[index];
      if (result.status === 'fulfilled') {
        data[key] = result.value;
      } else {
        console.warn(`${key} 加载失败:`, result.reason);
        data[key] = null; // 或提供默认值
      }
    });
    
    return data;
  } catch (error) {
    console.error('数据加载失败:', error);
  }
};

接口合并策略

对于一些小数据量的接口,合并请求可以减少网络开销。不过这个要看具体情况,如果数据更新频率不同,就不太适合合并。

// 小数据量接口合并
class ApiCombiner {
  constructor() {
    this.batchRequests = new Map();
    this.batchTimeout = null;
    this.batchDelay = 50; // 50ms内的请求会被合并
  }

  // 合并多个小接口到一个请求
  async batchFetch(requests) {
    return new Promise((resolve) => {
      // 将请求添加到批次中
      const requestId = Date.now() + Math.random();
      this.batchRequests.set(requestId, { requests, resolve });
      
      // 设置批次处理延迟
      if (this.batchTimeout) {
        clearTimeout(this.batchTimeout);
      }
      
      this.batchTimeout = setTimeout(() => {
        this.processBatch();
      }, this.batchDelay);
    });
  }

  async processBatch() {
    if (this.batchRequests.size === 0) return;
    
    // 收集所有请求
    const allRequests = [];
    const resolvers = [];
    
    this.batchRequests.forEach(({ requests, resolve }) => {
      allRequests.push(...requests);
      resolvers.push(resolve);
    });
    
    try {
      // 发送合并请求
      const response = await fetch('/api/batch', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          requests: allRequests
        })
      });
      
      const results = await response.json();
      
      // 分发结果给各个请求者
      let resultIndex = 0;
      this.batchRequests.forEach(({ requests, resolve }) => {
        const requestResults = results.slice(resultIndex, resultIndex + requests.length);
        resolve(requestResults);
        resultIndex += requests.length;
      });
      
    } catch (error) {
      // 错误处理
      resolvers.forEach(resolve => resolve([]));
    } finally {
      this.batchRequests.clear();
      this.batchTimeout = null;
    }
  }
}

// 使用示例
const apiCombiner = new ApiCombiner();

// 原本需要多个请求的数据
const loadDashboardData = async () => {
  const requests = [
    { type: 'user_stats', params: {} },
    { type: 'recent_activities', params: { limit: 5 } },
    { type: 'system_notifications', params: {} },
    { type: 'quick_actions', params: {} }
  ];
  
  const results = await apiCombiner.batchFetch(requests);
  
  return {
    userStats: results[0],
    recentActivities: results[1],
    systemNotifications: results[2],
    quickActions: results[3]
  };
};

GraphQL实现数据合并

如果你的项目用GraphQL,那数据合并就更简单了。一次查询就能拿到所有需要的数据。

// 使用GraphQL一次性获取多种数据
import { gql } from '@apollo/client';

// 合并查询
const DASHBOARD_QUERY = gql`
  query DashboardData($userId: ID!) {
    user(id: $userId) {
      id
      name
      email
      avatar
      preferences {
        theme
        language
        notifications
      }
    }
    
    notifications(userId: $userId, limit: 10) {
      id
      title
      content
      createdAt
      read
    }
    
    dashboardStats(userId: $userId) {
      totalProjects
      completedTasks
      pendingTasks
      teamMembers
    }
    
    recentActivities(userId: $userId, limit: 5) {
      id
      action
      target
      createdAt
    }
  }
`;

// React组件中使用
import { useQuery } from '@apollo/client';

const Dashboard = () => {
  const { loading, error, data } = useQuery(DASHBOARD_QUERY, {
    variables: { userId: getCurrentUserId() },
    // 启用缓存
    fetchPolicy: 'cache-first',
    // 错误处理
    errorPolicy: 'partial'
  });

  if (loading) return <DashboardSkeleton />;
  if (error) return <ErrorBoundary error={error} />;

  return (
    <div className="dashboard">
      <UserProfile user={data.user} />
      <NotificationList notifications={data.notifications} />
      <StatsWidget stats={data.dashboardStats} />
      <ActivityFeed activities={data.recentActivities} />
    </div>
  );
};

2.2 分批渲染:处理大量DOM的性能杀手

当页面需要渲染大量DOM元素时,一次性渲染会造成明显的卡顿。这时候虚拟滚动和分批渲染就派上用场了。

虚拟滚动实现

虚拟滚动的核心思想是只渲染可视区域内的元素,其他的用占位符代替。这样即使有几万条数据,DOM元素也只有几十个。

<template>
  <div 
    class="virtual-list-container"
    @scroll="handleScroll"
    :style="{ height: containerHeight + 'px' }"
  >
    <!-- 占位元素,用于撑开滚动条 -->
    <div 
      class="virtual-list-phantom"
      :style="{ height: totalHeight + 'px' }"
    ></div>
    
    <!-- 可视区域内容 -->
    <div 
      class="virtual-list-content"
      :style="{
        transform: `translateY(${offsetY}px)`
      }"
    >
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="virtual-list-item"
        :style="{ height: itemHeight + 'px' }"
      >
        <slot :item="item" :index="item.index"></slot>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';

const props = defineProps({
  items: {
    type: Array,
    required: true
  },
  itemHeight: {
    type: Number,
    default: 50
  },
  containerHeight: {
    type: Number,
    default: 400
  },
  bufferSize: {
    type: Number,
    default: 5 // 缓冲区大小
  }
});

const scrollTop = ref(0);
const containerRef = ref(null);

// 计算总高度
const totalHeight = computed(() => {
  return props.items.length * props.itemHeight;
});

// 计算可视区域能显示的项目数量
const visibleCount = computed(() => {
  return Math.ceil(props.containerHeight / props.itemHeight);
});

// 计算开始索引
const startIndex = computed(() => {
  const index = Math.floor(scrollTop.value / props.itemHeight);
  return Math.max(0, index - props.bufferSize);
});

// 计算结束索引
const endIndex = computed(() => {
  const index = startIndex.value + visibleCount.value + props.bufferSize * 2;
  return Math.min(props.items.length, index);
});

// 计算可视区域的项目
const visibleItems = computed(() => {
  return props.items.slice(startIndex.value, endIndex.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }));
});

// 计算偏移量
const offsetY = computed(() => {
  return startIndex.value * props.itemHeight;
});

// 滚动处理
const handleScroll = (event) => {
  scrollTop.value = event.target.scrollTop;
};

// 滚动到指定项目
const scrollToItem = (index) => {
  const targetScrollTop = index * props.itemHeight;
  containerRef.value.scrollTop = targetScrollTop;
};

// 暴露方法
defineExpose({
  scrollToItem
});
</script>

<style scoped>
.virtual-list-container {
  position: relative;
  overflow-y: auto;
}

.virtual-list-phantom {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: -1;
}

.virtual-list-content {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}

.virtual-list-item {
  box-sizing: border-box;
}
</style>

分批渲染实现

如果不想用虚拟滚动,分批渲染也是个不错的选择。基本思路是把大量数据分成小批次,每次只渲染一部分,避免长时间阻塞主线程。

// 分批渲染工具类
class BatchRenderer {
  constructor(options = {}) {
    this.batchSize = options.batchSize || 20;
    this.delay = options.delay || 16; // 约60fps
    this.isRendering = false;
    this.renderQueue = [];
  }

  // 添加渲染任务
  addRenderTask(items, container, renderFunction) {
    return new Promise((resolve) => {
      this.renderQueue.push({
        items,
        container,
        renderFunction,
        resolve,
        currentIndex: 0
      });
      
      if (!this.isRendering) {
        this.startRendering();
      }
    });
  }

  // 开始渲染
  async startRendering() {
    this.isRendering = true;
    
    while (this.renderQueue.length > 0) {
      const task = this.renderQueue[0];
      const { items, container, renderFunction, resolve, currentIndex } = task;
      
      // 计算本批次要渲染的项目
      const endIndex = Math.min(currentIndex + this.batchSize, items.length);
      const batchItems = items.slice(currentIndex, endIndex);
      
      // 渲染本批次
      const fragment = document.createDocumentFragment();
      batchItems.forEach((item, index) => {
        const element = renderFunction(item, currentIndex + index);
        fragment.appendChild(element);
      });
      
      container.appendChild(fragment);
      
      // 更新进度
      task.currentIndex = endIndex;
      
      // 检查是否完成
      if (endIndex >= items.length) {
        this.renderQueue.shift();
        resolve();
      }
      
      // 让出控制权,避免阻塞UI
      await this.sleep(this.delay);
    }
    
    this.isRendering = false;
  }

  // 延迟函数
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 清空渲染队列
  clear() {
    this.renderQueue = [];
    this.isRendering = false;
  }
}

// 使用示例
const batchRenderer = new BatchRenderer({
  batchSize: 50,
  delay: 16
});

// 渲染大量列表项
const renderLargeList = async (data, container) => {
  // 清空容器
  container.innerHTML = '';
  
  // 显示加载状态
  const loadingElement = document.createElement('div');
  loadingElement.textContent = '正在加载...';
  loadingElement.className = 'loading';
  container.appendChild(loadingElement);
  
  // 定义单项渲染函数
  const renderItem = (item, index) => {
    const element = document.createElement('div');
    element.className = 'list-item';
    element.innerHTML = `
      <div class="item-header">
        <h3>${item.title}</h3>
        <span class="item-index">#${index + 1}</span>
      </div>
      <div class="item-content">
        <p>${item.description}</p>
        <div class="item-meta">
          <span>创建时间: ${item.createdAt}</span>
          <span>状态: ${item.status}</span>
        </div>
      </div>
    `;
    return element;
  };
  
  // 移除加载状态
  container.removeChild(loadingElement);
  
  // 开始分批渲染
  await batchRenderer.addRenderTask(data, container, renderItem);
  
  console.log('渲染完成,共渲染', data.length, '个项目');
};

export { BatchRenderer, renderLargeList };

React中的分批渲染

React项目中,可以结合useState和useEffect来实现分批渲染。

import React, { useState, useEffect, useCallback } from 'react';

const BatchList = ({ items, batchSize = 20, renderDelay = 16 }) => {
  const [visibleItems, setVisibleItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [currentBatch, setCurrentBatch] = useState(0);

  // 分批加载数据
  const loadNextBatch = useCallback(async () => {
    if (currentBatch * batchSize >= items.length) {
      return;
    }

    setIsLoading(true);
    
    // 模拟异步处理
    await new Promise(resolve => setTimeout(resolve, renderDelay));
    
    const startIndex = currentBatch * batchSize;
    const endIndex = Math.min(startIndex + batchSize, items.length);
    const newItems = items.slice(startIndex, endIndex);
    
    setVisibleItems(prev => [...prev, ...newItems]);
    setCurrentBatch(prev => prev + 1);
    setIsLoading(false);
  }, [items, batchSize, currentBatch, renderDelay]);

  // 初始化加载
  useEffect(() => {
    setVisibleItems([]);
    setCurrentBatch(0);
    loadNextBatch();
  }, [items]);

  // 滚动到底部时加载更多
  const handleScroll = useCallback((e) => {
    const { scrollTop, scrollHeight, clientHeight } = e.target;
    const isNearBottom = scrollTop + clientHeight >= scrollHeight - 100;
    
    if (isNearBottom && !isLoading && currentBatch * batchSize < items.length) {
      loadNextBatch();
    }
  }, [isLoading, loadNextBatch, currentBatch, batchSize, items.length]);

  return (
    <div 
      className="batch-list-container"
      onScroll={handleScroll}
      style={{ height: '400px', overflowY: 'auto' }}
    >
      {visibleItems.map((item, index) => (
        <div key={item.id || index} className="batch-list-item">
          <h3>{item.title}</h3>
          <p>{item.description}</p>
          <div className="item-meta">
            <span>索引: {index}</span>
            <span>状态: {item.status}</span>
          </div>
        </div>
      ))}
      
      {isLoading && (
        <div className="loading-indicator">
          <span>加载中...</span>
        </div>
      )}
      
      {currentBatch * batchSize >= items.length && (
        <div className="end-indicator">
          <span>已加载全部 {items.length}</span>
        </div>
      )}
    </div>
  );
};

export default BatchList;

性能监控和优化

做性能优化的时候,监控数据很重要。这样才能知道优化效果如何,哪里还有改进空间。

// 性能监控工具
class PerformanceMonitor {
  constructor() {
    this.metrics = {
      renderTime: [],
      memoryUsage: [],
      fps: []
    };
    this.isMonitoring = false;
  }

  // 开始监控
  startMonitoring() {
    this.isMonitoring = true;
    this.monitorFPS();
    this.monitorMemory();
  }

  // 停止监控
  stopMonitoring() {
    this.isMonitoring = false;
  }

  // 监控FPS
  monitorFPS() {
    let lastTime = performance.now();
    let frameCount = 0;
    
    const measureFPS = (currentTime) => {
      frameCount++;
      
      if (currentTime - lastTime >= 1000) {
        const fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
        this.metrics.fps.push(fps);
        
        frameCount = 0;
        lastTime = currentTime;
        
        // 如果FPS过低,发出警告
        if (fps < 30) {
          console.warn('FPS过低:', fps);
        }
      }
      
      if (this.isMonitoring) {
        requestAnimationFrame(measureFPS);
      }
    };
    
    requestAnimationFrame(measureFPS);
  }

  // 监控内存使用
  monitorMemory() {
    const checkMemory = () => {
      if (performance.memory) {
        const memoryInfo = {
          used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024),
          total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024),
          limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)
        };
        
        this.metrics.memoryUsage.push(memoryInfo);
        
        // 内存使用率过高时警告
        const usageRate = memoryInfo.used / memoryInfo.limit;
        if (usageRate > 0.8) {
          console.warn('内存使用率过高:', Math.round(usageRate * 100) + '%');
        }
      }
      
      if (this.isMonitoring) {
        setTimeout(checkMemory, 5000); // 每5秒检查一次
      }
    };
    
    checkMemory();
  }

  // 测量渲染时间
  measureRenderTime(renderFunction) {
    return async (...args) => {
      const startTime = performance.now();
      const result = await renderFunction(...args);
      const endTime = performance.now();
      
      const renderTime = endTime - startTime;
      this.metrics.renderTime.push(renderTime);
      
      if (renderTime > 16) { // 超过一帧的时间
        console.warn('渲染时间过长:', renderTime.toFixed(2) + 'ms');
      }
      
      return result;
    };
  }

  // 获取性能报告
  getReport() {
    const avgRenderTime = this.metrics.renderTime.length > 0 
      ? this.metrics.renderTime.reduce((a, b) => a + b) / this.metrics.renderTime.length 
      : 0;
    
    const avgFPS = this.metrics.fps.length > 0
      ? this.metrics.fps.reduce((a, b) => a + b) / this.metrics.fps.length
      : 0;
    
    return {
      averageRenderTime: avgRenderTime.toFixed(2) + 'ms',
      averageFPS: avgFPS.toFixed(1),
      totalRenders: this.metrics.renderTime.length,
      memorySnapshots: this.metrics.memoryUsage.length
    };
  }
}

// 使用示例
const monitor = new PerformanceMonitor();
monitor.startMonitoring();

// 包装渲染函数
const monitoredRender = monitor.measureRenderTime(renderLargeList);

// 使用监控的渲染函数
monitoredRender(largeDataSet, containerElement);

// 获取性能报告
setTimeout(() => {
  console.log('性能报告:', monitor.getReport());
  monitor.stopMonitoring();
}, 30000);

export { PerformanceMonitor };

小结

这两个优化方向虽然收效相对较小,但在特定场景下还是很有价值的:

数据并行加载可以减少20-40%的数据加载时间,特别是在接口较多的管理后台类项目中效果明显。接口合并能减少网络请求数量,但要注意不要过度合并,影响缓存策略。

虚拟滚动和分批渲染在处理大量数据时性能提升很明显,可以支持数万条数据流畅滚动。关键是要在合适的场景下使用,避免为了优化而优化,增加不必要的复杂度。

下一篇我们会讲缓存策略和资源预加载,这些技术在提升用户体验方面效果更加明显。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。