JavaScript性能优化系列(八)弱网环境体验优化 - 8.1 请求优先级:核心功能请求优先处理

JavaScript性能优化实战 10w+人浏览 480人参与

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)+工具类”实现完整方案。核心适配逻辑如下:

  1. 请求分类:通过 TypeScript 枚举定义优先级,约束所有请求必须指定优先级;
  2. 拦截器注入:在 Axios 拦截器中,为不同优先级的请求添加标记(如自定义 Header X-Request-Priority),或适配 Fetch 的 priority 属性;
  3. 队列管理:实现“优先级队列”工具类,控制请求的发起顺序和并发数,确保高优先级请求先执行;
  4. 生命周期联动:结合 Vue3 的 onMountednextTick 等生命周期,在合适时机发起不同优先级的请求(如核心请求在 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>

问题分析

  1. 并发数超限:浏览器对同一域名的并发TCP连接限制为6个(不同浏览器略有差异)。若首页同时发起5个API请求,再加上图片、CSS、JS等静态资源请求,会导致部分请求进入“等待队列”;
  2. 优先级倒置:非核心请求(如推荐、统计)与核心请求(用户信息、商品列表)同时发起,若非核心请求先被分配到连接,核心请求会被阻塞,导致用户“看不到个人信息和商品,却先加载出推荐内容”;
  3. 无容错机制:弱网下非核心请求若超时或失败,可能阻塞后续代码执行(如统计请求失败导致其他逻辑中断,虽示例中用了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>

问题分析

  1. 逻辑依赖错误:商品详情是用户访问该页面的核心目标,分类列表仅用于“切换分类”,二者无逻辑依赖关系,不该让核心请求等待非核心请求;
  2. 串行阻塞:弱网下分类列表请求若延迟5秒,商品详情请求会被阻塞5秒后才发起,进一步延长核心内容的加载时间;
  3. 容错缺失:若分类列表请求失败(如接口报错),会直接导致商品详情请求无法发起,用户完全看不到核心内容。

弱网下的后果:用户进入详情页后,长时间看到空白或加载中状态,分类列表和详情都无法显示,严重影响转化(如用户想买商品却看不到详情)。

反例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>

问题分析

  1. 浏览器无法识别优先级:由于未设置 priority 或自定义 Header,浏览器会按“请求发起顺序”调度,若核心请求发起稍晚(如被其他逻辑延迟),会被后续请求阻塞;
  2. 前后端无协同:后端无法识别核心请求,只能按“接收顺序”处理,弱网下若核心请求在传输过程中延迟,后端会先处理非核心请求,进一步延长核心请求的响应时间;
  3. 超时时间一刀切:所有请求统一设置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();

核心功能解析

  1. 队列排序:通过“权重”对请求排序,高优先级请求(HIGH)始终在队列头部;
  2. 并发控制:弱网下最大并发数设为3,避免请求拥堵;
  3. 网络状态监听:通过 navigator.connection 判断是否为弱网,弱网时自动中止未执行的低优先级请求;
  4. 差异化容错:核心请求(HIGH)失败后重试1次,非核心请求不重试,减少无效网络开销;
  5. 单例模式:全局共享一个队列实例,确保所有请求统一调度。
步骤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);
  }
};

关键配置说明

  1. 自定义 Header X-Request-Priority:后端可通过该 Header 优先处理高优先级请求(如调整后端接口的线程池优先级);
  2. Fetch 适配器适配:若项目需要使用 Fetch API(而非 XMLHttpRequest),可通过 axios-fetch-adapter 替换 Axios 适配器,从而支持 priority 属性;
  3. 错误过滤:主动过滤“弱网下中止低优先级请求”的错误,避免前端错误监控误报。
步骤4:Vue3 组件中使用(结合生命周期)

在 Vue3 组件中,通过“优先级队列”发起请求,并结合 onMountednextTick、用户交互事件,在合适时机发起不同优先级的请求。

示例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>

核心优化点

  1. 请求时机分层
    • onMounted 早期:发起高优先级核心请求(用户信息、商品列表);
    • 核心请求完成后:发起中等优先级请求(公告);
    • nextTick 后:发起低优先级的推荐列表(确保核心内容先渲染);
    • 最后:发起无感知的统计请求;
  2. 并行与串行结合:核心请求(用户信息、商品列表)并行发起,非核心请求串行延迟,平衡加载速度和资源占用;
  3. 页面卸载清理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>

核心优化点

  1. 解除错误依赖:商品详情(核心)不依赖分类列表(非核心),独立发起,避免串行阻塞;
  2. 核心请求超时缩短:商品详情超时设为3秒,比默认的5秒更短,确保快速失败后重试(符合用户“等待详情的迫切需求”);
  3. 非核心请求并行:分类和推荐列表在核心请求完成后并行发起,减少非核心内容的加载时间。
步骤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>

核心优化点

  1. 预加载提前发起<link rel="preload"> 在 HTML 解析阶段就发起请求,比 onMounted 中的请求更早,弱网下可节省数百毫秒;
  2. 双保险机制:先检查 preload 结果,未完成则通过队列发起高优先级请求,避免预加载失败导致核心数据缺失;
  3. Fetch priority 加持:使用 Fetch 适配器并设置 priority: 'high',浏览器会将该请求标记为“最高优先级”,优先分配带宽。

