「我想做一个隐私优先的检索增强生成应用,文档永远不离开浏览器。」开发者这样开场。听起来理想主义,但「搜索」这一步在5000个向量时就崩了。
这不是算法问题。余弦相似度不过是点积除以两个范数,但10000个向量×1536维=单次查询1500万次浮点乘法。JavaScript的垃圾回收器才不管你是不是在热循环里——它想停就停。
实测:纯JS的硬天花板
他测遍了所有客户端库。10000向量×1536维,client-vector-search耗时37毫秒,MeMemo的HNSW降到17毫秒,但建索引要几分钟;VecLite做到8毫秒,仍是它的两倍慢。突破10万后,纯JS开始吃瘪——单次搜索1.5秒以上。
生态位的空缺很明显:没有东西能填上「玩具库(上限1千向量)」和「生产级向量库(需要服务器)」之间的鸿沟。他需要在浏览器里跑10万向量、亚秒级延迟。
第一个抉择:留在JavaScript,还是下沉到更底层?他选了Rust编译成WebAssembly,后续所有决定都从这里 cascade(级联)开来。
Rust/WASM的三张底牌
这不是推测。PGlite、DuckDB-WASM、Figma都在用同一套架构,工具链成熟。相比JavaScript,Rust/WASM提供三样东西:
一是接近原生的速度,WASM的浮点运算没有JS的解释开销;二是内存布局可控,向量用连续的Float32Array存储,没有JS对象的指针跳跃;三是没有垃圾回收,内存生命周期由开发者管理,不会在搜索中途卡顿。
但架构有三层严格分界:TypeScript、WASM边界、Rust。边界的规则比算法更重要。
边界开销:一次调用 vs 一万次调用
每次WASM穿越都有成本。调用一次WASM传10000个向量,和调用10000次每次传1个向量,差距是40毫秒对4秒——两个数量级。
实际的上传路径被压缩成单次穿越:TypeScript负责校验、扁平化、序列化,然后一次跨过边界。
所有向量以f32存储和计算,绝不用f64。这是刻意选择:f64的精度对语义搜索是浪费,但内存和带宽成本翻倍。在浏览器里,每个字节都值钱。
存储层:故意做得「蠢」
VecLite的存储适配器只有四个方法:get、set、delete、clear。键值接口,越简单越好。
这个设计把持久化策略完全踢给调用方。想用IndexedDB?自己包一层。需要加密?在外面做。存储层不碰这些,只保证向量索引本身足够快、足够小。
这种「愚蠢」是刻意的。浏览器环境太碎片化,试图内置智能存储只会变成配置地狱。把复杂度推出去,核心库保持原子性。
为什么这件事值得看
这不是又一个Rust重写的故事。它戳中了一个被忽视的中间地带:客户端AI的「最后一公里」——嵌入模型可以在浏览器跑(Transformers.js),但向量搜索成了瓶颈。
他的选择揭示了一条务实路径:不用等浏览器原生支持,也不倒退回服务器,而是用成熟工具链把计算密度压进客户端。隐私优先的RAG、离线可用的语义搜索、零API成本的个人知识库——这些场景突然变得可行。
当然,10万向量在浏览器里仍是小众需求。但生态位空着的时候,先占住的人定义标准。VecLite的存储适配器设计尤其聪明:它不假装能解决所有问题,而是让能解决的人无缝接入。
下次有人再说「浏览器做不了重计算」,你可以把这篇甩过去——顺便提醒他,垃圾回收器可不会看场合暂停。
热门跟贴