在开发功能堆砌型网站的过程中,几乎每个前端开发者都遇到过这样一个困境:
功能越加越多,代码越写越乱,性能越来越差。
每次修改都像拆炸弹,一不小心就让整个项目“爆掉”。
面对这样的局面,我们不禁要问:如何破局?
今天,就让我们通过一个埋点项目,来重新审视前后端开发中的优化思路,
看看如何在不推翻重写的前提下,让系统重新轻盈起来。
🚀 一、前端传输的启示:物尽其用,借助浏览器的力量
埋点系统的第一步,就是前端数据上报。
看似只是“发个请求”,但其中隐藏着性能与可靠性的问题。
🌿 1. 控制传输内容大小
传输内容越大,延迟越高、丢包越多。
因此在埋点设计时,应当只保留必要字段,尽量减少冗余。
这不仅提升了性能,也减轻了后端压力。
⚙️ 2. 选择高效的传输协议
如今,主流浏览器已支持 HTTP/3。
只要服务端接口开启该协议,就能显著提升传输效率与连接复用。
⚡ 小结:传输优化的核心在于“减负 + 提速”。
🧠 3. 善用浏览器的能力——navigator.sendBeacon()
sendBeacon() 是浏览器提供的一种异步、非阻塞的数据发送方式。
它在页面卸载(unload)或跳转时依然能完成请求,非常适合埋点上报。
可以把它理解成浏览器对开发者说的一句话:
“别担心这些琐碎的发送任务,交给我吧。”
既然浏览器已经替我们提供了可靠的上报机制,为什么还要手动造轮子呢?
下面是一个简洁高效的实现示例 👇
function send(api, metric) {
const body = JSON.stringify(metric);
if (document.URL.startsWith("https://www.laiys.com")) {
api = "https://analysis.laiys.com/api/v1/" + api;
} else {
api = "https://localanalysis.laiys.com/api/v1/" + api;
}
if (navigator.sendBeacon && navigator.sendBeacon(api, body)) {
// sendBeacon 成功发送
} else {
// 兼容方案:使用 XHR
xhrsend(api, body);
}
}
function xhrsend(api, body) {
const xhr = createXHR();
xhr.open("POST", api, true);
xhr.setRequestHeader("Content-type", "application/json;charset=UTF-8");
xhr.send(body);
}
function createXHR() {
const XHR = [
() => new XMLHttpRequest(),
() => new ActiveXObject("Msxml2.XMLHTTP"),
() => new ActiveXObject("Msxml3.XMLHTTP"),
() => new ActiveXObject("Microsoft.XMLHTTP")
];
for (let i = 0; i < XHR.length; i++) {
try {
return XHR[i]();
} catch (e) {}
}
}
✅ 核心思路:
- 优先使用浏览器原生的
sendBeacon,提高可靠性; - 若不支持或发送失败,则自动回退到 XHR。
这就是“物尽其用”的最佳体现——
让浏览器替你干更多的活!
🧩 二、后端接口的优化:化繁为简,让专业的人做专业的事
埋点系统的后端,往往承受着高并发请求的压力。
接口如果直接操作数据库,很容易出现 I/O 阻塞和性能瓶颈。
要解决这个问题,关键是简化职责。
🎯 1. 接口的唯一职责:接收数据
接口的核心任务只有一个:接收请求并保证数据可靠落地。
它不该负责复杂的业务逻辑或数据库写入。
🔄 2. 解耦存储与接收:引入消息队列
正确的架构思路是:
让接口只负责“接收”,存储交给**消息队列(MQ)**来异步处理。
Kafka 是一个非常适合埋点系统的 MQ。
它能高效处理海量消息,起到“削峰填谷”的作用,
并能与 ClickHouse 等分析型数据库无缝衔接。
⚡ 3. 使用 Go + Kafka 实现高并发接收
Go 语言以并发性能著称,非常适合做高并发接口层。
下面是一段 Kafka 客户端封装示例 👇
package service
import (
"context"
"log"
"github.com/segmentio/kafka-go"
"github.com/spf13/viper"
)
type Client struct {
host string
instance *kafka.Conn
}
func (client *Client) Connect(topic string) {
conn, err := kafka.DialLeader(context.Background(), "tcp", client.host, topic, 0)
if err != nil {
log.Println("failed to dial leader:", err)
}
client.instance = conn
}
type Producer struct {
client *Client
}
func (p Producer) Send(key, value string) {
msg := kafka.Message{Key: []byte(key), Value: []byte(value)}
_, err := p.client.instance.WriteMessages(msg)
if err != nil {
log.Println("reconnect kafka:", err)
p.client.Connect(viper.GetString("common.kafka.topic"))
_, _ = p.client.instance.WriteMessages(msg)
}
}
func NewClient(host string) *Client {
return &Client{host: host}
}
func NewProducer() Producer {
kafkaClient := NewClient(viper.GetString("common.kafka.addr"))
kafkaClient.Connect(viper.GetString("common.kafka.topic"))
return Producer{client: kafkaClient}
}
✅ 设计亮点:
- 接口层只负责将消息写入 Kafka;
- Kafka 异步分发消息;
- 后续由消费服务写入 ClickHouse。
通过这种方式,系统的存储与计算解耦,接口性能与稳定性显著提升。
🧱 三、ClickHouse:为大规模数据而生
为什么选择 ClickHouse?
因为它是一款专为实时分析与海量数据设计的列式数据库。
相比传统行存数据库,它能以更小的存储量、更高的查询速度,
处理 TB~PB 级数据量 的埋点日志与行为数据。
Kafka + ClickHouse 的组合,
就像“高速公路 + 收费站”:
前者负责高速传输,后者负责高效统计与聚合。
🧭 四、结语:开发的难点,不在代码,而在思考
优化的核心,不是换语言、重构架构,而是做减法。
前端:让浏览器干更多的事。
后端:让接口干更少的事。
当每个模块都专注于自己的职责,
系统自然会变得高效、稳定、可扩展。
破局开发困境,从不是推倒重来,
而是让每个环节“回归本职”。
希望这篇文章能给你带来一些启发,
让你在面对复杂系统时,也能找到属于自己的破局之道。
💡 项目开源地址: