阅读视图

发现新文章,点击刷新页面。

PD 分离推理的加速大招,百度智能云网络基础设施和通信组件的优化实践

为了适应 PD 分离式推理部署架构,百度智能云从物理网络层面的「4us 端到端低时延」HPN 集群建设,到网络流量层面的设备配置和管理,再到通信组件和算子层面的优化,显著提升了上层推理服务的整体性能。

百度智能云在大规模 PD 分离式推理基础设施优化的实践中,充分展现了网络基础设施、通信组件与上层业务特征深度融合的重要性。


01 PD分离式推理服务对网络的需求

传统的推理服务均是集中式,大多是单机部署。即使是多机部署,机器规模也非常小,对网络的带宽和时延需求都不大。当前大规模 PD 分离式推理系统来说,对网络通信的需求则发生了变化:

  • 引入大规模的 EP 专家并行。EP 会从单机和双机的小规模,变成更大规模,因此 EP 之间的「 Alltoall 通信域」成倍增长。这对于网络基础设施、Alltoall 算子等的通信效率都提出了更高的要求,它们会直接影响 OTPS、TPOT 等指标,从而影响最终的用户体验。

  • PD 分离式部署,Prefill 和 Decode 之间会有 KV Cache 流量的传输,KV Cache 通信的时延直接影响推理服务整体的性能。

为了提升大规模 PD 分离式推理系统的效率,百度智能云针对性地优化了网络基础设施和通信组件:

  • 物理网络层面:为适配 Alltoall 流量专门建设了「4us 端到端低时延」 HPN 集群,支持自适应路由功能彻底解决网络哈希冲突,保证稳定的低时延。

  • 流量管理层面:优化 Alltoall 多打一 incast 流量导致的降速问题。对 HPN 网络中训练、推理等不同类型流量进行队列管理,实现训推任务的互不干扰。通过自研高性能 KV Cache 传输库实现 DCN 弹性 RDMA 满带宽传输。

  • 通信组件层面:Alltoall 算子优化,相比开源的方案,大幅提升 Prefill 和 Decode 的 Alltoall 通信性能。针对 batch size 级别的动态冗余专家编排,将专家均衡度控制在了 1.2 以下,确保集群中所有 GPU 通信时间大致相同。优化双流,实现最大程度的计算和通信 overlap,整体提升 20% 吞吐。

下面我们逐一介绍百度智能云在以上几个层面的优化实践。

02 解决方案和最佳实践

2.1 建设适配的 HPN 网络设施

百度智能云在训练场景下的 HPN 网络架构设计已经有着丰富的经验,AIPod 使用多导轨网络架构,GPU 服务器配有 8 张网卡,然后每张网卡分别连到一个汇聚组的不同 LEAF 上。在 LEAF 和 SPINE 层面,通过 Full Mesh 的方式进行互联。

以下图为例,考虑一个训练场景下的 3 层架构 HPN 网络:

图片

2.1.1 训练和推理任务的流量特征

  • 非 MoE 训练任务的流量特征

在传统非 MoE 的训练场景下,跨机通信产生的流量大多数都是同号卡流量。例如在梯度同步时候产生的 AllReduce 或者 ReduceScatter 或者 AllGather,PP 之间的 SendRecv 等。同号卡通信最佳情况可以只经过一跳,以上图为例,每个 LEAF 交换机有 64 个下联口,因此 64 台服务器规模同号卡通信理论上可以做到一跳可达。

规模再大,就只能经过 SPINE 或者最差经过 SUPER SPINE 来进行通信。为了减少流量上送 SPINE,百度百舸在任务调度的时候会自动进行服务器的亲和性调度。在创建任务的时候,尽量把同一通信组下的 Rank 排布在同一 LEAF 交换机下的服务器内,那么理论上大部分流量都可以收敛在 LEAF 下。

  • MoE 推理流量特征

对于推理服务来说,MoE EP 之间的 Alltoall 通信流量模式与 AllReduce 等不同,会产生大量的跨导轨流量。虽然对于 Prefill 阶段来说,可以通过软件实现层面规避掉跨导轨的流量,但是 Decode 阶段仍然无法避免跨导轨,这会导致多机之间的通信不只是同号卡通信,跨机流量大部分并不能一跳可达,会有大量的流量上到 SPINE 或者 SUPER SPINE,从而导致时延增加。

  • MoE 训练流量特征

对于 MoE 训练的流量会更加复杂,综合了训练和推理的流量特征,既存在传统的梯度同步产生的 AllReduce 或者 ReduceScatter 或者 AllGather,PP 之间的 SendRecv,也存在 EP 之间的 Alltoall 流量。这些流量不但会出现跨导轨传输的问题,他们之间可能会存在 overlap 导致互相干扰。

2.1.2 面向 EP 的 HPN 架构优化

鉴于 Alltoall 通信的特点,我们在设计 HPN 网络的时候,会考虑优先保证跨导轨流量至多 2 跳可达,让 Alltoall 流量收敛到 SPINE 层,以这种方式尽量减少跨导轨的通信时延。如下图所示:

图片

LEAF 层所有设备全部有一根线接入同一台 SPINE 交换机,这样可以让集群内 Alltoall 跨导轨流量全部收敛到 SPINE 层,跨导轨通信时延可以进一步从 5us+ 缩小为 4us。

这种经过优化后的 HPN 网络架构,能接入的卡数主要取决于交换机芯片支持的最大的下联口有多少。虽然对于超大模型的训练任务来说,这个集群规模可能不够,但是对于推理来说,通常不需要那么大规模的机器,是可以满足需求的。

2.1.3 自适应路由彻底消除 hash 冲突

同时,由于 Alltoall 通信流量的特征,LEAF 到 SPINE 之间的通信流量会成为常态。当流量需要通过 SPINE 传输的时候,会由 hash 选择 SPINE 出口的过程,这时候有可能会产生 hash 冲突,导致网络抖动。因此为了避免 hash 冲突,百度智能云基于自研交换机实现自适应路由。如下图所示:

图片

假设 A 和 C 进行 Alltoall 跨导轨通信,A 发出的流量必然要经过 SPINE,那么流量在到达 LEAF 的时候,会基于 packet 做 hash,并结合链路的负载情况动态选择最优的出口,将报文发送到多个 SPINE 上。

基于报文 hash 到不同的物理路径,百度智能云实现了链路负载均衡,消除因 hash 冲突时延增加导致的性能抖动,实现稳定的低时延网络。

详情可参考:彻底解决网络哈希冲突,百度百舸的高性能网络 HPN 落地实践

2.2 流量的管理和优化

2.2.1 避免 incast 造成降速,不同类型流量的分队列管理

  • Alltoall 多打一,不合理的配置造成降速

在推理服务中,EP 间的 Alltoall 通信流量特性与传统训练中的 AllReduce 完全不同,网络上多打一造成的 incast 流量非常常见。这种 incast 的严重程度会随着规模的增大而增大。incast 流量的突发,可能会造成接收侧网卡上联的交换机端口向发送侧反压 PFC,导致网络降速。

传统 Alltoall 流量多打一的示意图如下,假设机器 A 和机器 C 的 GPU0、GPU2、GPU4、GPU6 都需要给机器 B 的 GPU0 发送数据,那么在网络上就会出现 8 打 1 的情况。

图片

传统的 Alltoall 实现,例如 PyTorch 内部调用的 Alltoall,是使用 send recv 去实现的,如果使用 PXN 可以缩小网络上的发生多打一的规模,但是多打一依然存在,如下图所示:

图片

因此无论 Alltoall 的算子实现方式如何,网络上的多打一都无法避免。此时如果网络侧的拥塞控制算法的配置不合理,对拥塞过于敏感,就会产生降速,进而对整体吞吐造成影响。

  • 推理训练任务中非 Alltoall 流量的干扰

除此之外,如果集群内还存在其他流量,例如训练任务 DP(数据并行)之间的 AllReduce 或者 ReduceScatter 或者 AllGather,或者 PD(Prefill-Decode)之间的 KV Cache 传输,也会对 Alltoall 的流量造成影响,从而进一步降低推理引擎的整体吞吐。

因此无论是端侧网卡的配置,或者是交换机的配置,都需要针对 Alltoall 这种多打一 incast 流量做针对性优化,同时尽量避免集群内其他流量对 Alltoall 流量造成影响。

针对这种情况,我们给出的解决方案如下:

  • 在队列管理层面,通过端侧网卡将 EP 的流量做专门的优先级配置,将 Alltoall 流量导入到高优先级队列。其他训练的流量,比如 AllReduce 等使用低优先级队列。

  • 在资源层面,在端侧网卡和交换机的高优先级队列上,预留更多的 buffer,分配更高比例的带宽,优先的保证高优先级队列的流量。

  • 在拥塞控制算法配置层面,高优先级队列关闭 ECN 标记功能,让 DCQCN 算法对 Alltoall 流量微突发造成的拥塞不做出反应,从而解决 incast 问题造成的降速。

在经过端侧网卡和网侧交换机配合调整后,可以保障 Alltoall 通信流量的通信带宽和传输时延,实现训推任务的互不干扰,并有效的缓解 incast 流量带来的非预期的降速而造成的性能抖动。

经过测试,在我们部署的推理服务中,Alltoall 过程的整体通信时延有 5% 的降低。

2.2.2 DCN 支持弹性 RDMA 实现 KV Cache 满带宽传输

在 PD 分离式推理系统中,还存在 PD 之间 KV Cache 传输的流量。相比 Alltoall 虽然他的带宽需求不算大,但为了避免二者的流量互相干扰,通常我们会让 KV Cache 的传输流量单独走 DCN 网络,使其与 Alltoall 的流量完全隔离开。

图片

在 DCN 网络的设计上,为了保证 KV Cache 流量的传输带宽,其网络架构收敛比采用 1:1。端侧网卡支持弹性 RDMA,使用 RDMA 协议保证 KV Cache 的高性能传输。

在传输库层面,百度智能云使用自研的高性能 KV Cache RDMA 传输库,其接口设计与框架层深度定制,支持上层框架分层传输,也支持多层 KV Cache 的批量传输,便于在框架层做计算与传输的 overlap。

通过以上设计优化,KV Cache 传输在主网卡可以用满带宽传输时间可以完全被计算 overlap 住,不成为推理系统的瓶颈。

2.3 提高推理服务组件的网络通信效率

在有了高带宽低时延的网络基础设施的基础上,如何用好网络基础设施,是推理服务软件层面需要重点考虑的事情。

在我们对 PD 分离推理服务的 profile 分析当中,发现了一些影响网络通信效率的关键因素。

2.3.1 Alltoall 算子的通信效率