8.1.5 代码评审要点:如何确保请求优先级落地

代码评审(Code Review)是确保“请求优先级”方案落地的关键环节。以下是针对本节内容的评审要点,可作为团队评审的 checklist。

1. 优先级分类是否清晰
  • 检查点:所有请求是否指定了 priority(HIGH/MEDIUM/LOW),无“未指定优先级”的请求;
  • 检查方法:搜索代码中 requestQueue.addrequest.get/post 的调用,确认每个请求都传入了 priority 参数;
  • 标准:核心请求(如首屏关键数据、用户操作触发)必须设为 HIGH,非核心请求(如推荐、统计)设为 LOW,无“核心请求设为 LOW”或“非核心请求设为 HIGH”的情况。
2. 请求时机是否合理
  • 检查点:核心请求是否在 onMounted 早期发起,非核心请求是否延迟发起(如 nextTick 后、用户交互后);
  • 检查方法:查看组件的 onMounted 生命周期函数,确认核心请求无“等待非核心请求”的串行逻辑;
  • 标准:核心请求不依赖非核心请求的结果,非核心请求不阻塞核心请求的发起,无“反例2”中的错误依赖。
3. 队列与并发控制是否生效
  • 检查点:是否使用 requestQueue 发起请求,而非直接调用 Axios 实例;弱网下是否会中止低优先级请求;
  • 检查方法
    1. 模拟弱网(Chrome DevTools → Network → Throttling → 2G);
    2. 查看控制台日志,确认低优先级请求是否被标记为“请求已中止(弱网优化)”;
    3. 查看 Network 面板,确认并发请求数不超过3个;
  • 标准:弱网下低优先级且 abortable: true 的请求会被中止,并发数控制在3以内,无“无差别并发”的情况。
4. 浏览器原生API是否利用
  • 检查点:核心请求是否使用 preload 或 Fetch 的 priority 属性;
  • 检查方法
    1. 查看 index.html,确认首屏超核心数据是否有 <link rel="preload">
    2. 查看 Network 面板,点击核心请求,在“Request Headers”中确认是否有 priority: high(Fetch 请求)或 X-Request-Priority: high(Axios 请求);
  • 标准:超核心请求(如用户信息)使用 preload,核心 Fetch 请求设置 priority: high,无“核心请求未利用原生优化”的情况。
5. 错误处理与清理是否完善
  • 检查点:核心请求是否有重试机制,页面卸载时是否清空队列;
  • 检查方法
    1. 模拟核心请求失败(Chrome DevTools → Network → 右键请求 → Block request domain);
    2. 查看控制台日志,确认核心请求是否重试1次;
    3. 切换路由(页面卸载),查看 Network 面板,确认未完成的请求是否被中止;
  • 标准:核心请求失败后重试1次,页面卸载时队列被清空,无“核心请求不重试”或“页面卸载后请求仍在执行”的情况。

8.1.6 对话小剧场:团队如何讨论请求优先级优化

场景:某电商项目迭代会议,测试反馈“弱网下首页加载慢,用户信息和商品列表延迟显示”,团队讨论解决方案。

人物

  • 小美(前端开发,负责首页):“最近测试反馈,弱网下首页要等10多秒才能看到用户信息和商品列表,推荐内容反而先出来了,这是怎么回事啊?”
  • 小迪(前端开发,负责商品模块):“我看了下你首页的代码,onMounted 里同时发了5个请求,包括用户信息、商品列表、推荐、公告、统计,所有请求一起发,弱网下肯定堵了!浏览器同一域名只能6个并发,要是再加上图片、CSS,API请求就排队了,推荐这种非核心请求可能先占了连接,核心请求就卡了。”
  • 小稳(资深前端,技术负责人):“小迪说的对,这就是‘请求无优先级’的问题。我们之前定义过 RequestPriority 枚举,你是不是没给请求加优先级?而且没用到 requestQueue 队列,所有请求无差别并发,弱网下肯定出问题。”
  • 小美:“啊,我忘了加优先级了!那现在该怎么改?”
  • 小稳:“首先,你要把请求分个类:用户信息、商品列表是核心,设为 HIGH;公告是中等,设为 MEDIUM;推荐、统计是非核心,设为 LOW。然后用 requestQueue.add 发起请求,而不是直接调用 Axios。”
  • 大熊(后端开发):“对了,我们后端已经支持 X-Request-Priority Header 了,你们前端把这个 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利用”,解决“请求拥堵”问题。关键要点如下:

  1. 原理层面:弱网下请求并发受限,需通过优先级区分核心与非核心请求,借助浏览器原生机制和队列控制提升核心请求的调度优先级;
  2. 实践层面:通过 TypeScript 枚举约束优先级,用 RequestPriorityQueue 控制请求顺序和并发,结合 Vue3 生命周期在合适时机发起请求,利用 preload 和 Fetch priority 进一步优化;
  3. 评审层面:重点检查优先级分类、请求时机、队列控制、原生API利用、错误处理,确保方案落地;
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值