哥们页面多接口处理思考

199 阅读6分钟

写给自己,个人对页面接口太多是否会影响性能得一个思考和自己得解决方案,我的兄弟,你可能觉得没有什么意义罢了🤦‍♂️🤦‍♂️🤦‍♂️


src=http___img3.doubanio.com_view_richtext_large_public_p100009607.jpg&refer=http___img3.doubanio.webp

铺垫 😊

我有个 Dashboard 页面,大约 7 个接口请求,页面被拆成 6 个子组件。每个子组件在自己的 useEffect 里独立发请求——从业务角度来看这些请求本质上是并行的。我在思考要不要把它们统一合并到 store 层(用 Promise.all 并行),以期望获得性能上的提升。实践中我选择了混合策略:把无参数、稳定、页面初始需要的数据合并到 store 并行获取(Promise.all),把按参数、按交互、依赖上游数据的请求留给组件自身 useEffect(按需、串联或防抖)。

image.png

image.png


为什么这是个值得深思的问题 🤔

  • 并行请求听起来简单,但还要考虑:浏览器并发连接限制(HTTP/1.1 下同域大概 ~6 条),服务端/代理能力,接口间依赖关系,失败策略,用户体验(是否能优雅降级)等。
  • HTTP/2/3 大幅缓解了连接限制,但并不等于可以随意开大量小请求(还有渲染阻塞、带宽、CPU 等因素)。
  • Promise.all 优雅,但单点失败会让整组数据被判为失败(可用 allSettled 或单独的错误处理来缓解)。

我思考得问题代码(我是react代码):

//并行请求
useEffect(() => {
  fetchData1();
  fetchData2();
  fetchData3();
}, []);
//并行请求
useEffect(() => {
  const fetchAll = async () => {
    const [res1, res2, res3] = await Promise.all([
      fetchData1(),
      fetchData2(),
      fetchData3(),
    ]);
    console.log(res1, res2, res3);
  };
  fetchAll();
}, []);

try {
  const [res1, res2, res3] = await Promise.all([...]);
} catch (err) {
  console.error("其中一个请求出错:", err);
}

//串行串联请求得
useEffect(() => {
  const fetchAll = async () => {
    await fetchData1(); // 等1完成
    await fetchData2(); // 再发2
    await fetchData3(); // 最后发3
  };
  fetchAll();
}, []);

  • 这几个接口之间是否存在串联关系,请求1是否需要等待请求2
  • 接口之前得请求参数是否需要单独维护
  • 是否需要统一错误状态处理和loading状态

对上述请求得方案处理步骤分析

本喵给你分析分析.gif

  • 我的每个接口各自独立,互不干扰,可以纯并联

tips 😉 并联请求,需要查看HTTP 协议版本(HTTP Version),如果说是http/1.1,大多数浏览器默认是同时并发能请求6个请求,多余得会进行排队,如果说是HTTP/2,理论上是可以并发无数个

开启 Protocol 列步骤(Chrome)

  1. 打开 Chrome 开发者工具 (F12) → Network 面板
  2. 在请求列表表头空白处 右键
  3. 在弹出的列选项里勾选 Protocol

image.png

  • 我的接口存在参数各自维护情况,我想部分组件实现内部自己维护自己得请求
  • 我的接口部分需要统一得错误处理和loading状态

清楚我自己得需求后就是我的代码部分了

我在项目里的关键代码(已简化,只保留关键性代码) 😎

哥们得数据统一放在react-redux里面得仓库进行统一处理得,用了多个extraReducers