目前社区开源的 DeepEP 已经给出了推理系统中 dispatch 和 combine 过程的 Alltoall 高性能的算子的实现,且性能表现优异。

对于 Prefill 来说,由于输入的 batch size 较大,Alltoall 通信算子的同号卡传输阶段为了平衡显存资源和性能,采用分 chunk 传输的方式,发送和接收会循环使用一小块显存,并对每次 RDMA 发送以及机内 NVLink 传输的 token 数做了限制。

通过实际观测网卡的传输带宽,发现其并没有被打满。在此基础上,我们对网络传输的显存的大小,以及每一轮发送接收的最大 token 数等配置,针对不同的 GPU 芯片,做了一些精细化的调整,使之在性能上有进一步的提升。通过优化,DeepEP 的传输性能有大概 20% 的性能提升,网络带宽已经基本被打满。

对于 Decode 来说,DeepEP 的实现是多机之间的 EP 通信,不区分机内和机间,一律采用网络发送。这样做的考虑是为了机内传输也不消耗 GPU 的 SM 资源,完成网络发送后算子即可退出。在网络传输的时间内做计算,完成后再调用 Alltoall 的接收算子,以此来实现计算和通信的 overlap。但这样做的缺点是机内的 NVLink 的带宽并没有被高效的利用起来,网络传输的数据量会变大。

因此,百度智能云通过在 GPU 算子内使用 CE 引擎做异步拷贝,在不占用 GPU SM 资源的情况下,也能实现机内 NVLink 带宽的高效利用,同时不影响计算和通信的 overlap。

2.3.2 动态冗余专家编码,保证 EP 负载均衡

EP 专家之间如果出现处理 token 不均衡的情况,将会导致 Alltoall 通信算子的不同 SM 之间,以及不同 GPU 的通信算子之间,出现负载不均的情况,导致的结果就是整体通信时间会被拉长。

由于 EP 专家之间的负载均衡是推理服务引擎提升吞吐的非常重要的一环,经过百度智能云的大规模的线上实践的经验来看,静态冗余专家并不能很好的保证专家均衡。因此我们专门适配了针对 batch size 级别的动态冗余专家,把专家均衡度(max token/avg token)基本控制在了 1.2 以下,不会出现明显的「快慢卡」的情况

2.3.3 极致优化双流效果,整体吞吐进一步提升

通信和计算 overlap,隐藏通信的开销,一直都是推理服务,或者大模型训练中,非常重要的课题。

在百度智能云的实践中,我们在线上大规模的推理服务中开启了双流。为了尽量隐藏掉通信的开销,达到最好的 overlap 的效果,除了做 EP 之间的专家均衡以外,对计算算子也做了针对性的优化,例如对计算算子和通信算子 kernel launch 的顺序做合理排布,对二者所需的 SM 资源做合理的分配,避免出现计算算子占满 SM 导致通信算子 launch 不进去的情况,尽可能的消灭掉 GPU 间隙的资源浪费。通过这些优化,整体的吞吐可以提升 20% 以上。

03 总结

百度智能云在大规模 PD 分离式推理基础设施优化的实践中,充分展现了网络基础设施、通信组件与上层业务特征深度融合的重要性。这种融合不仅是技术层面的创新,更是对实际业务需求的深刻理解和响应。

vue3中shallowRef有什么作用?

在 Vue3 里,shallowRef是一个很实用的函数,其作用是创建一种特殊的 ref 对象,它只对自身的值变化进行追踪,而不会递归追踪内部属性的变化。下面详细介绍它的功能和适用情形。

主要功能

  • 浅层响应性shallowRef仅对.value的赋值操作作出响应,而内部对象的属性变化不会触发更新。
  • 性能优化:在处理大型数据结构或者第三方对象(像 DOM 元素、API 响应数据)时,若无需追踪内部变化,使用它能避免不必要的响应式开销。
  • 保持原始对象:它不会像ref那样对深层对象进行代理转换,有助于维持对象的原始状态。

典型应用场景

  1. 缓存大型数据
    当你有大量静态数据,且不需要监听其变化时,可以使用shallowRef来避免性能浪费。
const largeData = shallowRef(getLargeDataFromAPI()); // 数据更新时才需手动触发更新
  1. 集成第三方库
    在集成第三方库时,使用shallowRef可以存储库返回的实例,防止 Vue 对其进行不必要的代理。
const map = shallowRef(null);

onMounted(() => {
  map.value = new MapLibreGL.Map(...); // 存储原生DOM或库实例
});
  1. 手动控制更新
    如果你希望手动控制更新时机,以减少渲染次数,shallowRef是个不错的选择。
const state = shallowRef({ count: 0 });

function increment() {
  state.value.count++; // 不会触发更新
  nextTick(() => {
    forceUpdate(); // 手动触发更新
  });
}

与普通 ref 的差异

特性 ref shallowRef
深层响应性 具备 不具备
对象代理 进行代理转换 保持原始对象
触发更新的方式 对象属性变化会触发更新 .value赋值操作会触发更新

手动触发更新的方法

如果使用了shallowRef,但又需要在内部属性变化时触发更新,可以采用以下方法:

// 方法一:替换整个value
state.value = { ...state.value, count: state.value.count + 1 };

// 方法二:结合triggerRef强制更新
import { shallowRef, triggerRef } from 'vue';

const state = shallowRef({ count: 0 });

function increment() {
  state.value.count++;
  triggerRef(state); // 手动触发更新
}

使用建议

  • 当你需要处理大型数据结构,并且不需要追踪内部变化时,优先考虑使用shallowRef

  • 存储第三方实例(如 DOM、Canvas、Map 等)时,shallowRef是很好的选择。

  • 如果需要响应式地追踪内部变化,应使用普通的refreactive

通过合理运用shallowRef,可以在 Vue 应用中实现更精准的响应式控制,从而优化性能。

后端接口传来的值可能为undefined怎么办,快使用?. (可选链操作符)

我们在实际开发时候经常会遇见赋值,但是后端可能为传一个undefined的场景,比如后端传了一个obj.data.name1,但是直接接收赋值可能为为undefined,那前端怎么校验呢

可选链操作符

const token = memberStore.profile?.token

注意这个 ?. 就是可选链运算符

在前端代码里,?. 是可选链操作符,其用途是防止因访问可能不存在的属性而引发错误。

在这段代码里:  const token = memberStore.profile?.token,这里使用可选链操作符有以下几个原因:

防止出现 Cannot read property 'token' of undefined 错误

当 memberStore.profile 的值为 undefined 或者 null 时:

  • 若使用普通的点号访问(memberStore.profile.token),程序会抛出错误,进而使后续代码无法继续执行。
  • 而使用可选链操作符(memberStore.profile?.token),表达式会直接返回 undefined,不会中断程序的运行。

适配异步数据加载场景

在实际的应用当中,memberStore.profile 可能需要通过异步方式获取,例如在用户登录之后才会有值。使用可选链操作符可以确保:

  • 在数据尚未加载完成时,不会因为访问 profile.token 而导致页面崩溃。
  • 能够顺利处理用户未登录的情况,此时 profile 可能为 undefined

示例对比

下面通过代码示例来说明两者的区别:

// 假设 memberStore.profile 未定义

const token1 = memberStore.profile.token;        // 抛出错误:Cannot read property 'token' of undefined 
const token2 = memberStore.profile?.token;       // 返回 undefined,不会报错

常见应用场景

可选链操作符在以下这些场景中经常会用到:

  1. 异步数据加载:像接口返回的数据结构可能不完整的情况。
  2. 条件渲染:在组件中需要处理未定义的 props。
  3. 深度嵌套对象:避免因为中间某个属性不存在而引发错误。

结合空值合并操作符

通常,可选链操作符会和空值合并操作符 ?? 一起使用,以便为未定义的值设置默认值:

const token = memberStore.profile?.token ?? ''; // 若 token 为 undefined,则默认赋值为空字符串

总结

在你的代码里使用 ?. 是一种安全的编程实践,它能够优雅地处理数据可能不存在的情况,增强代码的健壮性,同时避免因未定义的属性访问而导致应用崩溃。

优化了盟友几行代码,硬要请我喝咖啡

前些日子对接过联盟一个帮派的 Java web 项目,对接人王道友提过好几次,说该项目的用户登录校验经常失灵,明明登录过了,刷新页面后又时常会跳去登录页,一直未找到原因,甚是困扰。

话已至此,一向行侠仗义的我决定趟一趟这浑水。

image.png

问题分析

由于不熟悉项目,首先让王道友介绍了项目登录的主流程,得到如下流程图。

image.png

随后花了点时间在问题复现上,最终定位到了核心代码,321上代码

 let userCheck = {
    isRunning: false,
    interval: 30,
    checkSessionUrl: "https://xxx/user/checkSession",
    returnUrl: null,
    userGuid: null,
    timer: null,

    close: function () {
      clearTimeout(timer);
      this.isRunning = false;
    },
    open: function () {
      if (this.isRunning === true) {
        return;
      }

      this.isRunning = true;
      let container = document.getElementById("userCheckContainer");
      if (!container) {
        container = document.createElement("div");
      }
      container.id = "userCheckContainer";
      container.style.display = "none";
      document.body.appendChild(container);
      this.returnUrl = this.removeQueries(window.location.href);
      // 问题所在
      window.addEventListener(
        "message",
        function (event) {
          this.userGuid = event.data.userGuid;
        },
        false
      );

      this.refreshFrame();
    },
    // 获取当前登录用户guid,当前未登录用户则返回空字符串
    getUserGuid: function () {
      this.close();
      this.open();
      return this.userGuid;
    },
    refreshFrame: function () {
      let frame = document.createElement("iframe");
      frame.id = "userCheckFrame";
      frame.src = `${this.checkSessionUrl}?returnUrl=${encodeURIComponent(
        this.returnUrl
      )}`;
      frame.sandbox = "allow-same-origin allow-scripts allow-forms";

      let container = document.getElementById("userCheckContainer");
      if (container) {
        container.innerHTML = "";
        container.appendChild(frame);
      }

      if (this.isRunning) {
        this.timer = setTimeout(
          () => this.refreshFrame(),
          this.interval * 1000
        );
      }
    },
    removeQueries: function (url = "") {
      let idx = url.indexOf("?");
      if (idx < 1) {
        return url;
      }
      return url.substring(0, idx - 1);
    },
  };

  getUserInfo().then((userInfo) => {
    const newGuid = userUtils.getUserGuid();
    if (userInfo.guid !== newGuid) {
      // 跳去登录页
    }
  });

