阅读视图
构建AI时代的大数据基础设施-MaxCompute多模态数据处理最佳实践
直播预告|从新手到贡献者:手把手教你融入开源生态
第16课:JavaScript性能优化——让代码飞起来
大型前端应用如何做系统融合?
为了写些demo,我上架了个小程序:【i不简】
vue3中shallowRef有什么作用?
在 Vue3 里,shallowRef
是一个很实用的函数,其作用是创建一种特殊的 ref 对象,它只对自身的值变化进行追踪,而不会递归追踪内部属性的变化。下面详细介绍它的功能和适用情形。
主要功能
-
浅层响应性:
shallowRef
仅对.value
的赋值操作作出响应,而内部对象的属性变化不会触发更新。 - 性能优化:在处理大型数据结构或者第三方对象(像 DOM 元素、API 响应数据)时,若无需追踪内部变化,使用它能避免不必要的响应式开销。
-
保持原始对象:它不会像
ref
那样对深层对象进行代理转换,有助于维持对象的原始状态。
典型应用场景
-
缓存大型数据
当你有大量静态数据,且不需要监听其变化时,可以使用shallowRef
来避免性能浪费。
const largeData = shallowRef(getLargeDataFromAPI()); // 数据更新时才需手动触发更新
-
集成第三方库
在集成第三方库时,使用shallowRef
可以存储库返回的实例,防止 Vue 对其进行不必要的代理。
const map = shallowRef(null);
onMounted(() => {
map.value = new MapLibreGL.Map(...); // 存储原生DOM或库实例
});
-
手动控制更新
如果你希望手动控制更新时机,以减少渲染次数,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
是很好的选择。 -
如果需要响应式地追踪内部变化,应使用普通的
ref
或reactive
。
通过合理运用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,不会报错
常见应用场景
可选链操作符在以下这些场景中经常会用到:
- 异步数据加载:像接口返回的数据结构可能不完整的情况。
- 条件渲染:在组件中需要处理未定义的 props。
- 深度嵌套对象:避免因为中间某个属性不存在而引发错误。
结合空值合并操作符
通常,可选链操作符会和空值合并操作符 ??
一起使用,以便为未定义的值设置默认值:
const token = memberStore.profile?.token ?? ''; // 若 token 为 undefined,则默认赋值为空字符串
总结
在你的代码里使用 ?.
是一种安全的编程实践,它能够优雅地处理数据可能不存在的情况,增强代码的健壮性,同时避免因未定义的属性访问而导致应用崩溃。
优化了盟友几行代码,硬要请我喝咖啡
前些日子对接过联盟一个帮派的 Java web 项目,对接人王道友提过好几次,说该项目的用户登录校验经常失灵,明明登录过了,刷新页面后又时常会跳去登录页,一直未找到原因,甚是困扰。
话已至此,一向行侠仗义的我决定趟一趟这浑水。
问题分析
由于不熟悉项目,首先让王道友介绍了项目登录的主流程,得到如下流程图。
随后花了点时间在问题复现上,最终定位到了核心代码,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 执行前返回结果,登录校验流程才能正常,否则校验异常跳去登录页。
妙啊!原来是玄学编程,有缘者登录之。
话不多说,开始设计解决方案吧。
解决方案
方案设计方向是把上述用户一致性校验环节的异步流程理顺。
源代码流程是动态创建 iframe 并监听其 message 回调,并以轮询方式重试,存在的问题有:
- 同步异步时序错乱(核心问题,上文分析过)
- 未考虑 iframe 加载异常场景、未设置 refreshFrame 轮询上限,可能进入死循环
- 未校验 message 消息合法性,可能拿到错误信息
- 未正常处理 getUserId 多次调用场景
现在以异步编程的方式重新设计下流程:
编码实战
基于上述流程上代码
// 执行程序单例
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;
}
完成代码,接入测试下:
正常场景
超时场景
竞态场景
完美,项目交由王道友测试后,其嘴角微微上扬,内心亦起了一丝丝敬意。
小结
用户登录校验经常失灵,根本原因在于用户一致性校验环节的异步流程错乱。
本文通过梳理原有检验流程,结合promise、async/await 异步函数重构了代码,并考虑到了多重边界场景,最终道友们的登录缘分,终可不必再靠玄学。
不多说了,王道友已经买好咖啡在门口了,我去去就回……
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 执行、样式计算、布局、绘制和合成等活动的详细时间线。
使用方法:
- 打开 DevTools(Windows/Linux: F12 或 Ctrl+Shift+I, Mac: Command+Option+I)
- 切换到 Performance 选项卡
- 点击左上角的"录制"按钮(圆形记录图标)
- 在页面上执行需要分析的操作(如滚动、点击、输入等)
- 点击"停止"按钮结束录制
- 分析生成的性能报告
你也可以使用 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 的任务)
优化决策方法:
查看性能记录时,应重点关注以下几点:
-
长任务:查找持续时间超过 50ms 的任务(在火焰图中显示为红色标记),这些任务会阻塞主线程,导致界面无响应。
-
布局抖动:查找反复触发布局(紫色事件)的模式,这通常表示代码中存在强制重排的问题。
-
过多垃圾回收:频繁的垃圾回收(标记为 GC 的灰色事件)表明可能存在内存管理问题。
-
阻塞渲染的资源:检查资源加载是否阻塞了关键渲染路径。
内存面板(Memory Panel)实用指南
内存泄漏是导致页面长时间运行后性能不断下降的主要原因之一。Chrome DevTools 的 Memory 面板提供了强大的工具来分析内存使用情况:
使用方法:
- 打开 DevTools 并切换到 Memory 选项卡
- 选择分析类型:
- Heap Snapshot(堆快照):捕获 JavaScript 对象和相关 DOM 节点的完整内存快照
- Allocation Timeline(分配时间线):记录随时间推移的内存分配情况
- Allocation Sampling(分配采样):低开销的内存分配采样
内存泄漏检测步骤:
- 基线快照:在页面加载完成后立即拍摄一个堆快照作为基准
- 操作执行:执行可能导致内存泄漏的操作(如打开/关闭模态框、切换页面等)
- 强制垃圾回收:点击内存面板中的垃圾桶图标强制执行垃圾回收
- 比较快照:拍摄第二个快照,并使用比较功能(选择 "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
WeakMap
和 WeakSet
是解决特定类型内存泄漏的利器,它们持有对对象的弱引用,不会阻止被引用对象的垃圾回收:
// 使用 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
的实际应用场景包括:
- 存储 DOM 节点的额外数据,而不影响节点的生命周期
- 实现私有属性和方法
- 缓存计算结果,但不阻止对象被回收
JavaScript 内存管理最佳实践
除了解决具体的内存泄漏问题,还应遵循以下最佳实践:
- 定期检查内存使用情况:将内存分析纳入开发和测试流程
- 避免全局变量:全局变量不会被垃圾回收,除非页面刷新
- 使用事件委托:减少事件监听器数量
- 合理使用闭包:确保闭包不会无意中引用大型对象
- 注意 DOM 引用:不要在长期存在的对象中保存对临时 DOM 元素的引用
- 定期进行代码审查:特别关注内存管理相关问题
重排重绘:渲染性能优化
理解浏览器的渲染流程是优化视觉性能的基础。这一流程通常包括以下步骤:
- JavaScript: 执行 JavaScript 代码,可能改变 DOM 或 CSSOM
- Style: 根据 CSS 规则计算元素的样式
- Layout (重排): 计算元素的几何位置和大小
- Paint (重绘): 填充元素的像素
- Composite: 将各层合成并显示在屏幕上
重排(Layout/Reflow)和重绘(Paint/Repaint)是渲染过程中最消耗性能的步骤:
- 重排:当元素的几何属性(如宽度、高度、位置)发生变化时触发,需要重新计算布局
- 重绘:当元素的视觉属性(如颜色、透明度)发生变化时触发,不改变布局
检测重排重绘问题
Chrome DevTools 提供了多种方法来识别重排重绘问题:
-
Performance 面板:
- 重排在火焰图中显示为紫色的"Layout"事件
- 重绘显示为绿色的"Paint"事件
- 这些事件时间过长或频率过高都表明存在性能问题
-
渲染面板:
- 打开 DevTools > 按 Esc 键 > 在出现的抽屉面板中选择"Rendering"
- 启用"Paint flashing"可以高亮显示重绘区域
- 启用"Layout Shifts"可以显示布局偏移区域
-
性能监控:
- 开启 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 的 transform
和 opacity
属性是特殊的,它们的变化通常只触发合成阶段,跳过布局和绘制步骤:
/* 不佳实践:更改位置属性导致重排 */
.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. 实施优化
遵循"最大收益原则",先处理影响最显著的问题:
-
优先级划分:
- P0: 影响核心功能的严重性能问题
- P1: 影响用户体验但不阻碍核心功能的问题
- P2: 小型优化和改进
-
增量实施:
- 每次修改后测量性能改进
- 确保不引入新的性能问题
- 建立性能回归测试
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 性能优化是一个不断发展的领域,需要持续学习和实践。通过本文介绍的诊断工具、优化策略和最佳实践,我希望能为你提供一个全面的性能优化框架。
和许多事情一样,性能优化也不是一蹴而就的,而是需要贯穿整个开发生命周期的持续实践。
参考资源
- Web Vitals - Google 的 Web 性能指标体系
- Chrome DevTools 官方文档
- JavaScript 性能优化 - Web.dev 上的性能优化指南
- 高性能 JavaScript - Nicholas Zakas 的经典著作
- You Don't Know JS - 深入理解 JavaScript 的系列书籍
如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇
终身学习,共同成长。
咱们下一期见
💻
【CodeBuddy】今天520,我只教你一遍。
我用扣子开发了一个属于自己的Agent....
📖前言
最近我接触了扣子平台,了解了可以使用扣子开发属于自己的Agent,所以我花了一些时间了解了如何从零到一,开发一款属于属于自己的Agent,并且成功开发了R&B音乐精灵,使用这款Agent,可以随时随地听自己喜爱的R&B的音乐,而且还能和它交流R&B风格的歌手,它还可以给你每日推荐适合你的R&B音乐!🎵🎵🎵
🤖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该如何操作吧!👇👇
📝注册并创建
首先,大家需要进入扣子开发平台,进行用户注册,然后选择导航栏的开发平台,选择快速开始的按钮。
进入到主界面后,咱们选择左上角的+号,然后选择选择创建智能体就可以开始创建啦!
🆕创建初始化
在创建Agent的初始化,扣子网页会提出一个信息栏,有标准创建和AI创建,本文就着重于讲解标准创建的方式,在该方式中,需要你填写该Agent的名字和简介,咱们只需要根据自己的Agent的内容与特点写一个名称和一段功能简介就行啦,然后工作空间默认选择个人空间就行了。
你可能会注意到,最下方有个图标,并且旁边还有个button,提示我们可以通过AI自动生成咱们产品的图标,咱们可以点击然后,它就会调用API,帮我们自动生成一个图标的图片啦,AI会根据你的Agent的名称和描述来生成合适的图标,但是生成的图标也会存在随机性,就像抽卡一样,不过如果你不满意的话,可以多生成几次!实在不行,咱们也可以选择自己上传的方式来进行图标的上传。
⚙️配置Agent
在创建Agent之后,咱们会进入到Agent的编排页面,你会发现:
- 在左侧人设与回复逻辑面板中描述智能体的身份和任务。
- 在中间技能面板为智能体配置各种扩展能力。
- 在右侧预览与调试面板中,实时调试智能体。
你可以通过这三个区域的配置我们的Agent,让他变成你想要的样子。
💡编写提示词
配置智能体的第一步也是最重要的一步,就是编写提示词,也就是智能体的人设与回复逻辑。智能体的人设与回复逻辑定义了智能体的基本人设,此人设会持续影响智能体在所有会话中的回复效果。建议在人设与回复逻辑中指定模型的角色、设计回复的语言风格、限制模型的回答范围,让对话更符合用户预期。
咱们首先可以通过自己编写提示词,如果大家对提示词想详细了解,可以去看扣子官方对提示词的解释
这里作者先通过自己学习过的Prompt的书写规范,简要地按照人设-任务-步骤-注意事项,来书写一段提示词
然后当我们觉得自己写的提示词不够满意,觉得不够系统的话,可以点击右上角的button,用AI帮我们自动优化。
优化后选择替换,就可以一键替换到你刚刚输入的提示词啦
我们可以看到AI就是专业,用规范的格式来优化好了我们刚刚写的提示词 实际上这种格式我们也可以在“提示词库”进行查看,咱们可以选择不同的场景来按照提示词写,这样子的话,我们写出来的Prompt就能十分专业且准确啦!
🎛️详细配置各种能力
好了,编写提示词至此咱们就完成啦,接下来就是要在技能面板中为智能体配置各种能力了。
我们可以看到,在配置能力时,分为四种类型的配置,下面是扣子官方给出的对这四种类型的介绍。
- 技能:技能是智能体的基础能力,你可以在搭建智能体时通过插件、工作流等方式拓展模型的能力边界。
- 知识:知识库功能支持添加本地或线上数据供智能体使用,以提升大模型回复的可用性和准确性。更多信息,参考知识库概述。
- 记忆:模型最大对话轮数是有限的,记忆相关的能力可以为模型提供可以反复调用的长期记忆,让智能体的回复更加个性化。
- 对话体验:对话体验可以增强用户和智能体对话过程中的交互效果。
接下来,我将会对这四种类型进行详细讲解
技能设置
首先,咱们来介绍一下技能吧,技能是智能体的基础能力,你可以在搭建智能体时通过插件、工作流等方式拓展模型的能力边界。在技能中每个功能的作用如下:
功能 说明 插件 通过 API 连接集成各种平台和服务,扩展了智能体能力。扣子平台内置丰富的插件供你直接调用,你也可以创建自定义插件,将你所需要的 API 集成在扣子内作为工具来使用。更多信息,参考插件介绍。例如使用新闻插件来搜索新闻,使用搜索工具查找在线信息等。 工作流 工作流是一种用于规划和实现复杂功能逻辑的工具。你可以通过拖拽不同的任务节点来设计复杂的多步骤任务,提升智能体处理复杂任务的效率。更多信息,参考工作流介绍。 触发器 触发器功能支持智能体在特定时间或特定事件下自动执行任务。更多信息,参考触发器。
1.插件的选择
我们可以看到,有大量的插件供我们选择,我们也可以通过搜索的方式来进行插件的查找
作者选择了网易云音乐的插件,添加了一些需要用到的接口
2.工作流和触发器的选择
作者这里在选择插件之后,工作流和触发器就使用默认的设置了。大家也可以通过自己的定制化去选择
知识设置
知识的选择分为文本、表格、照片,支持添加本地或线上的数据供智能体使用,这玩意就是一个数据库,咱们可以添加各种各样的知识,以提升智能体的信息检索的范围。让咱们的智能体“学识深渊”。
这里我的软件主要通过上网搜索来进行信息的查询,所以这里就默认不额外添加知识。
记忆设置
模型最大对话轮数是有限的,记忆相关的能力可以为模型提供可以反复调用的长期记忆,让智能体的回复更加个性化。
下面是记忆的官方介绍:
功能 | 说明 |
---|---|
变量 | 变量功能可用来保存用户的语言偏好等个人信息,让智能体记住这些特征,使回复更加个性化。 |
数据库 | 数据库功能提供了一种简单、高效的方式来管理和处理结构化数据,开发者和用户可通过自然语言插入和查询数据库中的数据。同时,也支持开发者开启多用户模式,以实现更灵活的读写控制。更多信息,参考数据库。 |
长期记忆 | 长期记忆功能模仿人类大脑形成对用户的个人记忆,基于这些记忆可以提供个性化回复,提升用户体验。更多信息,参考长期记忆。 |
文件盒子 | 文件盒子提供了多模态数据的合规存储、管理以及交互能力。通过文件盒子,用户可以反复使用已保存的多模态数据。更多信息,参考文件盒子。 |
这里咱们的音乐软件暂时不设置记忆功能,后续若有需要再进行补充
对话体验设置
对话体验里存在很多项设置
下面是在对话体验中的各个功能的说明:
功能 说明 开场白 设置智能体对话的开场语,让用户快速了解智能体的功能。例如 我是一个旅行助手智能体,我能帮助你计划行程和查找旅行信息。详情请参考开场白。 用户问题建议 智能体每次响应用户问题后,系统会根据上下文自动提供三个相关的问题建议给用户使用。 快捷指令 快捷指令是开发者在搭建智能体时创建的预置命令,方便用户在对话中快速、准确地输入预设的信息,进入指定场景的会话。详情请参考快捷指令。 背景图片 智能体的背景图片,在调试和商店中和智能体对话时展示,令对话过程更沉浸,提高对话体验。 语音 在搭建智能体时,你可以配置语音功能,以提供更自然和个性化的交互体验。配置语音时,需要选择语言和音色,确保智能体能够以用户喜爱的方式进行交流。此外,还支持开启语音通话功能,使用户能够通过语音与智能体进行实时互动,无需手动输入文字。 用户输入方式 在搭建智能体时,可以选择多种用户输入方式,以满足不同用户的需求和使用场景。用户输入方式支持打字输入、语音输入和语音通话。仅开启了语音通话功能,才支持选择语音通话输入方式。
1. 开场白
咱们为咱们的Agent设置一个开场白,让它能够更主动给咱们的用户打招呼
同时咱们还能在开场白里设置预置问题,通过这种方式,可以引导用户进行提问
2. 用户问题建议
当咱们的用户输入的信息不完整或模糊时,Agent自动生成追问或建议选项,引导用户补充关键信息或明确需求,比如咱们,提问一个不完整的信息,“我想听歌”,此时开启后,Agent可能会回答“ Agent:您想听什么类型的音乐? ➔ [ 今日热门 ] [ 心情放松 ] [ 运动健身 ] ”这种将模糊需求转化为明确任务的选项。
所以咱们就打开这个功能吧!
3.快捷指令
快捷指令是对话输入框上方的按钮,配置完成后,用户可以快速发起预设对话,这里咱们就创建一个测试指令,来测试一下基本的使用
4.背景图片、语音、用户输入方式
这三项就是在扣子平台的一些支持功能了,这里提一下,扣子支持用户可以以语音通话的方式与Agent进行交互,所以咱们可以设置不同的机器人音色,提升用户的体验。
⚖️模型对比调试
最后咱们选择一个模型即可,大家可以通过对比的方式来进行选择适合自己的大模型。
🚀发布
最后咱们可以选择发布我们的Agent到指定的平台啦!方便我们在别的平台也能进行使用。
🎯总结
本文主要讲解关于Agent开发的一个基本使用流程,开发的整个过程是几乎一步步手动去生成的,当然咱们也可以在初始化之后,选择让AI一键生成,但是本文主要是想让你了解各个概念以及如何使用,各个功能的使用较为基础,待读者再精进后,会再进行更新,介绍一下利用扣子开发的一些更进阶的内容,希望在看完本文之后,您能够学会开发Agent的基本使用!😊😊😊
🌇结尾
本文部分内容参考扣子官方的:参考文档
感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生
(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)
作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
《UI界面无代码编程小示例:悬停放大》| uiotos、零代码、0代码、无代码、低代码、可视化编程、nodered、amis、appsmith、codewave
如何统计某个网站加载资源或者发送请求的时候,都使用了哪些域名
要统计某个网站在加载资源或者发送请求时使用的所有域名,可以通过以下方法进行:
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
)
- 静态URL(例:
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%
百度-网站添加
第一步:注册百度站长平台账号
注册一个百度站长平台账号,如果有百度的账号也可以直接用,然后登录这个账号。
第二步:在百度搜索引擎搜索你的网站
在百度搜索框,输入如下内容搜索你的网站:
site:your_domain
比如我这里输入site:ideaflow.top,如果没有找到,表明你的网站没有被百度收录。
第三步:提交网址
百度添加网站地址
根据步骤提交资源,以下结果:
第四步:添加站点资源链接
请注意限制
Bing-网站添加
Bing是全球领先的搜索引擎之一,让自己的网站在Bing上快速被索引,是很多网站站长的首要目标。在这里,我将分享如何使用Bing Webmaster工具提交sitemap,以助你快速实现Bing搜索引擎对你网站的收录。
第一步:创建Bing Webmaster工具账户
登录后首次:
第二步:添加站点地图
第三步:确认网站被收录
site:yourdomian.com
谷歌-网站添加
谷歌需要魔法登录~ 谷歌官网seo介绍
第一步添加网站资源
第二步:提交站点sitemap
提交后的效果:
第三部:验证
site:yourdomain.com
基于 Elpis下的DSL设计与实现
DSL 设计理解与总结
前言
在工作中我曾开发过类似的功能,因此在学习哲哥课程时,对其表达的思想能够较为容易地理解。例如,工作中的菜单栏是通过后端配置来渲染的,接口返回的信息包含了图标、路由跳转信息、描述等;弹窗模块中的权限渲染,也是依据后端配置的表结构实现权限联动,这种基于配置的开发模式大幅减少了后续的维护成本,基本实现了“一次开发,终身使用”。
不过在课程的学习过程中,由于对 DSL 结构不够清晰,虽然可以理解其意图,却难以记住各个配置的作用与处理方式。随着学习的深入,我才逐渐理解了这些配置结构的价值和意义。
核心思想:学会偷懒
通过一部分配置,减少重复的工作,把时间放到更有意义的地方。
一、什么是 DSL
在学习哲哥课程的过程中,最初不清楚“DSL”到底是什么,只知道它是一种配置规范。直到学习到里程碑3,才去深入了解。
DSL(Domain Specific Language) ,即领域特定语言,是一种专为特定问题领域设计的编程语言或语言规范。不同于通用编程语言(如 Python、Java),DSL 更专注于特定业务场景,提供更贴合该领域的语法和语义,使得配置和表达更加直观清晰。
在 DSL 配置中,字段结构严谨、类型明确,使人一眼就能明白每个配置的意图及其影响。
类比理解模型与模板
在学习完里程碑3时,我觉得可以将 DSL 中的“模型”理解为装不同类型玩具的盒子:有的盒子用来装汽车,有的用来装玩偶,还有的用来装积木。每个盒子内部会包含某类玩具的通用特征,比如所有汽车都有四个轮子、方向盘。这些通用特征可以作为模型的基础配置,这些“盒子”就相当于系统中的“模型(Model)”。
而对于某些特殊类型的玩具(如消防车、救护车),虽然它们都属于“汽车”模型,但还会有各自的特殊属性(比如消防车上有洒水装置、救护车有急救装置等等)。这时,我们就可以在模型的基础上,通过模板的方式扩展或重载配置,实现灵活定制,对应类型的玩具就相当于模型下的模板。
目录结构如下图所示:
我们基于不同的模型来延伸出各式各样的模板。
二、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.tableConfig
、schema.option.searchConfig
等,以提升模块化程度。
五、实践思考
例如在二次封装 date-range
组件时,dateType
是一个关键配置项。这时可以考虑将其纳入 DSL 规范中,增强配置的一致性和标准化。当然,设计 DSL 时也要考虑是否具备通用性,不能胡乱配置。
总结
DSL 的核心价值在于将开发中的共性提取为结构化的配置,进而通过模板机制实现高度复用和快速迭代。学习与实践 DSL,不仅可以优化开发效率,也能增强系统的可维护性和扩展能力。
通过这次学习与总结,我更加理解了 DSL 的设计思想和实际落地方式,在今后的工作中也将更多尝试将其应用于实际业务系统的建设中。