页面级合并并行请求(我用的 Promise.all

export const fetchDashboardAll = createAsyncThunk(
  "dashboard/fetchAll",
  async (_, { rejectWithValue }) => {
    try {
      const [dashboardRes, behaviorRes, subscriptionRes] = await Promise.all([
        getDashboard(),
        getFunctionalBehavior(),
        getAnalyseSubscription(),
      ]);
      return {
        dashboard: dashboardRes.data,
        behavior: behaviorRes.data,
        subscription: subscriptionRes.data,
      };
    } catch (err: any) {
      return rejectWithValue(err.message || "请求失败");
    }
  }
);

时间范围与初始 state(简化)

export const sevDaysAgoStart = dayjs().tz(TZ).subtract(7, "day").startOf("day").unix();
export const todayEnd = dayjs().tz(TZ).endOf("day").unix();

const initialState: InitialState = {
  loading: false,
  error: null,
  dashboard: null,
  behavior: null,
  funnel: null,
  subscription: null,
  params: { page: 1, page_size: 10, begin_timestamp: sevDaysAgoStart, end_timestamp: todayEnd },
  table: { status: "init", data: undefined },
};

表格请求(单独 thunk)

export const getListAsync = createAsyncThunk(
  "dashboard/getListAsync",
  async (params) => {
    const res = await getAnalyseRegionalDistribution(params);
    return { data: res, params };
  }
);

串联请求示例(单独获取漏斗,按日期)

export const getFunnelAsync = createAsyncThunk(
  "dashboard/getFunnelAsync",
  async (params: { date_ymd: string }, { rejectWithValue }) => {
    try {
      const res = await getAnalyseFunnel(params);
      return res.data;
    } catch (err: any) {
      return rejectWithValue(err.message || "请求失败");
    }
  }
);

slice 中对并行请求的状态处理(简化)

builder
  .addCase(fetchDashboardAll.pending, (state) => { state.loading = true; state.error = null; })
  .addCase(fetchDashboardAll.fulfilled, (state, action) => {
    state.loading = false;
    state.dashboard = action.payload.dashboard;
    state.behavior = action.payload.behavior;
    state.subscription = action.payload.subscription;
  })
  .addCase(fetchDashboardAll.rejected, (state, action) => { state.loading = false; state.error = action.payload as string; });

Keep 组件关键请求逻辑(只保留要点)

useEffect(() => {
  const newParams = {
    ...params,
    begin_timestamp: sevDaysAgoStart,
    end_timestamp: todayEnd,
    zone: "ALL",
  };
  dispatch(changeParams(newParams));
  dispatch(getListAsync(newParams));
}, []);

// Select 切换:某个选项触发单独请求(按需)
<Select
  value={params.user_type}
  onChange={(user_type) => {
    dispatch(changeParams({ user_type }));
    dispatch(getListAsync({ ...params, user_type }));
    if (user_type === "3") {
      getAnalyseRetention().then((res) => setMonthData(res.data));
    }
  }}
  options={[...user_type, { label: "月留存", value: "3" }]}
/>

判断并发 vs 串联的决策流程(实战模板)

按下面几步问自己,结果会比较稳妥:

  1. 接口是否相互依赖?

    • 是 -> 串联(await a(); await b(aResult)
    • 否 -> 继续下一步
  2. 是否是页面初始必需且无参数(或参数固定)?

    • 是 -> 可以考虑合并到 store,用 Promise.all 并行(统一 loading、缓存、去重)
    • 否 -> 交给组件按需请求(useEffect
  3. 是否能容忍部分失败?

    • 不能 -> Promise.all 并配合错误回退策略(或先探测接口稳定性)
    • 能 -> Promise.allSettled 或单独处理每个接口结果
  4. 是否支持 HTTP/2/3?

    • 支持 -> 并行开多个请求代价小得多(多路复用)
    • 不支持 -> 小心超过浏览器连接数导致排队

具体可落地的优化建议(checklist)

  • ✅ 优先确定后端是否支持 HTTP/2/3(多路复用能显著改善并发)
  • ✅ 对无参数 / 常用数据放在 Redux(或缓存层),避免重复请求
  • ✅ 使用 AbortController 在组件卸载或参数变更时取消请求
  • ✅ 对非关键接口允许降级展示(失败不阻塞主视图)
  • ✅ 对批量小接口,考虑后端做批量合并 API(一个请求拿到多个数据)
  • Promise.allSettled 在容忍部分失败时非常好用
  • ✅ 搜索/筛选类请求做防抖(减少无意义并发)
  • ✅ 区分全局 loading卡片级 loading(避免整页灰屏)

常见误区(别踩)

  • Promise.all 总比单独请求快” —— 不一定,连接竞争、后端吞吐、HTTP 版本都会影响实际效果。
  • “支持 HTTP/2 就不需要做任何优化” —— 仍需考虑缓存、批量、UI 渲染优先级等。
  • “把所有请求合并就能减少一切问题” —— 合并会牺牲组件自治、增加单点失败风险,并可能增加首屏等待时间。

我最终的实践(一句话)

混合策略:把那些 无参数、页面初次一定要的、且多个子组件可能复用 的接口合并到 fetchDashboardAll(并行);把按需、参数依赖、用户交互触发 的请求留在各自组件的 useEffect(或串联)。如果必须串联的流程,一律 await 串联处理。


结语

最后知道真相的我眼泪掉下来.gif

优化网络请求,其实像减肥:不是把所有东西都饿掉,而是学会怎么吃,什么时候吃,吃多少。并发请求不是万能药,串联请求也不是禁忌,关键是看你的数据依赖、用户体验需求和后端能力。

最后一句老生常谈但很管用:别慌,先想清楚数据依赖和使用场景,再去动 Promise.all

其实我也不知道我自己写了啥,我想的就是自己得一个思考方式吧,首先就是页面存在很多个接口,是否存在串联请求,请求1依赖请求2,其次就是是否需要把其它得并行请求统一放到promise.all进行处理,promise支持统一错误处理和loading状态,更健壮,然后就是是否需要组件内部自己维护自己得参数,让代码看起来不复杂,然后就是http1,http2得请求协议得认识 🤐🤐🤐🤐🤐🤐🤐🤐🤐🤐🤐🤐