眼尖的道友估计一眼就看出了端倪,核心问题在于用户一致性校验环节的异步流程错乱。
userCheck 行 28-34 的 message 事件回调是异步给 userGuid 赋值的,而行 39-42 getUserGuid 却以同步的方式返回结果。
行 75 执行getUserGuid时,只有当 message 回调在行 76 执行前返回结果,登录校验流程才能正常,否则校验异常跳去登录页。

妙啊!原来是玄学编程,有缘者登录之。

企业微信截图_20250520130713.png

话不多说,开始设计解决方案吧。

解决方案

方案设计方向是把上述用户一致性校验环节的异步流程理顺。
源代码流程是动态创建 iframe 并监听其 message 回调,并以轮询方式重试,存在的问题有:

  • 同步异步时序错乱(核心问题,上文分析过)
  • 未考虑 iframe 加载异常场景、未设置 refreshFrame 轮询上限,可能进入死循环
  • 未校验 message 消息合法性,可能拿到错误信息
  • 未正常处理 getUserId 多次调用场景

现在以异步编程的方式重新设计下流程:

image.png

编码实战

基于上述流程上代码

// 执行程序单例
let processPromise: Promise<string> | null = null;
// 副作用池
const sideEffectPools: Function[] = [];

function log(...args: any[]) {
  console.log(`[AuthCheck]`, ...args);
}
// 创建iframe容器
function createCheckFrame(url: string, returnUrl = window.location.href) {
  const frameId = "userCheckFrame";
  let frame = document.createElement("iframe");
  const frameAttrs = {
    id: frameId,
    src: `${url}?returnUrl=${encodeURIComponent(returnUrl)}`,
    sandbox: "allow-same-origin allow-scripts allow-forms",
    style: "display: none;position: absolute;",
  };
  Object.entries(frameAttrs).forEach(([key, val]) => {
    frame.setAttribute(key, val);
  });

  // 销毁iframe容器
  sideEffectPools.push(() => {
    frame.parentNode?.removeChild(frame);
    frame = null; // 解除引用
  });
  return frame;
}

// iframe 加载异常
function frameErrorPromise(frame: HTMLIFrameElement) {
  return new Promise((_, reject) => {
    frame.onerror = () => {
      reject(new Error("iframe加载错误"));
    };
  });
}

// iframe 消息回调
function frameMessagePromise<T>(
  validator?: (evt: MessageEvent, next: Function) => void
) {
  return new Promise<T>((resolve) => {
    const messageCallback = (event: MessageEvent) => {
      if (typeof validator === "function") {
        validator(event, resolve);
        return;
      }
      resolve(event.data);
    };
    window.addEventListener("message", messageCallback, false);

    // 清理事件监听
    sideEffectPools.push(() => {
      window.removeEventListener("message", messageCallback);
    });
  });
}
// 超时 Promise
function frameTimeoutPromise(ms = 1000, err?: string) {
  return new Promise((_, reject) => {
    let timer = setTimeout(() => reject(new Error(err)), ms);
    // 清理计时器
    sideEffectPools.push(() => {
      clearTimeout(timer);
      timer = null;
    });
  });
}
// 清理副作用
function cleanSideEffects() {
  while (sideEffectPools.length) {
    const sideEffect = sideEffectPools.pop();
    sideEffect?.();
  }
}
// 获取用户基本信息
function _getCurrentUser(url: string, timeout = 5 * 1000) {
  cleanSideEffects();

  const frame = createCheckFrame(url);
  const promise = Promise.race([
    frameMessagePromise((evt: MessageEvent, next: Function) => {
      log("messageCallback:", event.origin, event.data);
      const host = new URL(url).origin;
      // 只接受同源消息
      if (evt.origin === host) {
        next(evt.data?.userGuid);
      }
    }),
    frameErrorPromise(frame),
    frameTimeoutPromise(timeout, "获取用户信息超时"),
  ]).finally(cleanSideEffects);
  document.body.appendChild(frame);
  return promise;
}
// 获取当前登录用户guid
async function getUserGuid(args?: {
  retryCount?: number;
  checkSessionUrl: string;
  timeout?: number;
}) {
  let {
    retryCount = 1,
    checkSessionUrl = "https://xxx/user/checkSession",
    timeout,
  } = args || {};

  if (processPromise) return processPromise;

  const execute = async () => {
    try {
      const res = (await _getCurrentUser(checkSessionUrl, timeout)) as string;
      processPromise = null;
      return res;
    } catch (err) {
      log(err?.message);

      if (retryCount > 0) {
        log(`开始重新获取,剩余重试次数${retryCount}次`);
        retryCount--;
        return execute();
      }
      processPromise = null;
      throw new Error(`获取用户信息失败`);
    }
  };

  processPromise = execute();
  return processPromise;
}

完成代码,接入测试下:

正常场景 image.png

超时场景
image.png

竞态场景 image.png

完美,项目交由王道友测试后,其嘴角微微上扬,内心亦起了一丝丝敬意。

小结

用户登录校验经常失灵,根本原因在于用户一致性校验环节的异步流程错乱。
本文通过梳理原有检验流程,结合promise、async/await 异步函数重构了代码,并考虑到了多重边界场景,最终道友们的登录缘分,终可不必再靠玄学。

不多说了,王道友已经买好咖啡在门口了,我去去就回……

793f9b86e950352a5209b3df1643fbf2b0118bcf.gif

JavaScript 性能优化:调优策略与工具使用

JavaScript 性能优化:调优策略与工具使用

引言

在当今的 Web 开发领域,性能优化已不再是锦上添花,而是产品成功的关键因素。据 Google 研究表明,页面加载时间每增加 3 秒,跳出率将提高 32%。而移动端用户如果页面加载超过 3 秒,有 53% 的用户会放弃访问。性能直接影响用户体验、转化率,这使得性能优化成为我们必备的核心技能。

性能评估指标

在开始优化之前,我们需要建立清晰的性能衡量标准。Google 提出的 Web Vitals 是目前业界公认的性能评估指标体系:

  • TTFB (Time To First Byte): 从用户请求到收到服务器响应第一个字节的时间,反映服务器响应速度和网络状况。理想值应小于 100ms。

  • FCP (First Contentful Paint): 首次内容绘制时间,指浏览器渲染出第一块内容(文本、图片等)的时间点。这是用户首次看到页面有内容的时刻,是感知速度的重要指标。良好的 FCP 值应小于 1.8 秒。

  • LCP (Largest Contentful Paint): 最大内容绘制时间,衡量视窗内最大内容元素(通常是主图或标题文本)完成渲染的时间。这是 Core Web Vitals 的重要指标,良好表现值应在 2.5 秒以内。

  • TTI (Time To Interactive): 页面可交互时间,指页面首次完全可交互的时刻。此时,页面已经显示有用内容,事件处理程序已注册,且界面能在 50ms 内响应用户输入。

  • TBT (Total Blocking Time): 总阻塞时间,衡量 FCP 到 TTI 之间主线程被阻塞的总时长。阻塞时间是指任何超过 50ms 的长任务所阻塞的时间。这个指标直接反映了页面交互流畅度。

  • CLS (Cumulative Layout Shift): 累积布局偏移,量化页面加载过程中视觉元素意外移动的程度。良好的 CLS 值应低于 0.1,表明页面加载过程中元素位置较为稳定。

这些指标构成了 Google Core Web Vitals,直接影响搜索排名和用户体验。在优化工作中,我们应该以这些指标为目标,有针对性地改进应用性能。

Chrome DevTools 性能分析

Chrome DevTools 是前端性能分析的核心工具,掌握它的使用方法对于发现和解决性能问题至关重要。

性能面板(Performance Panel)详解

Performance 面板允许我们录制和分析页面在特定操作期间的性能表现。通过它,我们可以看到 JavaScript 执行、样式计算、布局、绘制和合成等活动的详细时间线。

使用方法

  1. 打开 DevTools(Windows/Linux: F12 或 Ctrl+Shift+I, Mac: Command+Option+I)
  2. 切换到 Performance 选项卡
  3. 点击左上角的"录制"按钮(圆形记录图标)
  4. 在页面上执行需要分析的操作(如滚动、点击、输入等)
  5. 点击"停止"按钮结束录制
  6. 分析生成的性能报告

你也可以使用 Performance API 在代码中标记和测量特定操作:

// 开始标记一个操作
performance.mark('操作开始');

// 执行需要测量的代码
doSomething();

// 结束标记
performance.mark('操作结束');

// 创建测量(从开始到结束)
performance.measure('操作耗时', '操作开始', '操作结束');

// 获取测量结果
const measurements = performance.getEntriesByType('measure');
console.log(measurements);

性能记录解读

Performance 面板的报告包含多个关键区域,每个区域提供不同的性能信息:

  • 控制栏:包含录制设置(如设备模拟、网络节流、CPU 节流等),这些设置可以模拟不同的设备条件。

  • 概述窗格:显示 FPS(帧率)、CPU 利用率和网络活动的总览图表。您可以在此处拖动选择要查看详细信息的时间段。

    • FPS 图表中的绿色条越高,表示帧率越高,用户体验越流畅
    • 红色块表示帧率下降严重,可能导致卡顿
    • CPU 图表展示了不同类型活动(如脚本执行、渲染、垃圾回收)占用的 CPU 时间
  • 火焰图(Flame Chart):主要展示主线程活动的详细时间线。这是分析性能瓶颈的核心区域:

    • 每个条形代表一个事件,宽度表示执行时间
    • 条形堆叠表示调用栈,顶部事件由其下方的事件调用
    • 黄色部分表示 JavaScript 执行
    • 紫色部分表示布局计算(可能导致重排)
    • 绿色部分表示绘制操作(重绘)
    • 灰色部分通常表示系统活动或空闲时间
  • 关键性能事件:在时间线上标记的重要事件,如:

    • FCP(首次内容绘制)
    • LCP(最大内容绘制)
    • Layout Shifts(布局偏移)
    • Long Tasks(长任务,执行时间超过 50ms 的任务)

优化决策方法

查看性能记录时,应重点关注以下几点:

  1. 长任务:查找持续时间超过 50ms 的任务(在火焰图中显示为红色标记),这些任务会阻塞主线程,导致界面无响应。

  2. 布局抖动:查找反复触发布局(紫色事件)的模式,这通常表示代码中存在强制重排的问题。

  3. 过多垃圾回收:频繁的垃圾回收(标记为 GC 的灰色事件)表明可能存在内存管理问题。

  4. 阻塞渲染的资源:检查资源加载是否阻塞了关键渲染路径。

内存面板(Memory Panel)实用指南

内存泄漏是导致页面长时间运行后性能不断下降的主要原因之一。Chrome DevTools 的 Memory 面板提供了强大的工具来分析内存使用情况:

