从Embedding到Rust:一个远程开发者的性能优化实战手记

后端修仙人
2026-04-17 10:36
阅读 1388

上周五晚上十一点,我正瘫在沙发上刷LeetCode——毕竟跳槽季不刷题等于裸奔。突然手机叮咚一声,是前同事发来的消息:“你之前提的那个JS性能优化方案,线上跑稳了,QPS涨了40%!” 我咧嘴一笑,顺手把刚写完的Rust小工具推到GitHub上,心里琢磨着:这事儿得写篇博客,不然白熬这么多夜。

作为一个常年在家办公的独立开发者,我既享受自由安排时间的快感,也偶尔被孤独感击中。特别是深夜debug的时候,连个能喊“救命”的人都没有。不过好处是,我可以任性地折腾新技术,比如最近沉迷Rust,觉得它那套“零成本抽象”简直是对前端出身的我最大的诱惑。


起因:一个慢得离谱的智能搜索功能

事情要从去年底说起。当时接了个外包项目,客户要做一个知识库问答系统。核心逻辑很简单:用户输入问题,系统用Embedding模型把问题向量化,再和预存的知识片段做相似度匹配,最后返回最相关的几条答案。

听起来挺酷?实际一跑就翻车。

前端用的是React + TypeScript,后端是Node.js(别问为什么不用Go或Java,甲方说“你们不是擅长JS吗?”)。Embedding模型用的是开源的text-embedding-ada-002(通过OpenAI API),向量检索用的是FAISS(通过Python子进程调用)——典型的“缝合怪”架构。

上线第一周,客服电话被打爆

“搜个‘怎么重置密码’要等8秒?”
“页面转圈转到我怀疑人生……”

我抓包一看,好家伙,一次搜索请求平均耗时 6.2秒,其中:

  • JS前端组装请求:0.1s
  • Node.js调OpenAI Embedding API:1.8s
  • 启动Python子进程+加载FAISS索引:2.3s
  • 相似度计算+返回结果:1.5s
  • 网络延迟和其他杂项:0.5s

产品经理还在群里@我:“能不能优化到2秒内?不然下周迭代会挂掉。”

我当时真的想砸电脑——但转念一想,这不就是跳槽前练手的绝佳机会吗?


第一步:砍掉不必要的开销

1. Embedding缓存:别让同样的问题反复算

很多用户其实问的是高度重复的问题,比如“登录不了怎么办”、“发票怎么开”。于是我在Node.js层加了个Redis缓存:

// embedCache.js
const redis = require('redis');
const client = redis.createClient();

async function getOrComputeEmbedding(text) {
  const cacheKey = `emb:${crypto.createHash('md5').update(text).digest('hex')}`;
  const cached = await client.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const embedding = await openai.embeddings.create({
    model: "text-embedding-ada-002",
    input: text,
  });
  
  // 缓存1小时
  await client.setex(cacheKey, 3600, JSON.stringify(embedding.data[0].embedding));
  return embedding.data[0].embedding;
}

这一招直接干掉了30%的重复Embedding请求。但Python子进程启动还是慢如老牛。

2. 告别Python子进程:用WASM跑FAISS?

我一度想把FAISS编译成WebAssembly,直接在Node.js里跑。查了一圈资料发现:FAISS依赖BLAS/LAPACK,WASM支持极差,社区方案基本停留在“理论上可行”。

放弃。


转机:Rust + WASM + JS 的奇妙组合

既然WASM这条路走不通,不如换个思路——用Rust重写整个向量检索服务,然后通过WASM桥接到前端?

等等,前端?对啊!如果能把轻量级的近似最近邻(ANN)算法直接放在浏览器里跑,不就彻底绕过后端瓶颈了吗?

于是我开始研究Rust生态里的向量库。hnswfaiss-rsusearch……最终选定了 usearch,理由很实在:

  • 支持WASM编译
  • 内存占用低
  • API简洁

花了三天时间,我写了个Rust库,导出WASM模块:

