JavaScript性能优化系列(Vue3+TypeScript):第八章 弱网环境体验优化
8.1 请求优先级:核心功能请求优先处理
8.1.1 引言:弱网下的“请求拥堵”痛点
在前端开发中,我们常聚焦于“正常网络”下的性能优化(如首屏加载、组件渲染),却容易忽视弱网环境的特殊性——这里的“弱网”并非仅指“2G/3G网络”,还包括地铁、电梯等信号不稳定区域,以及带宽被占用的家庭/办公网络(如多设备同时下载)。
弱网环境的核心问题是**“资源争夺”**:浏览器对同一域名的并发请求存在限制(通常为6个TCP连接),且网络带宽低、延迟高、丢包率高。若所有请求“无差别发起”,非核心请求(如推荐列表、用户行为统计)会占用有限的连接数和带宽,导致核心请求(如用户登录、商品支付、首屏关键数据)排队等待,最终表现为“用户想做的操作加载慢,不想看的内容先出来”,严重破坏用户体验。
本节将围绕**“请求优先级”** 展开,结合Vue3+TypeScript生态,从原理、反例、正确实践、代码评审四个维度,讲解如何让核心功能请求“插队”优先处理,解决弱网下的请求拥堵问题。
8.1.2 原理讲解:请求优先级的核心逻辑
要实现“核心请求优先”,需先理解其底层逻辑——本质是**“资源分配策略”**:在有限的网络资源下,优先将带宽、连接数分配给对用户体验影响最大的请求。我们需要从“请求分类”“浏览器机制”“Vue3生态适配”三个层面掌握原理。
1. 核心请求与非核心请求的区分标准
首先要明确:什么是“核心请求”?什么是“非核心请求”?这是优先级设计的前提,不同业务场景的划分标准不同,但核心原则是**“是否影响用户当前操作目标”**。
| 请求类型 | 定义 | 常见场景 | 优先级 |
|---|---|---|---|
| 核心请求 | 影响首屏渲染、用户当前操作目标、业务核心流程的请求 | 1. 首屏关键数据(如用户信息、页面标题、核心功能入口) 2. 用户主动操作触发的请求(如登录提交、支付下单、按钮点击后的数据加载) 3. 阻断后续操作的请求(如表单验证、权限校验) | HIGH(高) |
| 非核心请求 | 不影响当前操作,可延迟加载或降级的请求 | 1. 非首屏内容(如页面底部的推荐列表、历史记录) 2. 统计类请求(如用户行为埋点、页面停留时长上报) 3. 可缓存的非关键数据(如商品评价、帮助文档内容) | LOW(低)/ MEDIUM(中) |
注意:优先级是“相对概念”,需结合业务场景动态调整。例如“商品详情页”中,“商品基本信息(名称、价格、库存)”是核心请求,“商品评价列表”是中等优先级,“同类商品推荐”是低优先级。
2. 浏览器原生的请求优先级机制
浏览器本身已具备“请求优先级调度”能力,前端可借助这些原生机制提升核心请求的优先级,避免重复造轮子。目前主流的原生能力有两种:
(1)Fetch API 的 priority 属性
Fetch API 支持通过 priority 参数显式设置请求优先级,浏览器会根据该值调整请求的调度顺序(高优先级请求优先进入网络队列)。该属性的取值范围如下:
high:高优先级,用于核心请求(如首屏数据、用户操作触发的请求)low:低优先级,用于非核心请求(如统计、非首屏内容)auto:默认值,浏览器根据请求类型自动判断(如script/fetch默认优先级中等,img默认优先级低)
兼容性:Chrome 102+、Edge 102+、Firefox 113+,主流现代浏览器已支持;若需兼容旧浏览器,可通过“请求头标记+后端配合”降级。
(2)资源预加载(<link rel="preload">)
<link rel="preload"> 用于告知浏览器“提前加载关键资源”,浏览器会将预加载的资源视为高优先级,优先分配带宽。虽然它主要用于静态资源(如JS、CSS、图片),但也可用于“核心API数据”(通过预加载JSON资源实现)。
例如,预加载“用户信息”API数据:
<!-- 预加载核心API数据,优先级高 -->
<link
rel="preload"
href="/api/v1/user/current"
as="fetch"
crossorigin="anonymous"
onload="handleUserInfoLoad(this.responseText)"
>
3. Vue3+TypeScript 生态的适配逻辑
Vue3 本身不直接提供“请求优先级”能力,但可通过“组合式API+请求库(Axios/Fetch)+工具类”实现完整方案。核心适配逻辑如下:
- 请求分类:通过 TypeScript 枚举定义优先级,约束所有请求必须指定优先级;
- 拦截器注入:在 Axios 拦截器中,为不同优先级的请求添加标记(如自定义 Header
X-Request-Priority),或适配 Fetch 的priority属性; - 队列管理:实现“优先级队列”工具类,控制请求的发起顺序和并发数,确保高优先级请求先执行;
- 生命周期联动:结合 Vue3 的
onMounted、nextTick等生命周期,在合适时机发起不同优先级的请求(如核心请求在onMounted早期发起,非核心请求延迟发起)。
8.1.3 实践反例:这些错误正在阻塞核心请求
在实际项目中,“请求优先级混乱”是弱网下体验差的主要原因之一。以下是三个典型反例,结合 Vue3+TypeScript 代码分析问题根源及后果。
反例1:所有请求无差别并发发起
场景:首页加载时,同时发起5个请求(用户信息、商品列表、推荐内容、公告、行为统计),未区分优先级。
代码实现(Vue3 SFC + TypeScript):
<template>
<div class="home-page">
<!-- 核心内容:用户信息 + 商品列表 -->
<UserInfo :user="user" />
<GoodsList :goods="goodsList" />
<!-- 非核心内容:推荐 + 公告 + 统计(不可见) -->
<RecommendList :list="recommendList" />
<Announcement :content="announcement" />
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import axios from 'axios';
import UserInfo from './components/UserInfo.vue';
import GoodsList from './components/GoodsList.vue';
import RecommendList from './components/RecommendList.vue';
import Announcement from './components/Announcement.vue';
// 状态定义
const user = ref<any>(null);
const goodsList = ref<any[]>([]);
const recommendList = ref<any[]>([]);
const announcement = ref('');
// 所有请求在 onMounted 中无差别并发发起
onMounted(async () => {
// 核心请求:用户信息
const userRes = await axios.get('/api/v1/user/current');
user.value = userRes.data;
// 核心请求:商品列表
const goodsRes = await axios.get('/api/v1/goods/list');
goodsList.value = goodsRes.data;
// 非核心请求:推荐内容
const recommendRes = await axios.get('/api/v1/goods/recommend');
recommendList.value = recommendRes.data;
// 非核心请求:公告
const announcementRes = await axios.get('/api/v1/system/announcement');
announcement.value = announcementRes.data.content;
// 非核心请求:行为统计(用户无感知)
await axios.post('/api/v1/stat/behavior', {
page: 'home',
action: 'load',
time: new Date().getTime()
});
});
</script>
问题分析:
- 并发数超限:浏览器对同一域名的并发TCP连接限制为6个(不同浏览器略有差异)。若首页同时发起5个API请求,再加上图片、CSS、JS等静态资源请求,会导致部分请求进入“等待队列”;
- 优先级倒置:非核心请求(如推荐、统计)与核心请求(用户信息、商品列表)同时发起,若非核心请求先被分配到连接,核心请求会被阻塞,导致用户“看不到个人信息和商品,却先加载出推荐内容”;
- 无容错机制:弱网下非核心请求若超时或失败,可能阻塞后续代码执行(如统计请求失败导致其他逻辑中断,虽示例中用了
await,但实际项目中常忽略错误处理)。
弱网下的后果:首页加载10+秒,核心内容(用户信息、商品列表)延迟显示,用户可能因“看不到关键内容”直接退出页面。
反例2:核心请求依赖非核心请求的结果
场景:商品详情页中,需要先加载“商品分类列表”(非核心),再加载“商品详情”(核心),导致详情加载延迟。
代码实现(Vue3 SFC + TypeScript):
<template>
<div class="goods-detail-page">
<!-- 核心内容:商品详情 -->
<GoodsDetail :detail="goodsDetail" v-if="goodsDetail" />
<!-- 非核心内容:商品分类 -->
<GoodsCategory :categories="categories" v-if="categories" />
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import axios from 'axios';
import GoodsDetail from './components/GoodsDetail.vue';
import GoodsCategory from './components/GoodsCategory.vue';
// 状态定义
const goodsDetail = ref<any>(null);
const categories = ref<any[]>([]);
// 从路由获取商品ID
const goodsId = route.params.id as string;
onMounted(async () => {
try {
// 错误逻辑:先加载非核心的分类列表
const categoryRes = await axios.get('/api/v1/goods/categories');
categories.value = categoryRes.data;
// 核心请求:依赖分类列表加载完成后才发起
const detailRes = await axios.get(`/api/v1/goods/detail/${goodsId}`);
goodsDetail.value = detailRes.data;
} catch (error) {
console.error('加载失败', error);
}
});
</script>
问题分析:
- 逻辑依赖错误:商品详情是用户访问该页面的核心目标,分类列表仅用于“切换分类”,二者无逻辑依赖关系,不该让核心请求等待非核心请求;
- 串行阻塞:弱网下分类列表请求若延迟5秒,商品详情请求会被阻塞5秒后才发起,进一步延长核心内容的加载时间;
- 容错缺失:若分类列表请求失败(如接口报错),会直接导致商品详情请求无法发起,用户完全看不到核心内容。
弱网下的后果:用户进入详情页后,长时间看到空白或加载中状态,分类列表和详情都无法显示,严重影响转化(如用户想买商品却看不到详情)。
反例3:未利用浏览器原生优先级机制
场景:使用 Axios 发起核心请求时,未设置任何优先级标记,浏览器按“发起顺序”而非“重要性”调度请求。
代码实现(Axios 配置 + Vue3):
// src/utils/request.ts(Axios 实例配置)
import axios from 'axios';
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000, // 所有请求统一超时时间
headers: {
'Content-Type': 'application/json'
}
});
// 未添加任何优先级相关配置
request.interceptors.request.use(
(config) => {
// 仅添加token,无优先级标记
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
export default request;
<!-- 组件中使用 -->
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import request from '@/utils/request';
const payResult = ref<any>(null);
const payId = route.params.payId as string;
onMounted(async () => {
// 核心请求:支付结果查询(用户等待确认支付状态)
const res = await request.get(`/api/v1/pay/result/${payId}`);
payResult.value = res.data;
// 非核心请求:支付成功后的推荐商品
const recommendRes = await request.get('/api/v1/goods/recommend?scene=pay_success');
// ...
});
</script>
问题分析:
- 浏览器无法识别优先级:由于未设置
priority或自定义 Header,浏览器会按“请求发起顺序”调度,若核心请求发起稍晚(如被其他逻辑延迟),会被后续请求阻塞; - 前后端无协同:后端无法识别核心请求,只能按“接收顺序”处理,弱网下若核心请求在传输过程中延迟,后端会先处理非核心请求,进一步延长核心请求的响应时间;
- 超时时间一刀切:所有请求统一设置10秒超时,核心请求(如支付结果查询)需要更短的超时重试机制,非核心请求可容忍更长超时,统一配置会导致核心请求“该重试时不重试,该快速失败时不失败”。
弱网下的后果:用户支付后,长时间等待支付结果(核心请求被阻塞),而推荐商品(非核心)先加载出来,用户会因“不确定支付是否成功”产生焦虑,甚至重复支付。
8.1.4 正确实践:Vue3+TypeScript 实现请求优先级方案
针对上述反例,本节提供一套完整的“请求优先级”实践方案,涵盖“分类体系、队列管理、生命周期联动、原生API利用”四个核心环节,可直接在 Vue3+TypeScript 项目中复用。
步骤1:定义请求优先级分类体系(TypeScript 类型约束)
首先通过 TypeScript 枚举和接口,统一请求优先级的定义,确保所有请求必须指定优先级,避免“无优先级”的混乱场景。
代码实现(src/types/request.ts):
/**
* 请求优先级枚举
* HIGH:核心请求(影响首屏、用户关键操作)
* MEDIUM:中等优先级(非核心但用户可见内容)
* LOW:低优先级(用户无感知、可延迟/降级的请求)
*/
export enum RequestPriority {
HIGH = 'high',
MEDIUM = 'medium',
LOW = 'low'
}
/**
* 带优先级的请求选项接口
* 扩展 AxiosRequestConfig,添加 priority 字段
*/
import type { AxiosRequestConfig } from 'axios';
export interface PriorityAxiosRequestConfig extends AxiosRequestConfig {
/** 请求优先级,必传 */
priority: RequestPriority;
/** 弱网下是否可中止,默认:LOW优先级可中止,HIGH不可中止 */
abortable?: boolean;
/** 超时时间(毫秒),默认:HIGH=5000,MEDIUM=8000,LOW=10000 */
timeout?: number;
}
作用:
- 通过枚举约束优先级取值,避免随意定义(如“high”“HIGH”“高级”等不统一的写法);
- 扩展 Axios 配置接口,强制所有请求必须传入
priority,TypeScript 会在编译阶段报错,减少 runtime 错误; - 支持自定义
abortable(是否可中止)和timeout(超时时间),实现差异化配置。
步骤2:实现优先级请求队列(控制发起顺序与并发)
弱网下若请求数量超过浏览器并发限制,需要通过“优先级队列”控制请求的发起顺序——高优先级请求先进入网络队列,低优先级请求等待或延迟发起。以下实现一个通用的优先级队列工具类。
代码实现(src/utils/requestPriorityQueue.ts):
import type { PriorityAxiosRequestConfig } from '@/types/request';
import request from '@/utils/request'; // 后续步骤会配置的 Axios 实例
import { ref } from 'vue';
// 优先级权重:用于排序(权重越高,优先级越高)
const PRIORITY_WEIGHT = {
[RequestPriority.HIGH]: 3,
[RequestPriority.MEDIUM]: 2,
[RequestPriority.LOW]: 1
};
/**
* 请求优先级队列类
* 功能:按优先级排序请求、控制并发数、弱网下中止低优先级请求
*/
export class RequestPriorityQueue {
// 队列存储:二维数组,[权重, 请求配置, 中止控制器]
private queue: [number, PriorityAxiosRequestConfig, AbortController][] = [];
// 当前并发数
private currentConcurrency = 0;
// 最大并发数(弱网下建议设为3,正常网络可设为5)
private maxConcurrency = 3;
// 网络状态(是否为弱网)
public isWeakNetwork = ref(false);
constructor() {
// 监听网络状态变化(判断是否为弱网)
this.listenNetworkStatus();
}
/**
* 监听网络状态:通过 navigator.connection 判断
* effectiveType 为 '2g'/'slow-2g' 时视为弱网
*/
private listenNetworkStatus() {
if (!navigator.connection) return; // 兼容不支持的浏览器
const updateNetworkStatus = () => {
const effectiveType = navigator.connection.effectiveType;
this.isWeakNetwork.value = ['2g', 'slow-2g'].includes(effectiveType);
// 弱网下降低最大并发数
this.maxConcurrency = this.isWeakNetwork.value ? 3 : 5;
// 弱网时,中止所有未执行的低优先级请求
if (this.isWeakNetwork.value) {
this.abortLowPriorityRequests();
}
};
// 初始判断
updateNetworkStatus();
// 网络状态变化时更新
navigator.connection.addEventListener('change', updateNetworkStatus);
}
/**
* 添加请求到队列
* @param config 带优先级的请求配置
* @returns Promise<响应数据>
*/
add<T = any>(config: PriorityAxiosRequestConfig): Promise<T> {
return new Promise((resolve, reject) => {
// 1. 补全默认配置(超时时间、是否可中止)
const completeConfig = this.completeDefaultConfig(config);
// 2. 创建中止控制器(用于弱网下中止请求)
const controller = new AbortController();
completeConfig.signal = controller.signal;
// 3. 计算权重,添加到队列
const weight = PRIORITY_WEIGHT[completeConfig.priority];
this.queue.push([weight, completeConfig, controller]);
// 4. 对队列排序(权重降序,相同权重按添加顺序)
this.queue.sort((a, b) => {
if (a[0] !== b[0]) {
return b[0] - a[0]; // 权重高的在前
} else {
return 0; // 相同权重按添加顺序
}
});
// 5. 执行队列
this.runQueue(resolve, reject);
});
}
/**
* 补全请求的默认配置
*/
private completeDefaultConfig(
config: PriorityAxiosRequestConfig
): PriorityAxiosRequestConfig {
return {
// 默认超时时间:HIGH=5s,MEDIUM=8s,LOW=10s
timeout: config.timeout || {
[RequestPriority.HIGH]: 5000,
[RequestPriority.MEDIUM]: 8000,
[RequestPriority.LOW]: 10000
}[config.priority],
// 默认是否可中止:LOW可中止,其他不可中止
abortable: config.abortable ?? config.priority === RequestPriority.LOW,
...config
};
}
/**
* 执行队列:控制并发数,发起请求
*/
private async runQueue<T = any>(resolve: (data: T) => void, reject: (err: any) => void) {
// 若当前并发数已达上限,或队列为空,直接返回
if (this.currentConcurrency >= this.maxConcurrency || this.queue.length === 0) {
return;
}
// 1. 取出队列头部的请求(优先级最高)
const [_, config, controller] = this.queue.shift()!;
this.currentConcurrency++;
try {
// 2. 发起请求(使用配置好的 Axios 实例)
const response = await request<T>(config);
resolve(response.data);
} catch (error: any) {
// 3. 错误处理:核心请求重试,非核心请求直接 reject
if (config.priority === RequestPriority.HIGH && !error.name.includes('AbortError')) {
// 核心请求重试1次
const retryResponse = await request<T>(config);
resolve(retryResponse.data);
} else {
reject(error);
}
} finally {
// 4. 请求完成后,并发数减1,继续执行队列
this.currentConcurrency--;
this.runQueue(resolve, reject);
}
}
/**
* 弱网时中止所有未执行的低优先级请求
*/
private abortLowPriorityRequests() {
this.queue = this.queue.filter(([weight, config, controller]) => {
// 仅保留高/中优先级请求,中止低优先级且可中止的请求
if (config.priority === RequestPriority.LOW && config.abortable) {
controller.abort(); // 中止请求
return false; // 从队列中移除
}
return true;
});
}
/**
* 清空队列(如页面卸载时)
*/
clear() {
this.queue.forEach(([_, __, controller]) => {
controller.abort();
});
this.queue = [];
this.currentConcurrency = 0;
}
}
// 创建全局队列实例(单例模式,避免重复创建)
export const requestQueue = new RequestPriorityQueue();
核心功能解析:
- 队列排序:通过“权重”对请求排序,高优先级请求(HIGH)始终在队列头部;
- 并发控制:弱网下最大并发数设为3,避免请求拥堵;
- 网络状态监听:通过
navigator.connection判断是否为弱网,弱网时自动中止未执行的低优先级请求; - 差异化容错:核心请求(HIGH)失败后重试1次,非核心请求不重试,减少无效网络开销;
- 单例模式:全局共享一个队列实例,确保所有请求统一调度。
步骤3:配置 Axios 实例(注入优先级标记)
在 Axios 拦截器中,为不同优先级的请求添加“优先级标记”(自定义 Header 和 Fetch 优先级),实现“前后端协同”和“浏览器原生调度”。
代码实现(src/utils/request.ts):
import axios from 'axios';
import type { PriorityAxiosRequestConfig } from '@/types/request';
import { RequestPriority } from '@/types/request';
// 创建 Axios 实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器:注入优先级标记
request.interceptors.request.use(
(config: PriorityAxiosRequestConfig) => {
// 1. 必传优先级校验(TypeScript 编译时已约束,此处为 runtime 兜底)
if (!config.priority) {
throw new Error('请求必须指定 priority(RequestPriority)');
}
// 2. 添加自定义 Header:告知后端请求优先级
config.headers['X-Request-Priority'] = config.priority;
// 3. 适配 Fetch API 的 priority 属性(若使用 Fetch 适配器)
// 注:Axios 默认使用 XMLHttpRequest,需替换为 Fetch 适配器才能支持 priority
if (config.adapter === 'fetch') {
(config as any).priority = config.priority; // 传递 priority 到 Fetch
}
// 4. 添加 Token(常规操作)
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器:统一错误处理
request.interceptors.response.use(
(response) => response,
(error) => {
// 过滤中止错误(弱网下主动中止的低优先级请求)
if (error.name === 'AbortError') {
console.log('请求已中止(低优先级,弱网环境)', error.config.url);
return Promise.reject(new Error('请求已中止(弱网优化)'));
}
// 其他错误统一处理(如 Token 过期、服务器错误)
console.error('请求错误', error);
return Promise.reject(error);
}
);
// 导出带优先级的请求方法(覆盖默认的 get/post 等方法)
export default {
get<T = any>(url: string, config: PriorityAxiosRequestConfig): Promise<T> {
return request.get<T>(url, config);
},
post<T = any>(url: string, data?: any, config: PriorityAxiosRequestConfig): Promise<T> {
return request.post<T>(url, data, config);
},
put<T = any>(url: string, data?: any, config: PriorityAxiosRequestConfig): Promise<T> {
return request.put<T>(url, data, config);
},
delete<T = any>(url: string, config: PriorityAxiosRequestConfig): Promise<T> {
return request.delete<T>(url, config);
}
};
关键配置说明:
- 自定义 Header
X-Request-Priority:后端可通过该 Header 优先处理高优先级请求(如调整后端接口的线程池优先级); - Fetch 适配器适配:若项目需要使用 Fetch API(而非 XMLHttpRequest),可通过
axios-fetch-adapter替换 Axios 适配器,从而支持priority属性; - 错误过滤:主动过滤“弱网下中止低优先级请求”的错误,避免前端错误监控误报。
步骤4:Vue3 组件中使用(结合生命周期)
在 Vue3 组件中,通过“优先级队列”发起请求,并结合 onMounted、nextTick、用户交互事件,在合适时机发起不同优先级的请求。
示例1:首页加载(核心请求优先,非核心延迟)
<template>
<div class="home-page">
<!-- 核心内容:用户信息 + 商品列表(加载中提示) -->
<Skeleton v-if="!user || !goodsList" />
<UserInfo :user="user" v-else />
<GoodsList :goods="goodsList" v-else />
<!-- 非核心内容:推荐列表(延迟加载,弱网下可能不加载) -->
<div v-if="showRecommend">
<h3>为你推荐</h3>
<RecommendList :list="recommendList" />
</div>
<!-- 统计内容:用户无感知,低优先级 -->
<div style="display: none">
<BehaviorStat page="home" />
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, nextTick } from 'vue';
import { requestQueue } from '@/utils/requestPriorityQueue';
import { RequestPriority } from '@/types/request';
import Skeleton from './components/Skeleton.vue';
import UserInfo from './components/UserInfo.vue';
import GoodsList from './components/GoodsList.vue';
import RecommendList from './components/RecommendList.vue';
import BehaviorStat from './components/BehaviorStat.vue';
// 状态定义
const user = ref<any>(null);
const goodsList = ref<any[]>([]);
const recommendList = ref<any[]>([]);
const showRecommend = ref(false);
// 页面卸载时清空队列,避免内存泄漏
onUnmounted(() => {
requestQueue.clear();
});
onMounted(async () => {
try {
// 1. 高优先级:核心请求(用户信息 + 商品列表),并行添加到队列
const userPromise = requestQueue.add<any>({
url: '/api/v1/user/current',
method: 'get',
priority: RequestPriority.HIGH
});
const goodsPromise = requestQueue.add<any[]>({
url: '/api/v1/goods/list',
method: 'get',
priority: RequestPriority.HIGH
});
// 等待核心请求完成,更新核心内容
[user.value, goodsList.value] = await Promise.all([userPromise, goodsPromise]);
// 2. 中等优先级:公告(核心内容加载完成后发起)
const announcement = await requestQueue.add<any>({
url: '/api/v1/system/announcement',
method: 'get',
priority: RequestPriority.MEDIUM
});
// 处理公告逻辑...
// 3. 低优先级:推荐列表(核心内容渲染完成后,延迟发起)
await nextTick(); // 等待核心内容DOM渲染完成
showRecommend.value = true;
recommendList.value = await requestQueue.add<any[]>({
url: '/api/v1/goods/recommend',
method: 'get',
priority: RequestPriority.LOW,
abortable: true // 弱网下可中止
});
// 4. 低优先级:行为统计(所有可见内容加载完成后发起)
await requestQueue.add({
url: '/api/v1/stat/behavior',
method: 'post',
data: { page: 'home', action: 'load', time: Date.now() },
priority: RequestPriority.LOW,
abortable: true
});
} catch (error) {
console.error('首页加载失败', error);
}
});
</script>
核心优化点:
- 请求时机分层:
onMounted早期:发起高优先级核心请求(用户信息、商品列表);- 核心请求完成后:发起中等优先级请求(公告);
nextTick后:发起低优先级的推荐列表(确保核心内容先渲染);- 最后:发起无感知的统计请求;
- 并行与串行结合:核心请求(用户信息、商品列表)并行发起,非核心请求串行延迟,平衡加载速度和资源占用;
- 页面卸载清理:
onUnmounted中调用requestQueue.clear(),中止未完成的请求,避免内存泄漏和无效请求。
示例2:商品详情页(核心请求不依赖非核心)
<template>
<div class="goods-detail-page">
<!-- 核心内容:商品详情 -->
<Skeleton v-if="!goodsDetail" />
<GoodsDetail :detail="goodsDetail" v-else />
<!-- 非核心内容:商品分类 + 推荐(延迟加载) -->
<div class="non-core-content" v-if="showNonCore">
<GoodsCategory :categories="categories" />
<RecommendList :list="recommendList" />
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { requestQueue } from '@/utils/requestPriorityQueue';
import { RequestPriority } from '@/types/request';
import Skeleton from './components/Skeleton.vue';
import GoodsDetail from './components/GoodsDetail.vue';
import GoodsCategory from './components/GoodsCategory.vue';
import RecommendList from './components/RecommendList.vue';
const route = useRoute();
const goodsId = route.params.id as string;
// 状态定义
const goodsDetail = ref<any>(null);
const categories = ref<any[]>([]);
const recommendList = ref<any[]>([]);
const showNonCore = ref(false);
onUnmounted(() => {
requestQueue.clear();
});
onMounted(async () => {
try {
// 1. 高优先级:核心请求(商品详情),立即发起,不依赖任何非核心请求
goodsDetail.value = await requestQueue.add<any>({
url: `/api/v1/goods/detail/${goodsId}`,
method: 'get',
priority: RequestPriority.HIGH,
timeout: 3000, // 核心请求超时时间缩短,快速失败重试
abortable: false // 不可中止
});
// 2. 非核心请求:分类 + 推荐,在核心详情加载完成后发起
showNonCore.value = true;
const [categoryRes, recommendRes] = await Promise.all([
// 中等优先级:分类列表
requestQueue.add<any[]>({
url: '/api/v1/goods/categories',
method: 'get',
priority: RequestPriority.MEDIUM
}),
// 低优先级:推荐列表
requestQueue.add<any[]>({
url: `/api/v1/goods/recommend?goodsId=${goodsId}`,
method: 'get',
priority: RequestPriority.LOW,
abortable: true
})
]);
categories.value = categoryRes;
recommendList.value = recommendRes;
} catch (error) {
console.error('商品详情加载失败', error);
}
});
</script>
核心优化点:
- 解除错误依赖:商品详情(核心)不依赖分类列表(非核心),独立发起,避免串行阻塞;
- 核心请求超时缩短:商品详情超时设为3秒,比默认的5秒更短,确保快速失败后重试(符合用户“等待详情的迫切需求”);
- 非核心请求并行:分类和推荐列表在核心请求完成后并行发起,减少非核心内容的加载时间。
步骤5:利用浏览器原生API(preload + Fetch priority)
对于“超核心”的请求(如首屏关键数据、用户登录状态),可结合 <link rel="preload"> 和 Fetch 的 priority 属性,进一步提升优先级,确保浏览器优先调度。
示例:首屏关键数据预加载
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Vue3 性能优化示例</title>
<!-- 预加载核心API数据:用户登录状态(超核心) -->
<link
rel="preload"
href="/api/v1/user/current"
as="fetch"
crossorigin="anonymous"
id="preload-user-info"
>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
<!-- src/App.vue(首屏根组件) -->
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { requestQueue } from '@/utils/requestPriorityQueue';
import { RequestPriority } from '@/types/request';
const user = ref<any>(null);
onMounted(async () => {
try {
// 1. 先检查 preload 的结果(若已完成,直接使用)
const preloadLink = document.getElementById('preload-user-info') as HTMLLinkElement;
if (preloadLink.readyState === 'complete') {
// 读取 preload 的响应数据
const response = await fetch(preloadLink.href);
user.value = await response.json();
console.log('使用 preload 的用户信息数据');
return;
}
// 2. 若 preload 未完成,通过队列发起高优先级请求(兜底)
user.value = await requestQueue.add<any>({
url: '/api/v1/user/current',
method: 'get',
priority: RequestPriority.HIGH,
// 使用 Fetch 适配器,支持 priority 属性
adapter: 'fetch' as any,
priority: 'high' as any // 传递给 Fetch 的 priority
});
} catch (error) {
console.error('用户信息加载失败', error);
}
});
</script>
核心优化点:
- 预加载提前发起:
<link rel="preload">在 HTML 解析阶段就发起请求,比onMounted中的请求更早,弱网下可节省数百毫秒; - 双保险机制:先检查 preload 结果,未完成则通过队列发起高优先级请求,避免预加载失败导致核心数据缺失;
- Fetch priority 加持:使用 Fetch 适配器并设置
priority: 'high',浏览器会将该请求标记为“最高优先级”,优先分配带宽。
8.1.5 代码评审要点:如何确保请求优先级落地
代码评审(Code Review)是确保“请求优先级”方案落地的关键环节。以下是针对本节内容的评审要点,可作为团队评审的 checklist。
1. 优先级分类是否清晰
- 检查点:所有请求是否指定了
priority(HIGH/MEDIUM/LOW),无“未指定优先级”的请求; - 检查方法:搜索代码中
requestQueue.add或request.get/post的调用,确认每个请求都传入了priority参数; - 标准:核心请求(如首屏关键数据、用户操作触发)必须设为
HIGH,非核心请求(如推荐、统计)设为LOW,无“核心请求设为 LOW”或“非核心请求设为 HIGH”的情况。
2. 请求时机是否合理
- 检查点:核心请求是否在
onMounted早期发起,非核心请求是否延迟发起(如nextTick后、用户交互后); - 检查方法:查看组件的
onMounted生命周期函数,确认核心请求无“等待非核心请求”的串行逻辑; - 标准:核心请求不依赖非核心请求的结果,非核心请求不阻塞核心请求的发起,无“反例2”中的错误依赖。
3. 队列与并发控制是否生效
- 检查点:是否使用
requestQueue发起请求,而非直接调用 Axios 实例;弱网下是否会中止低优先级请求; - 检查方法:
- 模拟弱网(Chrome DevTools → Network → Throttling → 2G);
- 查看控制台日志,确认低优先级请求是否被标记为“请求已中止(弱网优化)”;
- 查看 Network 面板,确认并发请求数不超过3个;
- 标准:弱网下低优先级且
abortable: true的请求会被中止,并发数控制在3以内,无“无差别并发”的情况。
4. 浏览器原生API是否利用
- 检查点:核心请求是否使用
preload或 Fetch 的priority属性; - 检查方法:
- 查看
index.html,确认首屏超核心数据是否有<link rel="preload">; - 查看 Network 面板,点击核心请求,在“Request Headers”中确认是否有
priority: high(Fetch 请求)或X-Request-Priority: high(Axios 请求);
- 查看
- 标准:超核心请求(如用户信息)使用
preload,核心 Fetch 请求设置priority: high,无“核心请求未利用原生优化”的情况。
5. 错误处理与清理是否完善
- 检查点:核心请求是否有重试机制,页面卸载时是否清空队列;
- 检查方法:
- 模拟核心请求失败(Chrome DevTools → Network → 右键请求 → Block request domain);
- 查看控制台日志,确认核心请求是否重试1次;
- 切换路由(页面卸载),查看 Network 面板,确认未完成的请求是否被中止;
- 标准:核心请求失败后重试1次,页面卸载时队列被清空,无“核心请求不重试”或“页面卸载后请求仍在执行”的情况。
8.1.6 对话小剧场:团队如何讨论请求优先级优化
场景:某电商项目迭代会议,测试反馈“弱网下首页加载慢,用户信息和商品列表延迟显示”,团队讨论解决方案。
人物:
- 小美(前端开发,负责首页):“最近测试反馈,弱网下首页要等10多秒才能看到用户信息和商品列表,推荐内容反而先出来了,这是怎么回事啊?”
- 小迪(前端开发,负责商品模块):“我看了下你首页的代码,
onMounted里同时发了5个请求,包括用户信息、商品列表、推荐、公告、统计,所有请求一起发,弱网下肯定堵了!浏览器同一域名只能6个并发,要是再加上图片、CSS,API请求就排队了,推荐这种非核心请求可能先占了连接,核心请求就卡了。” - 小稳(资深前端,技术负责人):“小迪说的对,这就是‘请求无优先级’的问题。我们之前定义过
RequestPriority枚举,你是不是没给请求加优先级?而且没用到requestQueue队列,所有请求无差别并发,弱网下肯定出问题。” - 小美:“啊,我忘了加优先级了!那现在该怎么改?”
- 小稳:“首先,你要把请求分个类:用户信息、商品列表是核心,设为
HIGH;公告是中等,设为MEDIUM;推荐、统计是非核心,设为LOW。然后用requestQueue.add发起请求,而不是直接调用 Axios。” - 大熊(后端开发):“对了,我们后端已经支持
X-Request-PriorityHeader 了,你们前端把这个 Header 带上,后端会优先处理高优先级请求,响应速度能快一点。” - 小燕(质量工程师):“我测试的时候还发现,有时候商品详情页要等分类列表加载完才显示详情,这是不是依赖错了?弱网下分类列表加载慢,详情就一直空白。”
- 小迪(负责商品详情页):“那个是我之前写的,当时想先加载分类再加载详情,没想到分类是非核心的。我现在就改,让详情请求独立发起,设为
HIGH,分类列表等详情加载完再发,设为MEDIUM。” - 小稳:“还有,首屏的用户信息可以用
<link rel="preload">预加载,HTML 解析的时候就发起请求,比onMounted早很多。弱网下requestQueue会自动中止低优先级请求,并发数控制在3个,避免拥堵。” - 小美:“那我现在就按这个方案改:给所有请求加优先级,用
requestQueue发起,核心请求在onMounted早期发,非核心延迟发,再加上 preload 预加载用户信息。改完后怎么验证呢?” - 小燕:“我来帮你验证!用 Chrome DevTools 模拟2G网络,看核心请求是不是先发起,响应时间是不是比非核心短,并发数是不是不超过3个。另外,还要测试页面切换时,未完成的请求会不会被中止,避免内存泄漏。”
- 大熊:“后端这边也会配合,优先处理
X-Request-Priority: high的请求,确保前后端一起优化,效果更好。” - 小稳:“好,那就这么定了!小美负责首页的请求优先级调整,小迪负责商品详情页的依赖解除,改完后小燕重点测试弱网场景,确保核心内容优先显示。”
8.1.7 小结
本节围绕“弱网环境下核心功能请求优先处理”展开,核心是通过“分类体系+队列管理+生命周期联动+原生API利用”,解决“请求拥堵”问题。关键要点如下:
- 原理层面:弱网下请求并发受限,需通过优先级区分核心与非核心请求,借助浏览器原生机制和队列控制提升核心请求的调度优先级;
- 实践层面:通过 TypeScript 枚举约束优先级,用
RequestPriorityQueue控制请求顺序和并发,结合 Vue3 生命周期在合适时机发起请求,利用preload和 Fetchpriority进一步优化; - 评审层面:重点检查优先级分类、请求时机、队列控制、原生API利用、错误处理,确保方案落地;

682

被折叠的 条评论
为什么被折叠?