使用方法

  1. 打开 DevTools 并切换到 Memory 选项卡
  2. 选择分析类型:
    • Heap Snapshot(堆快照):捕获 JavaScript 对象和相关 DOM 节点的完整内存快照
    • Allocation Timeline(分配时间线):记录随时间推移的内存分配情况
    • Allocation Sampling(分配采样):低开销的内存分配采样

内存泄漏检测步骤

  1. 基线快照:在页面加载完成后立即拍摄一个堆快照作为基准
  2. 操作执行:执行可能导致内存泄漏的操作(如打开/关闭模态框、切换页面等)
  3. 强制垃圾回收:点击内存面板中的垃圾桶图标强制执行垃圾回收
  4. 比较快照:拍摄第二个快照,并使用比较功能(选择 "Comparison" 视图)分析两次快照的差异

分析关键点

  • 关注 "Objects added" (新增对象)部分
  • 检查 "Detached DOM trees"(分离的 DOM 树)和 "Detached elements"(分离的元素)
  • 查看对象的引用链(右键选择 "Show object's references")以了解是什么阻止了对象被垃圾回收

内存泄漏:检测与防范

内存泄漏不仅会导致页面随时间推移变得缓慢,还可能最终导致页面崩溃。了解常见的内存泄漏模式及其解决方法至关重要。

常见内存泄漏模式

1. 闭包引用导致的泄漏

闭包是 JavaScript 中强大的特性,但如果使用不当,可能导致大对象无法被垃圾回收:

// 泄漏示例:闭包长期引用大型数据结构
function createLeak() {
  // 创建一个大型数组(约占用 8MB 内存)
  const largeArray = new Array(1000000).fill('x');
  
  // 返回的函数形成闭包,持有对 largeArray 的引用
  return function() {
    // 即使只使用数组的一小部分,整个数组都不会被回收
    console.log(largeArray[0]);
  };
}

// 现在 leak 函数持有对 largeArray 的引用
// 即使 largeArray 再也不会被完全使用,它也不会被垃圾回收
const leak = createLeak();

// 即使调用无数次,largeArray 始终存在于内存中
leak(); 

这种情况下,返回的函数通过闭包持有对 largeArray 的引用,即使只用到了数组的第一个元素,整个 1,000,000 元素的数组也会一直保留在内存中。

解决方法

function avoidLeak() {
  // 创建大型数组
  const largeArray = new Array(1000000).fill('x');
  
  // 只保留需要的数据
  const firstItem = largeArray[0]; 
  
  // 返回仅引用所需数据的函数
  return function() {
    console.log(firstItem);
  };
  
  // largeArray 在函数结束后可以被垃圾回收
}

2. 未清除的事件监听器

事件监听器是最常见的内存泄漏来源之一,特别是在 SPA(单页应用)中:

// 泄漏示例:事件监听器未被清除
function setupListener() {
  const button = document.getElementById('my-button');
  // 创建引用大量数据的处理函数
  const largeData = new Array(1000000).fill('x');
  
  // 添加引用了 largeData 的事件监听器
  button.addEventListener('click', function() {
    console.log(largeData.length);
  });
}

// 调用函数设置监听器
setupListener();

// 即使后来移除了按钮元素,事件监听器仍然存在,
// 由于监听器引用了 largeData,所以 largeData 也不会被回收
document.body.removeChild(document.getElementById('my-button'));

在这个例子中,即使按钮从 DOM 中移除,事件监听器仍持有对 largeData 的引用,导致内存泄漏。

解决方法

function setupListenerProperly() {
  const button = document.getElementById('my-button');
  const largeData = new Array(1000000).fill('x');
  
  // 存储处理函数的引用,以便稍后可以移除
  const handleClick = function() {
    console.log(largeData.length);
  };
  
  button.addEventListener('click', handleClick);
  
  // 返回清理函数,在组件卸载或元素移除前调用
  return function cleanup() {
    button.removeEventListener('click', handleClick);
    // 现在 handleClick 和 largeData 可以被垃圾回收
  };
}

const cleanup = setupListenerProperly();

// 在移除元素前调用清理函数
cleanup();
document.body.removeChild(document.getElementById('my-button'));

3. 循环引用

对象之间相互引用可能导致整个对象图都无法被垃圾回收:

// 泄漏示例:循环引用
function createCyclicReference() {
  let objectA = { name: 'Object A', data: new Array(1000000) };
  let objectB = { name: 'Object B' };
  
  // 创建循环引用
  objectA.reference = objectB;
  objectB.reference = objectA;
  
  // 只返回 objectB,看似objectA可以被回收
  return objectB;
}

const result = createCyclicReference();
// 虽然我们只保留了对 objectB 的引用,
// 但由于循环引用,objectA 及其大型数组也不会被回收

现代 JavaScript 引擎通常能处理简单的循环引用,但复杂的对象关系仍可能导致问题。

使用 WeakMap 和 WeakSet

WeakMapWeakSet 是解决特定类型内存泄漏的利器,它们持有对对象的弱引用,不会阻止被引用对象的垃圾回收:

// 使用 WeakMap 存储与 DOM 元素关联的数据
const nodeData = new WeakMap();

function processNode(node) {
  // 为节点关联大量数据
  const data = { 
    processed: true, 
    timestamp: Date.now(),
    details: new Array(10000).fill('x')
  };
  
  // 使用 WeakMap 存储关联
  nodeData.set(node, data);
  
  // 当 node 被从 DOM 中移除并且没有其他引用时,
  // data 对象会被自动垃圾回收,不会造成内存泄漏
}

// 处理一个DOM节点
const div = document.createElement('div');
processNode(div);

// 当 div 不再被引用时,WeakMap 中关联的数据也会被回收
div = null; // 假设没有其他地方引用这个 div

WeakMap 的实际应用场景包括:

  1. 存储 DOM 节点的额外数据,而不影响节点的生命周期
  2. 实现私有属性和方法
  3. 缓存计算结果,但不阻止对象被回收

JavaScript 内存管理最佳实践

除了解决具体的内存泄漏问题,还应遵循以下最佳实践:

  1. 定期检查内存使用情况:将内存分析纳入开发和测试流程
  2. 避免全局变量:全局变量不会被垃圾回收,除非页面刷新
  3. 使用事件委托:减少事件监听器数量
  4. 合理使用闭包:确保闭包不会无意中引用大型对象
  5. 注意 DOM 引用:不要在长期存在的对象中保存对临时 DOM 元素的引用
  6. 定期进行代码审查:特别关注内存管理相关问题

重排重绘:渲染性能优化

理解浏览器的渲染流程是优化视觉性能的基础。这一流程通常包括以下步骤:

  1. JavaScript: 执行 JavaScript 代码,可能改变 DOM 或 CSSOM
  2. Style: 根据 CSS 规则计算元素的样式
  3. Layout (重排): 计算元素的几何位置和大小
  4. Paint (重绘): 填充元素的像素
  5. Composite: 将各层合成并显示在屏幕上

重排(Layout/Reflow)和重绘(Paint/Repaint)是渲染过程中最消耗性能的步骤:

  • 重排:当元素的几何属性(如宽度、高度、位置)发生变化时触发,需要重新计算布局
  • 重绘:当元素的视觉属性(如颜色、透明度)发生变化时触发,不改变布局

检测重排重绘问题

Chrome DevTools 提供了多种方法来识别重排重绘问题:

  1. Performance 面板

    • 重排在火焰图中显示为紫色的"Layout"事件
    • 重绘显示为绿色的"Paint"事件
    • 这些事件时间过长或频率过高都表明存在性能问题
  2. 渲染面板

    • 打开 DevTools > 按 Esc 键 > 在出现的抽屉面板中选择"Rendering"
    • 启用"Paint flashing"可以高亮显示重绘区域
    • 启用"Layout Shifts"可以显示布局偏移区域
  3. 性能监控

    • 开启 FPS 计数器:DevTools > 更多工具 > 渲染 > FPS meter
    • 帧率下降通常表明存在渲染性能问题

减少重排重绘的策略详解

1. 批量修改 DOM

每次 DOM 修改都可能触发重排和重绘。通过批量修改可以将多次更改合并为一次:

// 优化前:每次操作都会触发布局计算
function poorPerformance() {
  const element = document.getElementById('container');
  // 每行都可能导致单独的重排
  element.style.width = '100px';
  element.style.height = '200px';
  element.style.margin = '10px';
  element.style.padding = '15px';
  element.style.border = '1px solid black';
}

// 优化后:批量修改样式
function goodPerformance() {
  const element = document.getElementById('container');
  
  // 方法一:使用 cssText 一次性设置多个样式
  element.style.cssText = 'width: 100px; height: 200px; margin: 10px; padding: 15px; border: 1px solid black;';
  
  // 方法二:使用 class 切换而不是直接修改样式
  // element.classList.add('styled-container');
  
  // 方法三:使用 DocumentFragment 批量添加多个DOM元素
  // const fragment = document.createDocumentFragment();
  // for (let i = 0; i < 10; i++) {
  //   const child = document.createElement('div');
  //   child.textContent = `Item ${i}`;
  //   fragment.appendChild(child);
  // }
  // element.appendChild(fragment); // 只触发一次重排
}

2. 使用 will-change 属性提示浏览器

will-change 属性告诉浏览器元素的某个属性可能会发生变化,使浏览器提前做好准备:

/* 告诉浏览器这些元素的 transform 和 opacity 属性会发生变化 */
.animated-element {
  will-change: transform, opacity;
}

/* 注意:animated-element-gpu 为需要进行动画的元素创建新的层 */
.animated-element-gpu {
  /* 将元素提升到 GPU 层 */
  transform: translateZ(0);
  /* 或使用 will-change */
  will-change: transform;
}

需要注意的是,will-change 不应过度使用,因为:

  • 创建新的图层需要额外的内存
  • 对于过多元素同时使用会适得其反
  • 应该在动画开始前添加,在动画结束后移除

3. 使用 Transform 和 Opacity 属性代替直接改变位置和显示

CSS 的 transformopacity 属性是特殊的,它们的变化通常只触发合成阶段,跳过布局和绘制步骤:

/* 不佳实践:更改位置属性导致重排 */
.box-bad {
  transition: left 0.5s, top 0.5s;
  position: absolute;
  left: 0;
  top: 0;
}
.box-bad:hover {
  left: 100px;
  top: 100px;
}

/* 良好实践:使用 transform 只触发合成 */
.box-good {
  transition: transform 0.5s;
  position: absolute;
  transform: translate(0, 0);
}
.box-good:hover {
  transform: translate(100px, 100px);
}

4. 离线操作 DOM

当需要进行大量 DOM 操作时,先将元素从文档流中移除,操作完成后再放回:

