前端性能优化实用方案(二):并行加载让接口响应速度提升3倍
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%的数据加载时间,特别是在接口较多的管理后台类项目中效果明显。接口合并能减少网络请求数量,但要注意不要过度合并,影响缓存策略。
虚拟滚动和分批渲染在处理大量数据时性能提升很明显,可以支持数万条数据流畅滚动。关键是要在合适的场景下使用,避免为了优化而优化,增加不必要的复杂度。
下一篇我们会讲缓存策略和资源预加载,这些技术在提升用户体验方面效果更加明显。
- 点赞
- 收藏
- 关注作者
评论(0)