// lib.rs
use usearch::Index;

#[wasm_bindgen]
pub struct VectorIndex {
    inner: Index,
}

#[wasm_bindgen]
impl VectorIndex {
    #[wasm_bindgen(constructor)]
    pub fn new(dim: usize) -> Self {
        let mut index = Index::new();
        index.reserve(1000); // 预分配
        index.connect(); // 初始化
        Self { inner: index }
    }

    #[wasm_bindgen]
    pub fn add(&mut self, id: u32, vector: &[f32]) {
        self.inner.add(id, vector).unwrap();
    }

    #[wasm_bindgen]
    pub fn search(&self, query: &[f32], k: usize) -> JsValue {
        let results = self.inner.search(query, k).unwrap();
        serde_wasm_bindgen::to_value(&results).unwrap()
    }
}

然后用wasm-pack打包,生成JS绑定:

wasm-pack build --target web

最后在React组件里加载:

import init, { VectorIndex } from 'my-vector-wasm';

useEffect(() => {
  const loadIndex = async () => {
    await init(); // 加载WASM
    const index = new VectorIndex(1536); // ada-002的维度
    
    // 从CDN加载预构建的向量数据(JSON格式)
    const vectors = await fetch('/vectors.json').then(r => r.json());
    vectors.forEach(([id, vec]) => index.add(id, vec));
    
    setLocalIndex(index);
  };
  loadIndex();
}, []);

效果炸裂

  • 首次加载WASM约800ms(可缓存)
  • 后续搜索平均耗时 45ms
  • 完全去中心化,不依赖后端!

当然,代价也有:知识库更新时得重新生成vectors.json并推CDN。但对于静态知识库来说,完全可接受。


性能对比:数据说话

我把三种方案做了压测(本地MacBook Pro M1,1000次搜索请求):

方案 平均延迟 P95延迟 内存峰值 是否依赖后端
原始Node+Python 6200ms 8100ms 420MB
Node+缓存+Python 4100ms 5800ms 380MB
前端WASM方案 45ms 92ms 85MB

P95降到100ms以内,意味着95%的用户几乎无感等待。产品经理看完数据直接在群里发了个“膜拜”表情包。


踩过的坑与心得

  1. WASM内存管理是魔鬼
    Rust的Vec<f32>传给JS时,必须用serde_wasm_bindgen序列化,否则会内存泄漏。第一次测试时浏览器标签页崩了三次。

  2. Embedding维度别搞错
    OpenAI的ada-002是1536维,但有些开源模型是768维。我一开始混用了,导致相似度全是0——debug到凌晨三点才发现。

  3. 别在浏览器里跑大模型
    虽然WASM很快,但如果知识库超过10万条,vectors.json可能上百MB。我们最终做了分片加载+懒加载,只加载高频问题向量。

  4. Rust的学习曲线是真的陡
    为了搞定wasm-bindgen和生命周期,我看了整整两天文档。但一旦跑通,那种“掌控一切”的爽感,比喝十杯冰美式还提神。


结语:孤独开发者的价值,在于解决问题

远程办公的好处是,我可以自由选择技术栈,不用被“公司技术债”绑架。坏处是,遇到难题只能自己扛。但正是这种“无人可依”的状态,逼我深入底层,从Embedding原理看到WASM内存模型。

现在这个方案已经稳定运行两个月,客户准备把知识库规模扩大十倍。而我呢?简历上又多了一个“高性能前端向量检索系统”的项目,LeetCode刷到了300题,Rust也从“听说很牛”变成了“真香”。

下个月面试新公司时,我打算聊聊这段经历——毕竟,能把JS、Embedding和Rust串起来做性能优化的人,应该不多吧?

(完)

注:本文所有代码均为简化示例,生产环境需增加错误处理、类型校验、安全防护等。另外,别学我在周五晚上debug,容易猝死。

评论 0

最热最新
暂无评论
后端修仙人Lv.1
0
影响力
0
文章
0
粉丝