// 优化复杂 DOM 操作
function updateComplexUI(data) {
  const list = document.getElementById('large-list');
  
  // 1. 记录当前滚动位置
  const scrollTop = list.scrollTop;
  
  // 2. 从文档流中移除元素
  const parent = list.parentNode;
  const nextSibling = list.nextSibling;
  parent.removeChild(list);
  
  // 3. 进行大量DOM操作
  for (let i = 0; i < data.length; i++) {
    const item = document.createElement('li');
    item.textContent = data[i].name;
    list.appendChild(item);
  }
  
  // 4. 将元素放回文档
  if (nextSibling) {
    parent.insertBefore(list, nextSibling);
  } else {
    parent.appendChild(list);
  }
  
  // 5. 恢复滚动位置
  list.scrollTop = scrollTop;
}

5. 使用 CSS 动画而非 JavaScript 操作

CSS 动画通常比 JavaScript 动画更高效,因为浏览器可以对其进行优化:

/* CSS 动画示例 */
@keyframes slide-in {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

.animated {
  animation: slide-in 0.5s ease-out;
}

6. 避免强制同步布局

当 JavaScript 在读取某些 DOM 属性后立即修改 DOM 时,可能导致浏览器提前执行布局计算:

// 不良实践:强制同步布局
function forceSyncLayout() {
  const boxes = document.querySelectorAll('.box');
  
  boxes.forEach(box => {
    // 读取布局信息
    const width = box.offsetWidth;
    
    // 立即写入修改,导致浏览器必须重新计算布局
    box.style.width = (width * 2) + 'px';
    
    // 再次读取,导致另一次强制布局
    const height = box.offsetHeight;
    box.style.height = (height * 2) + 'px';
  });
}

// 良好实践:分离读写操作
function avoidForcedLayout() {
  const boxes = document.querySelectorAll('.box');
  const dimensions = [];
  
  // 先读取所有需要的布局信息
  boxes.forEach(box => {
    dimensions.push({
      width: box.offsetWidth,
      height: box.offsetHeight
    });
  });
  
  // 再一次性写入所有修改
  boxes.forEach((box, i) => {
    const dim = dimensions[i];
    box.style.width = (dim.width * 2) + 'px';
    box.style.height = (dim.height * 2) + 'px';
  });
}

异步加载优化

在现代 Web 应用中,资源加载策略直接影响页面启动性能。异步加载技术允许页面只加载当前需要的资源,推迟非关键资源的加载。

代码分割与懒加载详解

代码分割是将应用程序代码分解成多个小块(chunks),按需加载的过程。这种方法能显著减少初始加载时间:

在 React 中实现代码分割

// 传统方式:一次性加载所有组件
import Dashboard from './Dashboard';
import Profile from './Profile';
import Settings from './Settings';

// 使用 React.lazy 和 Suspense 实现代码分割
import React, { Suspense, lazy } from 'react';

// 组件将在需要渲染时才加载
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Router>
        <Route path="/dashboard" component={Dashboard} />
        <Route path="/profile" component={Profile} />
        <Route path="/settings" component={Settings} />
      </Router>
    </Suspense>
  );
}

在 Vue 中实现代码分割

// Vue Router 配置中的代码分割
const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    // 使用动态导入实现懒加载
    component: () => import('./views/Dashboard.vue')
  },
  {
    path: '/profile',
    name: 'Profile',
    component: () => import('./views/Profile.vue')
  }
];

使用 Webpack 手动控制代码分割

// Webpack 动态导入示例
button.addEventListener('click', () => {
  // 动态导入模块,仅在点击按钮时加载
  import('./modules/heavy-module.js')
    .then(module => {
      module.default();
    })
    .catch(err => console.error('Module loading failed:', err));
});

图片懒加载深度剖析

图片通常是 Web 应用中最大的资源,实现图片懒加载可以显著提升页面加载性能:

使用原生懒加载

<!-- 使用 HTML5 原生懒加载属性 -->
<img src="placeholder.jpg" 
     data-src="actual-image.jpg" 
     loading="lazy" 
     alt="Lazy loaded image" 
     class="lazy-image" />

使用 Intersection Observer API 实现自定义懒加载

// 高性能的图片懒加载实现
document.addEventListener("DOMContentLoaded", function() {
  // 获取所有带有 lazy-image 类的图片
  const lazyImages = document.querySelectorAll(".lazy-image");
  
  // 如果浏览器不支持 IntersectionObserver,则加载所有图片
  if (!('IntersectionObserver' in window)) {
    lazyImages.forEach(image => {
      if (image.dataset.src) {
        image.src = image.dataset.src;
      }
    });
    return;
  }
  
  // 创建观察器实例
  const imageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      // 当图片进入视口时
      if (entry.isIntersecting) {
        const img = entry.target;
        
        // 替换图片源
        if (img.dataset.src) {
          img.src = img.dataset.src;
        }
        
        // 图片加载完成后移除占位样式
        img.onload = () => {
          img.classList.remove("lazy-placeholder");
          img.classList.add("lazy-loaded");
        };
        
        // 停止观察已处理的图片
        observer.unobserve(img);
      }
    });
  }, {
    // 根元素,默认为浏览器视口
    root: null,
    // 根元素的边距,用于扩展或缩小视口
    rootMargin: '0px 0px 200px 0px', // 图片距离视口底部200px时开始加载
    // 元素可见度达到多少比例时触发回调
    threshold: 0.01 // 图片有1%进入视口时触发
  });
  
  // 开始观察所有懒加载图片
  lazyImages.forEach(image => {
    imageObserver.observe(image);
  });
});

相比简单的滚动事件监听,Intersection Observer API 更高效,不会阻塞主线程,并且提供更精确的可见性检测。

预加载和预获取技术

现代浏览器提供了资源提示(Resource Hints)API,允许开发者指示浏览器预加载关键资源:

<!-- 预加载当前页面立即需要的资源 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero-image.jpg" as="image">
<link rel="preload" href="main-font.woff2" as="font" crossorigin>

<!-- 预获取用户可能导航到的下一个页面资源 -->
<link rel="prefetch" href="next-page.html">
<link rel="prefetch" href="article-data.json">

<!-- 预连接到将要从中请求资源的域 -->
<link rel="preconnect" href="https://api.example.com">

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com">

这些资源提示的使用场景:

  • preload:用于当前页面肯定会用到的关键资源
  • prefetch:用于下一页面可能需要的资源
  • preconnect:用于提前建立到第三方域的连接
  • dns-prefetch:用于提前解析第三方域的 DNS

按需加载与按需执行

除了按需加载资源外,还可以实现按需执行代码:

// 按需执行示例:用户交互触发的代码
function setupDeferredExecution() {
  // 只设置事件监听,不立即加载或执行复杂逻辑
  document.getElementById('advanced-feature').addEventListener('click', () => {
    // 用户点击时再加载并执行复杂功能
    import('./features/advanced-chart.js')
      .then(module => {
        module.initializeChart('chart-container');
      });
  });
  
  // 使用 Intersection Observer 监测元素是否接近视口
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        // 元素接近视口时加载评论系统
        import('./features/comments.js')
          .then(module => {
            module.initComments();
            observer.unobserve(entry.target);
          });
      }
    });
  }, { rootMargin: '200px' });
  
  // 观察评论容器
  const commentsSection = document.getElementById('comments-section');
  if (commentsSection) {
    observer.observe(commentsSection);
  }
}

性能优化工作流程

一套有效的性能优化工作流程或许是这样的:

1. 建立基准

在开始优化前,必须建立性能基准,以便衡量改进效果:

// 使用 Performance API 建立基准
const performanceMeasures = {};

// 记录关键用户操作的性能
function measurePerformance(action, callback) {
  const startMark = `${action}_start`;
  const endMark = `${action}_end`;
  
  performance.mark(startMark);
  
  // 执行操作
  const result = callback();
  
  performance.mark(endMark);
  performance.measure(action, startMark, endMark);
  
  // 收集测量结果
  const measures = performance.getEntriesByName(action);
  performanceMeasures[action] = measures[0].duration;
  
  console.log(`${action} took ${measures[0].duration.toFixed(2)}ms`);
  
  return result;
}

// 使用示例
measurePerformance('product_filter', () => {
  return filterProducts(products, { category: 'electronics' });
});

除了代码测量外,使用以下工具建立全面基准:

  • Lighthouse: 提供全面的性能审计报告
  • WebPageTest: 在不同网络条件和设备上测试性能
  • Core Web Vitals 报告: 使用真实用户数据评估性能

2. 诊断问题

使用系统化方法定位性能瓶颈:

  • 性能瀑布图分析: 查看关键渲染路径和阻塞资源
  • JavaScript CPU 分析: 识别耗时的函数调用
  • 内存分析: 查找内存泄漏和过度内存使用
  • 渲染性能: 检测重排重绘和帧率下降

3. 制定方案

根据诊断结果,制定针对性的优化策略:

问题类型 优化策略
资源加载过多 代码分割、懒加载、资源压缩
主线程阻塞 Web Workers、长任务分解、节流/防抖
渲染性能不佳 虚拟滚动、减少重排重绘、使用 CSS 硬件加速
内存管理问题 修复内存泄漏、减少闭包、使用 WeakMap/WeakSet

4. 实施优化

遵循"最大收益原则",先处理影响最显著的问题:

  1. 优先级划分:

    • P0: 影响核心功能的严重性能问题
    • P1: 影响用户体验但不阻碍核心功能的问题
    • P2: 小型优化和改进
  2. 增量实施:

    • 每次修改后测量性能改进
    • 确保不引入新的性能问题
    • 建立性能回归测试

5. 验证成效

使用多种方法验证优化效果:

// 性能对比测试
function runPerformanceComparison(testName, oldFn, newFn, iterations = 1000) {
  console.log(`Running comparison for: ${testName}`);
  
  // 预热
  for (let i = 0; i < 10; i++) {
    oldFn();
    newFn();
  }
  
  // 测试旧实现
  const startOld = performance.now();
  for (let i = 0; i < iterations; i++) {
    oldFn();
  }
  const endOld = performance.now();
  const oldTime = endOld - startOld;
  
  // 测试新实现
  const startNew = performance.now();
  for (let i = 0; i < iterations; i++) {
    newFn();
  }
  const endNew = performance.now();
  const newTime = endNew - startNew;
  
  // 计算改进百分比
  const improvement = ((oldTime - newTime) / oldTime) * 100;
  
  console.log(`Old implementation: ${oldTime.toFixed(2)}ms`);
  console.log(`New implementation: ${newTime.toFixed(2)}ms`);
  console.log(`Improvement: ${improvement.toFixed(2)}%`);
  
  return {
    oldTime,
    newTime,
    improvement
  };
}

