96SEO 2026-04-20 13:10 0
大家好 👋,我是 Moment。Zui近我正忙着捣鼓 DocFlow,一个基于 AI 场景的协同文档平台。这玩意儿集成了 Tiptap 富文本编辑、NestJS 后端服务,还有实时协作和智Neng化工作流。在开发这个复杂系统的过程中,我深刻体会到:Ru果你kan不见用户在经历什么优化就无从谈起。这就是为什么我们需要一套靠谱的前端监控方案。

市面上确实有现成的 SaaS 服务,但对于追求极致定制化和数据隐私的团队来说自研 SDK 依然是必修课。今天我就把我在构建 DocFlow 过程中沉淀下来的经验摊开来讲,带你从零开始,手搓一个名为“四维”的前端监控 SDK。我们要Zuo的,不仅仅是收集数据,而是用上帝视角kan清页面里发生的每一丝细微波动。
在动手写第一行代码之前,咱们得先在脑子里搭好骨架。别一上来就陷进细节里先kankan大局。一个成熟的监控采集层,其实Ke以拆解为四个核心板块:配置管理、缓存与上报策略、各类 observer 与事件钩子,以及一个统一的入口类。
你Ke以把数据流想象成一条河流:从页面发生的各种事件,汇聚到内存中的队列,Zui后在合适的时机流向服务端。为了方便记忆,我把这个 SDK 命名为“四维”,寓意着从时间、性Neng、错误、行为四个维度去还原现场。
下面这张图展示了数据流与模块的边界,这和我们即将实现的代码逻辑是完全对齐的。
二、 地基工程:配置与初始化万事开头难,但配置模块相对简单。我们需要一个地方来集中管理默认值,同时允许业务方在运行时覆盖它们。比如上报地址、项目 ID、采样率,还有环境标识。
这里我定义了一个 MonitorConfig 接口。为了防止魔法字符串满天飞,建议把配置收敛在 config.ts 里。业务侧只需要调用 setConfig 就Neng覆盖默认值,非常清爽。
export interface MonitorConfig {
reportUrl: string;
appId: string;
userId?: string;
projectName?: string;
release?: string;
environment?: "development" | "staging" | "production";
sampleRate?: number;
}
const config: MonitorConfig = {
reportUrl: "http://localhost:3000/report",
appId: "fd-example",
projectName: "fd-example",
environment: "development",
sampleRate: 1,
};
export function setConfig: void {
Object.assign;
}
export function getConfig: Readonly {
return config;
}
有了配置,接下来就是统一入口类 FourDimension。这个类的职责hen简单:在初始化时拉起各个子模块。为了防止脚本被多次执行导致监听器重复注册,务必加一个 inited 幂等守卫。
import { initPerformance } from "./performance";
import { initBehavior } from "./behavior";
import { initError } from "./error";
export class FourDimension {
private inited = false;
init: void {
if return;
this.inited = true;
initPerformance;
initError;
initBehavior;
}
}
业务代码里异步加载这个 SDK,然后 new FourDimension.init 就完事了简单利落。
性Neng是监控的重头戏。Google 提出的 Core Web VitalsYi经成了行业标准,主要包括 LCP、INP和 CLS。虽然 FIDYi经被 INP 取代,但为了历史数据对比,我们也Ke以顺手采集。
1. 绘制时机:FP 与 FCP首屏渲染快不快,直接决定了用户是留下来还是关掉标签页。FP和 FCP是诊断“白屏”和“空刷背景”的关键指标。我们Ke以通过 PerformanceObserver 监听 paint 类型来获取。
注意,拿到 FCP 后就Ke以断开观察了没必要一直挂着。另外为了兼容性,我们在挂观察者之前Zui好先探测一下环境是否支持。
function safeObserverSupported: boolean {
return typeof PerformanceObserver !== "undefined";
}
export function observePaint: void {
if ) return;
const obs = new PerformanceObserver => {
for ) {
if (
entry.name !== "first-paint" &&
entry.name !== "first-contentful-paint"
)
continue;
const json = entry.toJSON;
enqueue({
type: "performance",
subType: "paint",
paintName: entry.name,
startTime: json.startTime,
pageURL: location.href,
});
if {
obs.disconnect;
scheduleFlush.reportUrl);
break;
}
}
});
obs.observe;
}
2. 加载体验:LCP 与导航时序
LCP 代表页面主要内容加载完成的时间点。这个指标在页面生命周期内可Neng会geng新,所以规范规定“Zui后一次汇报的 LCP 条目”才是有效的。简单点,我们Ke以在回调里每次dou上报Zui新值,让服务端去取Zui后一次。
这里有个坑:LCP 回调触发时entry.element 可NengYi经被移除了千万别直接操作 DOM,只上报 tagName 和资源 URL 就行。
export function observeLcp: void {
if ) return;
const obs = new PerformanceObserver => {
const entries = list.getEntries as PerformanceEntry;
const last = entries as LargestContentfulPaint &
PerformanceEntry;
const json = last.toJSON;
enqueue({
type: "performance",
subType: "lcp",
startTime: json.startTime,
element: last.element?.tagName,
url: "url" in last ? String.url ?? "") : "",
pageURL: location.href,
});
scheduleFlush.reportUrl);
});
obs.observe;
}
除了 LCP,我们还需要geng细粒度的网络时序。别再傻傻地自己在事件里用 Date.now 算差值了直接用 PerformanceNavigationTiming。它Neng提供 DNS 查询、TCP 连接、TTFB等精确数据。
export function collectNavigationTiming: void {
const = performance.getEntriesByType(
"navigation",
) as PerformanceNavigationTiming;
if return;
enqueue({
type: "performance",
subType: "navigation",
dns: nav.domainLookupEnd - nav.domainLookupStart,
tcp: nav.connectEnd - nav.connectStart,
ttfb: nav.responseStart - nav.requestStart,
domContentLoaded: nav.domContentLoadedEventEnd - nav.fetchStart,
load: nav.loadEventEnd - nav.fetchStart,
pageURL: location.href,
});
scheduleFlush.reportUrl);
}
记得在 load 事件后再调一次确保 loadEventEnd Yi经有值。对于单页应用的软导航,这招不管用,得结合路由钩子另算,别把 PV 和导航耗时混在一起硬解释。
CLSZui烦人,用户正要点按钮,按钮突然跑了这就是 CLS 高的锅。采集时记得过滤掉用户操作附近的偏移,别把用户自己造成的布局变化算成体验问题。
export function observeCls: void {
if ) return;
let clsScore = 0;
const obs = new PerformanceObserver => {
for as PerformanceEntry) {
const ls = entry as LayoutShift & {
hadRecentInput?: boolean;
value?: number;
};
if continue;
clsScore += ls.value ?? 0;
enqueue({
type: "performance",
subType: "cls",
value: ls.value,
cumulativeLayoutShift: clsScore,
pageURL: location.href,
});
}
scheduleFlush.reportUrl);
});
obs.observe;
}
资源加载情况也hen重要。通过 PerformanceObserver 监听 resource 类型,我们Neng拿到每个静态资源的耗时、大小、协议等信息。Ru果数据量太大,Ke以在客户端按域名白名单或耗时阈值过滤一下别把 CDN 的日志全搬过来了。
错误监控是救火队员。我们需要把资源错误、JS 运行时错误和 Promise 拒绝一网打尽。
1. 全局错误与资源加载失败window.addEventListener 是个万Neng抓手。在捕获阶段,我们Neng通过 event.target 拿到加载失败的 scriptimg 或 link 元素。Ru果是纯 JS 错误,target 通常为空,这时就取 message 和 stack。
这里有个细节:获取事件路径时别用Yi废弃的 event.path,要用标准的 event.composedPath,这样在 Shadow DOM 里也Neng准确定位。
function elementPath: string {
const path = typeof ev.composedPath === "function" ? ev.composedPath : ;
return path
.filter: n is Element => n instanceof Element)
.map => el.tagName);
}
export function initGlobalErrorHandlers: void {
window.addEventListener(
"error",
=> {
const t = ev.target;
if (
t &&
t instanceof HTMLElement &&
(t instanceof HTMLImageElement ||
t instanceof HTMLScriptElement ||
t instanceof HTMLLinkElement)
) {
const url =
"src" in t && t.src ? t.src : "href" in t && t.href ? t.href : "";
enqueue({
type: "error",
subType: "resource",
url,
tag: t.tagName,
paths: elementPath,
pageURL: location.href,
});
scheduleFlush.reportUrl);
return;
}
if return;
enqueue({
type: "error",
subType: "js",
message: ev.message,
filename: ev.filename,
lineno: ev.lineno,
colno: ev.colno,
stack: ev.error instanceof Error ? ev.error.stack : "",
pageURL: location.href,
});
scheduleFlush.reportUrl);
},
true,
);
// ... Promise rejection handler below
}
2. Promise 拒绝与网络请求劫持
未处理的 Promise 拒绝通过 unhandledrejection 监听。上报时尽量带上 reason 的栈信息,字符串化时小心大对象导致序列化问题。
光监听全局还不够,现代代码里大量使用 fetch。我们Ke以包装 window.fetch 和 XMLHttpRequest.prototype 来拦截网络请求。包装 fetch 时要注意,别假设调用方不克隆 Response 去读流,监控侧只读 status 和头就行,别和业务方抢 body。
export function patchFetch: void {
const orig = window.fetch.bind;
window.fetch = async => {
const start = performance.now;
const req = input instanceof Request ? input : new Request;
try {
const res = await orig;
const end = performance.now;
enqueue({
type: "performance",
subType: "fetch",
url: req.url,
method: req.method,
status: res.status,
duration: end - start,
pageURL: location.href,
});
scheduleFlush.reportUrl);
return res;
} catch {
const end = performance.now;
enqueue({
type: "error",
subType: "fetch",
url: req.url,
method: req.method,
duration: end - start,
message: err instanceof Error ? err.message : String,
pageURL: location.href,
});
scheduleFlush.reportUrl);
throw err;
}
};
}
五、 行为分析:用户在Zuo什么?
除了“崩没崩”和“卡不卡”,我们还得知道用户“干了啥”。PV/UV 是基础,点击热力图和停留时长Neng帮我们Zuo产品决策。
1. PV/UV 与会话追踪PV 在每次路由或首屏进入时打一条,带上 document.referrer。UV 必须在服务端聚合,客户端只Neng提供匿名 ID。我们Ke以用 sessionStorage 存一个会话 ID,首次访问时用 crypto.randomUUID 生成,兼容老环境就降级到时间戳加随机串。
单页应用要在路由变化时手动调一次 reportPv,只依赖首屏加载会严重低估流量。
function createSessionId: string {
if {
return crypto.randomUUID;
}
return `${Date.now}-${Math.random.toString.slice}`;
}
let sessionId = sessionStorage.getItem ?? "";
if {
sessionId = createSessionId;
sessionStorage.setItem;
}
export function reportPv: void {
enqueue({
type: "behavior",
subType: "pv",
pageURL: location.href,
referrer: document.referrer,
sessionId,
});
scheduleFlush.reportUrl);
}
2. 点击与停留时长
点击监听一定要防抖,不然用户长按或滑动时会产生暴风上报,把队列撑爆。坐标和 outerHTML 体积要限长,敏感页面只保留 data- 业务埋点键名。
停留时长别只在 beforeunload 算一次差值,太不准了。用 visibilitychange 记录可见累计时间,特别是移动端后台化场景。离开页面时再发一条汇总,带上 visibleMs。
let visibleAccum = 0;
let lastVisibleStart = performance.now;
document.addEventListener => {
const now = performance.now;
if {
lastVisibleStart = now;
} else {
visibleAccum += now - lastVisibleStart;
}
});
window.addEventListener => {
if {
visibleAccum += performance.now - lastVisibleStart;
}
enqueue({
type: "behavior",
subType: "dwell",
visibleMs: Math.round,
pageURL: location.href,
});
flushQueue.reportUrl, true);
});
六、 数据上报:稳准狠的传输管道
数据采来了怎么发出去是个大学问。我们的目标是:对主线程影响Zui小,且在页面卸载时也Neng尽力发出。
1. 队列与空闲调度别来一条发一条,那样请求太多。我们搞个内存队列,攒一批再发。利用 requestIdleCallback 在浏览器空闲时发送,Ru果不支持就降级到 setTimeout。
type ReportPayload = Record;
const queue: ReportPayload = ;
let flushTimer: ReturnType | null = null;
export function enqueue: void {
queue.push;
}
export function flushQueue: void {
if return;
const batch = queue.splice;
const body = JSON.stringify;
if {
sendReport;
return;
}
const run = => sendReport;
if {
requestIdleCallback;
} else {
setTimeout;
}
}
2. 传输通道的降级策略
这是重中之重。navigator.sendBeacon 是首选,它异步、不阻塞主线程,且在页面卸载时依然可靠。它发的是 POST,适合带 Blob。Ru果它返回 false 或者不支持,就降级到 1x1 图片 GET,Zui后再考虑带 keepalive: true 的 fetch。
export function isSupportSendBeacon: boolean {
return (
typeof navigator !== "undefined" &&
typeof navigator.sendBeacon === "function"
);
}
export function reportImage: void {
const qs = encodeURIComponent);
const img = new Image;
img.src = `${url}?reportData=${qs}`;
}
export function sendReport: void {
if ) {
const blob = new Blob;
const ok = navigator.sendBeacon;
if return;
}
reportImage as unknown);
}
离开页面时优先依赖 pagehide 和 visibilitychange 触发 flush,比 beforeunload 稳得多。Ru果两个事件dou触发 flush,记得在 flushQueue 里Zuo空队列检查,避免重复上报。
在把 SDK 推到生产环境之前,建议对照下面这张表在 Chrome 和目标浏览器里跑一遍:
跨域核对: 跨域资源Ru果没有 Timing-Allow-Origin,hen多细粒度时长会被浏览器抹成 0。这不是 SDK 坏了是安全策略。对比同源和 CDN 资源的 transferSize 等字段就Neng发现。
数据量级: 单次 payload 别太大,几十 KB 是个比较安全的量级,尽量压缩 body。
隐私合规: 采集前想清楚有没有涉及个人信息,错误栈和 URL 要不要脱敏。
指标对齐: 在 Chrome DevTools 的 Performance 面板里跑一遍,把 SDK 上报的 LCP、CLS 值和面板里的对比,数量级得一致。
搭建前端监控 SDK 不是一蹴而就的,它需要随着业务演进不断打磨。从Zui基础的“可运行”,到后来的“高精度”、“低侵入”,每一步dou是对工程化Neng力的考验。希望这篇实战指南Neng帮你少走弯路,让你的应用性Neng问题无处遁形。
Ru果你对 AI 全栈开发、文档编辑器、前端工程化或者 React 源码感兴趣,欢迎添加我的微信 yunmz777 一起交流。觉得 DocFlow 项目还不错的话,也欢迎给个 star ⭐。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback