普通视图

发现新文章,点击刷新页面。
今天 — 2025年5月20日技术

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

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

为了写些demo,我上架了个小程序:【i不简】

作者 不简说
2025年5月20日 13:56
哈喽哇!我是小不,不简说的不。在代码世界疯狂蹦跶的 “非资深选手”🙋‍♂️!主打一个*“踩坑我来,避坑你学”*。毕竟独乐乐不如众乐乐,让大家少走弯路,才是咱的终极使命✨~ 前言 最近发送有不少小伙伴想

vue3中shallowRef有什么作用?

作者 best666
2025年5月20日 13:44

在 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怎么办,快使用?. (可选链操作符)

作者 复苏季风
2025年5月20日 13:38

我们在实际开发时候经常会遇见赋值,但是后端可能为传一个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,则默认赋值为空字符串

总结

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

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

作者 Canmick
2025年5月20日 13:16

前些日子对接过联盟一个帮派的 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 性能优化:调优策略与工具使用

作者 BitCat
2025年5月20日 13:11

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

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

参考资源


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

终身学习,共同成长。

咱们下一期见

💻

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

作者 3Katrina
2025年5月20日 12:42

📖前言

最近我接触了扣子平台,了解了可以使用扣子开发属于自己的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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

《UI界面无代码编程小示例:悬停放大》| uiotos、零代码、0代码、无代码、低代码、可视化编程、nodered、amis、appsmith、codewave

作者 UIOTOS硫火
2025年5月20日 12:15
在做实际项目时,经常有遇到这种情况:当鼠标悬停图片时,图片变大,移出时,图标变小,如下所示: 效果 「运行」 「编辑」 过程 「步骤1」拖入图片按钮并设置 勾选“移入事件启用”,所用到的属性做标记。

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

作者 1024小神
2025年5月20日 11:51

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

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谷歌,百度,微软站点地图添加

2025年5月20日 11:47

谷歌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设计与实现

作者 Acky
2025年5月20日 11:26

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 的设计思想和实际落地方式,在今后的工作中也将更多尝试将其应用于实际业务系统的建设中。

❌
❌