除了代码测试,还应进行:

  • A/B 测试: 对比新旧实现在真实用户中的表现
  • 用户体验测试: 收集用户对优化后体验的反馈
  • 回归测试: 确保优化不影响功能正确性

6. 持续监控

建立长期性能监控系统:

  • 实时性能监控: 使用 Performance API 和 Beacon API 收集真实用户数据
  • 性能预算: 设定资源大小、加载时间和交互延迟的上限
  • 性能警报: 当性能指标超过阈值时触发警报
  • 定期审查: 每个版本发布前进行性能审查

通过这种系统化的方法,性能优化不再是一次性工作,而是开发流程中的持续活动。

未来趋势与进阶技术

作为前端工程师,了解性能优化的未来趋势对保持技术竞争力至关重要:

Web Assembly (WASM)

WASM 允许以接近原生的速度在浏览器中运行代码,适用于计算密集型任务:

// 示例:使用 WASM 加速图像处理
async function loadWasmImageProcessor() {
  try {
    // 加载 WASM 模块
    const wasmModule = await WebAssembly.instantiateStreaming(
      fetch('/image-processor.wasm'),
      {
        env: {
          abort: () => console.error('WASM模块出错')
        }
      }
    );
    
    // 获取导出的函数
    const { applyFilter } = wasmModule.instance.exports;
    
    // 使用 WASM 函数处理图像
    function processImage(imageData) {
      const { data, width, height } = imageData;
      
      // 分配内存
      const wasmMemory = wasmModule.instance.exports.memory;
      const inputPtr = wasmModule.instance.exports.allocate(data.length);
      
      // 拷贝数据到 WASM 内存
      const inputArray = new Uint8Array(wasmMemory.buffer, inputPtr, data.length);
      inputArray.set(data);
      
      // 调用 WASM 函数处理图像
      const outputPtr = applyFilter(inputPtr, width, height);
      
      // 获取结果
      const outputArray = new Uint8Array(wasmMemory.buffer, outputPtr, data.length);
      const resultData = new Uint8ClampedArray(outputArray);
      
      // 清理内存
      wasmModule.instance.exports.deallocate(inputPtr);
      wasmModule.instance.exports.deallocate(outputPtr);
      
      return new ImageData(resultData, width, height);
    }
    
    return processImage;
  } catch (error) {
    console.error('Failed to load WASM module:', error);
    // 降级处理
    return fallbackImageProcessor;
  }
}

HTTP/3 和 QUIC

新的网络协议提供更快的连接建立和更可靠的传输:

// 检测并优先使用 HTTP/3 
async function detectAndUseHTTP3() {
  // 检测浏览器是否支持 HTTP/3
  const supportsHTTP3 = 'http3' in window || 'quic' in window;
  
  if (supportsHTTP3) {
    // 使用支持 HTTP/3 的 CDN 域名
    return 'https://http3.example.com';
  } else {
    // 降级到 HTTP/2
    return 'https://cdn.example.com';
  }
}

// 在资源加载中使用
async function loadResources() {
  const baseUrl = await detectAndUseHTTP3();
  const resources = [
    `${baseUrl}/styles.css`,
    `${baseUrl}/main.js`,
    `${baseUrl}/images/hero.jpg`
  ];
  
  // 预连接
  const link = document.createElement('link');
  link.rel = 'preconnect';
  link.href = baseUrl;
  document.head.appendChild(link);
  
  // 加载资源
  // ...
}

Web Workers 和计算并行化

Web Workers 使复杂计算可以在后台线程运行,不阻塞 UI 线程:

// 主线程代码
function setupDataProcessing() {
  // 创建 Worker
  const worker = new Worker('data-processor.js');
  
  // 监听 Worker 消息
  worker.addEventListener('message', (event) => {
    const { type, result } = event.data;
    
    switch (type) {
      case 'PROCESSED_DATA':
        updateUI(result);
        break;
      case 'PROGRESS':
        updateProgressBar(result.percent);
        break;
      case 'ERROR':
        showError(result.message);
        break;
    }
  });
  
  // 发送数据到 Worker
  function processLargeDataSet(data) {
    worker.postMessage({
      type: 'PROCESS_DATA',
      data
    });
  }
  
  return {
    processLargeDataSet,
    terminateWorker: () => worker.terminate()
  };
}

// Worker 文件 (data-processor.js)
/* 
self.addEventListener('message', (event) => {
  const { type, data } = event.data;
  
  if (type === 'PROCESS_DATA') {
    try {
      // 报告进度
      self.postMessage({ type: 'PROGRESS', result: { percent: 0 } });
      
      // 进行耗时计算
      const chunks = splitIntoChunks(data, 10);
      let processedData = [];
      
      chunks.forEach((chunk, index) => {
        const processed = processChunk(chunk);
        processedData = processedData.concat(processed);
        
        // 更新进度
        const progress = Math.round(((index + 1) / chunks.length) * 100);
        self.postMessage({ type: 'PROGRESS', result: { percent: progress } });
      });
      
      // 发送处理结果
      self.postMessage({
        type: 'PROCESSED_DATA',
        result: processedData
      });
    } catch (error) {
      self.postMessage({
        type: 'ERROR',
        result: { message: error.message }
      });
    }
  }
});

function splitIntoChunks(array, numChunks) {
  // 将数组分成多个块
  const chunkSize = Math.ceil(array.length / numChunks);
  return Array.from({ length: numChunks }, (_, i) => 
    array.slice(i * chunkSize, (i + 1) * chunkSize)
  );
}

function processChunk(chunk) {
  // 处理数据的复杂计算
  return chunk.map(item => {
    // 假设这是一个复杂计算
    return complexTransformation(item);
  });
}
*/

结语

JavaScript 性能优化是一个不断发展的领域,需要持续学习和实践。通过本文介绍的诊断工具、优化策略和最佳实践,我希望能为你提供一个全面的性能优化框架。

和许多事情一样,性能优化也不是一蹴而就的,而是需要贯穿整个开发生命周期的持续实践。

参考资源


如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇

终身学习,共同成长。

咱们下一期见

💻

【CodeBuddy】今天520,我只教你一遍。

前言:当代码邂逅浪漫 在这个充满爱意的520,我仅用5分钟就完成了一个包含时空胶囊、动态情书、记忆时间轴等复杂功能的网页应用。这一切的实现密码,正是CodeBuddy展现的AI编程魔力。通过这次实

我用扣子开发了一个属于自己的Agent....

📖前言

最近我接触了扣子平台,了解了可以使用扣子开发属于自己的Agent,所以我花了一些时间了解了如何从零到一,开发一款属于属于自己的Agent,并且成功开发了R&B音乐精灵,使用这款Agent,可以随时随地听自己喜爱的R&B的音乐,而且还能和它交流R&B风格的歌手,它还可以给你每日推荐适合你的R&B音乐!🎵🎵🎵

image.png

image.png

image.png




🤖Agent介绍

首先跟大家简单介绍一下Agent,Agent也叫智能体,是指能够感知环境,自主决策并执行动作以实现特定目标的实体,简单来说,Agent就是一个智能的小助手,它像人一样有 眼睛(感知环境)、大脑(思考决策)、手脚(执行动作),但它是虚拟的。

俗话说得好,术业有专攻,Agent是可以分为不同领域的,有的人喜欢体育赛事,那么它就可以开发一个体育赛事的Agent,专门回答体育相关的内容,假如你可以问他库里这个赛季的三分数据;有的人喜欢棋类,也可以开发一个棋类的Agent,只回答棋类的内容,假如你可以问他棋类的问题,甚至你还可以跟他下棋......

而作者的爱好是听各类的R&B音乐(节奏蓝调,一种曲风),了解各类的R&B风格的歌手,陶喆、方大同、丁世光、曹格......,我喜欢听各种R&B曲风的音乐,所以我想开发一个华语R&B助手,我可以随时随地通过它,听我想听的R&B歌曲,我也可以跟他交流各种R&B歌手,我还可以让他像网易云音乐一样,根据我的喜好,每天给我推荐几首R&B歌曲。。。




🚀Agent开发步骤

于是作者准备去扣子平台自己动手开发一个这样的Agent,在作者注册了账号之后并且详细阅读了扣子的使用指南之后,我按照使用指南的操作自己一步步操作后,成功地开发出了一款属于我自己的Agent,接下来,我来分享一下咱们想要使用扣子平台开发一个属于自己的Agent该如何操作吧!👇👇



📝注册并创建

首先,大家需要进入扣子开发平台,进行用户注册,然后选择导航栏的开发平台,选择快速开始的按钮。

image.png

进入到主界面后,咱们选择左上角的+号,然后选择选择创建智能体就可以开始创建啦!

image.png


🆕创建初始化

在创建Agent的初始化,扣子网页会提出一个信息栏,有标准创建和AI创建本文就着重于讲解标准创建的方式,在该方式中,需要你填写该Agent的名字和简介,咱们只需要根据自己的Agent的内容与特点写一个名称和一段功能简介就行啦,然后工作空间默认选择个人空间就行了。

image.png

你可能会注意到,最下方有个图标,并且旁边还有个button,提示我们可以通过AI自动生成咱们产品的图标,咱们可以点击然后,它就会调用API,帮我们自动生成一个图标的图片啦,AI会根据你的Agent的名称和描述来生成合适的图标,但是生成的图标也会存在随机性,就像抽卡一样,不过如果你不满意的话,可以多生成几次!实在不行,咱们也可以选择自己上传的方式来进行图标的上传。

image.png


⚙️配置Agent

在创建Agent之后,咱们会进入到Agent的编排页面,你会发现:

  • 在左侧人设与回复逻辑面板中描述智能体的身份和任务。
  • 在中间技能面板为智能体配置各种扩展能力。
  • 在右侧预览与调试面板中,实时调试智能体。

你可以通过这三个区域的配置我们的Agent,让他变成你想要的样子。

image.png



💡编写提示词

配置智能体的第一步也是最重要的一步,就是编写提示词,也就是智能体的人设与回复逻辑。智能体的人设与回复逻辑定义了智能体的基本人设,此人设会持续影响智能体在所有会话中的回复效果。建议在人设与回复逻辑中指定模型的角色、设计回复的语言风格、限制模型的回答范围,让对话更符合用户预期。

咱们首先可以通过自己编写提示词,如果大家对提示词想详细了解,可以去看扣子官方对提示词的解释

这里作者先通过自己学习过的Prompt的书写规范,简要地按照人设-任务-步骤-注意事项,来书写一段提示词

image.png

然后当我们觉得自己写的提示词不够满意,觉得不够系统的话,可以点击右上角的button,用AI帮我们自动优化。

image.png

优化后选择替换,就可以一键替换到你刚刚输入的提示词啦

image.png

我们可以看到AI就是专业,用规范的格式来优化好了我们刚刚写的提示词 实际上这种格式我们也可以在“提示词库”进行查看,咱们可以选择不同的场景来按照提示词写,这样子的话,我们写出来的Prompt就能十分专业且准确啦!

image.png


🎛️详细配置各种能力

好了,编写提示词至此咱们就完成啦,接下来就是要在技能面板中为智能体配置各种能力了。

image.png

我们可以看到,在配置能力时,分为四种类型的配置,下面是扣子官方给出的对这四种类型的介绍。

  • 技能:技能是智能体的基础能力,你可以在搭建智能体时通过插件、工作流等方式拓展模型的能力边界。
  • 知识:知识库功能支持添加本地或线上数据供智能体使用,以提升大模型回复的可用性和准确性。更多信息,参考知识库概述
  • 记忆:模型最大对话轮数是有限的,记忆相关的能力可以为模型提供可以反复调用的长期记忆,让智能体的回复更加个性化。
  • 对话体验:对话体验可以增强用户和智能体对话过程中的交互效果。

接下来,我将会对这四种类型进行详细讲解

技能设置

首先,咱们来介绍一下技能吧,技能是智能体的基础能力,你可以在搭建智能体时通过插件、工作流等方式拓展模型的能力边界。在技能中每个功能的作用如下:

功能 说明
插件 通过 API 连接集成各种平台和服务,扩展了智能体能力。扣子平台内置丰富的插件供你直接调用,你也可以创建自定义插件,将你所需要的 API 集成在扣子内作为工具来使用。更多信息,参考插件介绍。例如使用新闻插件来搜索新闻,使用搜索工具查找在线信息等。
工作流 工作流是一种用于规划和实现复杂功能逻辑的工具。你可以通过拖拽不同的任务节点来设计复杂的多步骤任务,提升智能体处理复杂任务的效率。更多信息,参考工作流介绍
触发器 触发器功能支持智能体在特定时间或特定事件下自动执行任务。更多信息,参考触发器

1.插件的选择

我们可以看到,有大量的插件供我们选择,我们也可以通过搜索的方式来进行插件的查找

image.png

作者选择了网易云音乐的插件,添加了一些需要用到的接口

image.png


2.工作流和触发器的选择

作者这里在选择插件之后,工作流和触发器就使用默认的设置了。大家也可以通过自己的定制化去选择

image.png


知识设置

知识的选择分为文本、表格、照片,支持添加本地或线上的数据供智能体使用,这玩意就是一个数据库,咱们可以添加各种各样的知识,以提升智能体的信息检索的范围。让咱们的智能体“学识深渊”。

image.png

这里我的软件主要通过上网搜索来进行信息的查询,所以这里就默认不额外添加知识。


记忆设置

模型最大对话轮数是有限的,记忆相关的能力可以为模型提供可以反复调用的长期记忆,让智能体的回复更加个性化。

下面是记忆的官方介绍:

功能 说明
变量 变量功能可用来保存用户的语言偏好等个人信息,让智能体记住这些特征,使回复更加个性化。
数据库 数据库功能提供了一种简单、高效的方式来管理和处理结构化数据,开发者和用户可通过自然语言插入和查询数据库中的数据。同时,也支持开发者开启多用户模式,以实现更灵活的读写控制。更多信息,参考数据库
长期记忆 长期记忆功能模仿人类大脑形成对用户的个人记忆,基于这些记忆可以提供个性化回复,提升用户体验。更多信息,参考长期记忆
文件盒子 文件盒子提供了多模态数据的合规存储、管理以及交互能力。通过文件盒子,用户可以反复使用已保存的多模态数据。更多信息,参考文件盒子

这里咱们的音乐软件暂时不设置记忆功能,后续若有需要再进行补充

image.png

对话体验设置

对话体验里存在很多项设置

image.png

下面是在对话体验中的各个功能的说明:

功能 说明
开场白 设置智能体对话的开场语,让用户快速了解智能体的功能。例如 我是一个旅行助手智能体,我能帮助你计划行程和查找旅行信息。详情请参考开场白
用户问题建议 智能体每次响应用户问题后,系统会根据上下文自动提供三个相关的问题建议给用户使用。
快捷指令 快捷指令是开发者在搭建智能体时创建的预置命令,方便用户在对话中快速、准确地输入预设的信息,进入指定场景的会话。详情请参考快捷指令
背景图片 智能体的背景图片,在调试和商店中和智能体对话时展示,令对话过程更沉浸,提高对话体验。
语音 在搭建智能体时,你可以配置语音功能,以提供更自然和个性化的交互体验。配置语音时,需要选择语言和音色,确保智能体能够以用户喜爱的方式进行交流。此外,还支持开启语音通话功能,使用户能够通过语音与智能体进行实时互动,无需手动输入文字。
用户输入方式 在搭建智能体时,可以选择多种用户输入方式,以满足不同用户的需求和使用场景。用户输入方式支持打字输入、语音输入和语音通话。仅开启了语音通话功能,才支持选择语音通话输入方式。

1. 开场白

咱们为咱们的Agent设置一个开场白,让它能够更主动给咱们的用户打招呼

image.png

同时咱们还能在开场白里设置预置问题,通过这种方式,可以引导用户进行提问

2. 用户问题建议

当咱们的用户输入的信息不完整或模糊时,Agent自动生成追问或建议选项,引导用户补充关键信息或明确需求,比如咱们,提问一个不完整的信息,“我想听歌”,此时开启后,Agent可能会回答“ Agent:您想听什么类型的音乐? ➔ [ 今日热门 ] [ 心情放松 ] [ 运动健身 ] ”这种将模糊需求转化为明确任务的选项。

所以咱们就打开这个功能吧!

image.png

3.快捷指令

快捷指令是对话输入框上方的按钮,配置完成后,用户可以快速发起预设对话,这里咱们就创建一个测试指令,来测试一下基本的使用

image.png

image.png

4.背景图片、语音、用户输入方式

image.png


这三项就是在扣子平台的一些支持功能了,这里提一下,扣子支持用户可以以语音通话的方式与Agent进行交互,所以咱们可以设置不同的机器人音色,提升用户的体验。




⚖️模型对比调试

最后咱们选择一个模型即可,大家可以通过对比的方式来进行选择适合自己的大模型。

image.png

🚀发布

最后咱们可以选择发布我们的Agent到指定的平台啦!方便我们在别的平台也能进行使用。

image.png

image.png

🎯总结

本文主要讲解关于Agent开发的一个基本使用流程,开发的整个过程是几乎一步步手动去生成的,当然咱们也可以在初始化之后,选择让AI一键生成,但是本文主要是想让你了解各个概念以及如何使用,各个功能的使用较为基础,待读者再精进后,会再进行更新,介绍一下利用扣子开发的一些更进阶的内容,希望在看完本文之后,您能够学会开发Agent的基本使用!😊😊😊



🌇结尾

本文部分内容参考扣子官方的:参考文档

感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)

作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

如何统计某个网站加载资源或者发送请求的时候,都使用了哪些域名

要统计某个网站在加载资源或者发送请求时使用的所有域名,可以通过以下方法进行:

1. **使用浏览器开发者工具**:
   - 打开你的网站。
   - 按下 `F12` 打开开发者工具,或通过右键点击页面并选择“检查”。
   - 在开发者工具中,切换到 “网络”(Network) 面板。
   - 刷新页面,你会看到所有的网络请求。
   - 在“域名”或“名称”列中,你可以看到所有请求的 URL。可以手动统计这些 URL 中的域名。

2. **使用浏览器扩展**:
   - 有些浏览器扩展可以帮助你统计网站请求的域名,比如 `Ghostery`、`uBlock Origin` 等。
   - 安装并启用这些扩展,它们会显示所有的请求并提供统计数据。

3. **编写脚本**:
   - 你可以编写一个脚本来自动统计这些域名。以下是一个示例的 JavaScript 代码,可以在浏览器控制台中运行:

(function() {
    const domains = new Set();
    const requests = performance.getEntriesByType('resource');
    requests.forEach(request => {
        try {
            const url = new URL(request.name);
            domains.add(url.hostname);
        } catch (e) {
            console.error('Invalid URL:', request.name);
        }
    });
    console.log('Domains used:', Array.from(domains));
})();

将以上代码复制并粘贴到开发者工具的控制台中运行,你会在控制台中看到所有请求的域名列表。

通过这些方法,你就可以统计出某个网站加载资源或者发送请求时使用的所有域名。

方法三中的脚本讲解

通过以下步骤统计网站加载资源或发送请求时使用的所有域名:

1. **创建一个 Set 用于存储域名**:
 

const domains = new Set();

2. **获取所有的资源请求信息**:
   - 使用 `performance.getEntriesByType('resource')` 获取所有资源请求的性能条目。这些条目包括所有被请求的资源,如 CSS、JS、图像等。

const requests = performance.getEntriesByType('resource');

3. **遍历所有请求并提取域名**:
   - 对每个资源请求,尝试解析其 URL 并提取域名。
   - 使用 `new URL(request.name)` 解析 URL,并提取域名 `url.hostname`。
   - 将域名添加到 `Set` 中,确保不会有重复的域名。

   requests.forEach(request => {
       try {
           const url = new URL(request.name);
           domains.add(url.hostname);
       } catch (e) {
           console.error('Invalid URL:', request.name);
       }
   });

4. **输出所有独特的域名**:
   - 将 `Set` 转换为数组并打印出来。
 

   console.log('Domains used:', Array.from(domains));

完整的脚本如下:

(function() {
    const domains = new Set();
    const requests = performance.getEntriesByType('resource');
    requests.forEach(request => {
        try {
            const url = new URL(request.name);
            domains.add(url.hostname);
        } catch (e) {
            console.error('Invalid URL:', request.name);
        }
    });
    console.log('Domains used:', Array.from(domains));
})();

### 脚本原理总结

- **`performance.getEntriesByType('resource')`**:获取所有资源请求的性能条目。
- **`new URL(request.name)`**:创建 URL 对象以解析请求 URL。
- **`url.hostname`**:提取 URL 中的域名。
- **`Set`**:用于存储唯一的域名,避免重复。
- **`Array.from(domains)`**:将 `Set` 转换为数组,便于输出。

通过这些步骤,该脚本能够统计网站加载资源或发送请求时使用的所有域名。

域名拼接为字符串

(function() {
    const domains = new Set();
    const requests = performance.getEntriesByType('resource');
    requests.forEach(request => {
        try {
            const url = new URL(request.name);
            domains.add(url.hostname);
        } catch (e) {
            console.error('Invalid URL:', request.name);
        }
    });
    console.log('Domains used:', Array.from(domains));
    const domainsStr = Array.from(domains).join(',');
    console.log('Domains used:', domainsStr);
})();

seo介绍and谷歌,百度,微软站点地图添加

谷歌seo介绍 查看自己的网站是否被收录

site:ideaflow.top

search.google.com/search-cons…

资源页面

网站SEO指南:核心文件与作用

SEO核心优化步骤

1. 技术SEO

  • 网站速度优化
    • 压缩图片(WebP格式)
    • 启用GZIP/Brotli压缩
    • 使用CDN加速
  • 移动友好性
    • 响应式设计
    • 通过Google Mobile-Friendly Test
  • HTTPS加密
    • 安装SSL证书
  • URL结构优化
    • 静态URL(例:/seo-guide
    • 避免动态参数(?id=123

2. 内容优化

  • 关键词策略
    • 使用Google Keyword Planner
    • 长尾关键词布局(例:"新手SEO教程2024")
  • 内容质量
    • 深度≥2000字专业内容
    • 原创度≥90%
  • 语义优化
    • 使用LSI潜在语义关键词
    • 添加相关内部链接

3. 页面SEO

  • 标题标签
    • 长度≤60字符
    • 包含主关键词
  • Meta描述
    • 长度≤160字符
    • 行动号召语句
  • 结构化数据
    • Schema标记实现富媒体片段

必备SEO文件清单

文件类型 路径 核心作用
sitemap.xml /sitemap.xml XML格式站点地图,包含所有重要页面URL及其更新频率
robots.txt /robots.txt 控制搜索引擎爬虫的抓取权限
humans.txt /humans.txt 声明网站开发团队信息
验证文件 根目录或DNS Google Search Console/Bing Webmaster Tools所有权验证
结构化数据文件 页面部分 通过JSON-LD实现内容语义标注

关键文件详解

1. sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/</loc>
    <lastmod>2024-03-15</lastmod>
    <changefreq>daily</changefreq>
    <priority>1.0</priority>
  </url>
</urlset>
什么是站点地图(sitemap)
  • SEO价值:提升新页面发现速度30%-50%

  • 最佳实践:包含≤5万个URL,文件大小≤50MB

2. robots.txt
User-agent: *
Allow: /
Disallow: /private/
Disallow: /tmp/

Sitemap: https://example.com/sitemap.xml
  • 控制维度

    • 禁止抓取敏感目录(如/admin/)

    • 屏蔽重复内容路径

    • 指定sitemap位置

3. 页面结构化数据示例(Article)
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "SEO权威指南2024",
  "datePublished": "2024-03-15",
  "image": ["https://example.com/seo-guide.jpg"]
}
</script>
  • 效果提升:可使CTR提升15%-30%

百度-网站添加

百度添加网站地址 image.png

第一步:注册百度站长平台账号

注册一个百度站长平台账号,如果有百度的账号也可以直接用,然后登录这个账号。

第二步:在百度搜索引擎搜索你的网站

在百度搜索框,输入如下内容搜索你的网站:

site:your_domain

image.png

比如我这里输入site:ideaflow.top,如果没有找到,表明你的网站没有被百度收录。

第三步:提交网址

百度添加网站地址 根据步骤提交资源,以下结果: image.png

第四步:添加站点资源链接

请注意限制 image.png

Bing-网站添加

Bing是全球领先的搜索引擎之一,让自己的网站在Bing上快速被索引,是很多网站站长的首要目标。在这里,我将分享如何使用Bing Webmaster工具提交sitemap,以助你快速实现Bing搜索引擎对你网站的收录。

第一步:创建Bing Webmaster工具账户

www.bing.com/webmasters/…

登录后首次: image.png转存失败,建议直接上传图片文件

image.png

第二步:添加站点地图

bing自己网站分析官网地址 image.png

第三步:确认网站被收录

site:yourdomian.com

image.pngimage.png

谷歌-网站添加

谷歌需要魔法登录~ 谷歌官网seo介绍

第一步添加网站资源

官网地址

image.png

第二步:提交站点sitemap

进入资源页面 image.png

提交后的效果: image.png

第三部:验证

site:yourdomain.com

基于 Elpis下的DSL设计与实现

DSL 设计理解与总结

前言

在工作中我曾开发过类似的功能,因此在学习哲哥课程时,对其表达的思想能够较为容易地理解。例如,工作中的菜单栏是通过后端配置来渲染的,接口返回的信息包含了图标、路由跳转信息、描述等;弹窗模块中的权限渲染,也是依据后端配置的表结构实现权限联动,这种基于配置的开发模式大幅减少了后续的维护成本,基本实现了“一次开发,终身使用”。

不过在课程的学习过程中,由于对 DSL 结构不够清晰,虽然可以理解其意图,却难以记住各个配置的作用与处理方式。随着学习的深入,我才逐渐理解了这些配置结构的价值和意义。

核心思想:学会偷懒

通过一部分配置,减少重复的工作,把时间放到更有意义的地方。

一、什么是 DSL

在学习哲哥课程的过程中,最初不清楚“DSL”到底是什么,只知道它是一种配置规范。直到学习到里程碑3,才去深入了解。

DSL(Domain Specific Language) ,即领域特定语言,是一种专为特定问题领域设计的编程语言或语言规范。不同于通用编程语言(如 Python、Java),DSL 更专注于特定业务场景,提供更贴合该领域的语法和语义,使得配置和表达更加直观清晰。

在 DSL 配置中,字段结构严谨、类型明确,使人一眼就能明白每个配置的意图及其影响。

类比理解模型与模板

在学习完里程碑3时,我觉得可以将 DSL 中的“模型”理解为装不同类型玩具的盒子:有的盒子用来装汽车,有的用来装玩偶,还有的用来装积木。每个盒子内部会包含某类玩具的通用特征,比如所有汽车都有四个轮子、方向盘。这些通用特征可以作为模型的基础配置,这些“盒子”就相当于系统中的“模型(Model)”。

而对于某些特殊类型的玩具(如消防车、救护车),虽然它们都属于“汽车”模型,但还会有各自的特殊属性(比如消防车上有洒水装置、救护车有急救装置等等)。这时,我们就可以在模型的基础上,通过模板的方式扩展或重载配置,实现灵活定制,对应类型的玩具就相当于模型下的模板。

目录结构如下图所示:

image.png

我们基于不同的模型来延伸出各式各样的模板。

二、DSL 配置示例

以下是一个基于Elpis的 DSL 配置文件示例,用于定义电商系统的菜单与模块结构:

module.exports = {
    model: 'business',
    name: '电商系统',
    menu: [
        {
            key: 'product',
            name: '商品管理',
            menuType: 'module',
            moduleType: 'schema',
            schemaConfig: {
                api: '/api/proj/product',
                schema: {
                    type: 'object',
                    properties: {
                        product_id: {
                            type: 'string',
                            label: '商品ID',
                            tableOption: {
                                width: 300,
                                'show-overflow-tooltip': true
                            },
                            searchOption: {
                                comType: 'select',
                                enumList: [
                                    { label: '全部', value: 'all' },
                                    { label: '1', value: 1 },
                                    { label: '2', value: 2 },
                                    { label: '3', value: 3 }
                                ]
                            }
                        },
                        product_name: {
                            type: 'string',
                            label: '商品名称',
                            tableOption: {
                                width: 200,
                            },
                            searchOption: {
                                comType: 'input',
                                default: '',
                            }
                        },
                        price: {
                            type: 'number',
                            label: '价格',
                            tableOption: {
                                width: 200,
                            },
                            searchOption: {
                                comType: 'dynamicSelect',
                                api: '/api/proj/product_enum/list',
                            }
                        },
                        inventory: {
                            type: 'number',
                            label: '库存',
                            tableOption: {
                                width: 200,
                            }
                        },
                        create_time: {
                            type: 'string',
                            label: '创建时间',
                            tableOption: {
                                width: 400,
                            },
                            searchOption: {
                                comType: 'dateRange',
                                dateType: 'daterange',
                            }
                        }
                    }
                },
                tableConfig: {
                    headerButtons: [
                        { label: '新增商品', eventKey: 'showComponent', type: 'primary', plain: true }
                    ],
                    rowButtons: [
                        { label: '修改信息', eventKey: 'showComponent', type: 'warning' },
                        {
                            label: '删除',
                            eventKey: 'remove',
                            type: 'danger',
                            eventOption: {
                                params: {
                                    product_id: 'schema::product_id',
                                }
                            }
                        }
                    ]
                },
                searchConfig: {}
            }
        },
        {
            key: 'order',
            name: '订单管理',
            menuType: 'module',
            moduleType: 'custom',
            customConfig: {
                path: '/todo'
            }
        },
        {
            key: 'client',
            name: '客户管理',
            menuType: 'module',
            moduleType: 'custom',
            customConfig: {
                path: '/todo'
            }
        }
    ]
}

字段说明

  • menuType:描述菜单类型(模块 module 或 多级菜单 group)
  • moduleType:描述模块关联的模板类型,如 schema标准配置、自定义、有侧边栏、iframe第三方等

这里不做字段的过多说明,主要是思想!!!

三、DSL 在系统设计中的角色

在 Elpis 中,创建“模型”是确定系统分类的第一步。模型就像是各种业务系统的容器(例如:电商系统、人事系统等)。每个模型下可以拓展出多个模板(比如:商品管理、订单管理),模板之间可以共享基础配置,同时支持局部覆盖和个性化拓展。

结构示例:

  • 模型配置(如 model.js):定义基础配置
  • 模板目录:基于模型的配置进行扩展或重载
  • index.js:对结构进一步封装处理,供系统消费

四、Schema 模块的核心逻辑

moduleType 设置为 schema,系统会引入标准表单模板,在 schemaConfig 中读取配置,并封装成组件。

例如:

  • tableConfig:配置操作栏按钮
  • tableOption:配置表格列字段

通过组件的二次封装,这些配置项被透传至底层 UI 组件,从而实现强大的复用性与可扩展性。

此外,可以将多个配置项抽离为 option,例如:schema.option.tableConfigschema.option.searchConfig 等,以提升模块化程度。

五、实践思考

例如在二次封装 date-range 组件时,dateType 是一个关键配置项。这时可以考虑将其纳入 DSL 规范中,增强配置的一致性和标准化。当然,设计 DSL 时也要考虑是否具备通用性,不能胡乱配置。

总结

DSL 的核心价值在于将开发中的共性提取为结构化的配置,进而通过模板机制实现高度复用和快速迭代。学习与实践 DSL,不仅可以优化开发效率,也能增强系统的可维护性和扩展能力。

通过这次学习与总结,我更加理解了 DSL 的设计思想和实际落地方式,在今后的工作中也将更多尝试将其应用于实际业务系统的建设中。

❌