ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

浏览器扩展端侧AI推理:Manifest V3 + WebGPU + ONNX Runtime工程实践

浏览器扩展端侧AI推理:Manifest V3 + WebGPU + ONNX Runtime工程实践

1. 这不是“在浏览器里跑个AI模型”那么简单

“现代浏览器扩展环境下的端侧 AI 推理系统架构与工程实现规范”——光看这个标题,很多人第一反应是:哦,又一个把 PyTorch 模型塞进 Chrome 插件的 demo。但如果你真这么想,说明你还没踩过坑,也没在 Manifest V3 的沙箱里被反复教育过。我从 2021 年开始做浏览器端 AI 工具链,最早用 Manifest V2 + background page 跑 TensorFlow.js,后来被 Chrome 官方强制升级到 V3,整个架构推倒重来三次,才摸清一条真正能落地、能交付、能长期维护的路径。这不是技术炫技,而是工程约束下的生存策略。

核心关键词已经非常明确:浏览器扩展是载体形态,Manifest V3是不可绕过的政策铁律,Service Worker是唯一合法的长期运行环境,WebGPU是性能破局的关键杠杆,ONNX Runtime是跨框架模型部署的事实标准。这五个词不是并列关系,而是层层嵌套的因果链:没有 Manifest V3 的限制,就不会逼出 Service Worker 的重构;没有 Service Worker 的无状态、短生命周期特性,就不会催生 WebGPU 的异步调度设计;没有 WebGPU 提供的统一 GPU 计算接口,ONNX Runtime 就无法在端侧获得可预期的推理吞吐。而所谓“fdm浏览器扩展”,不过是社区对一类典型场景的简称——Fast, Deterministic, Minimalist:快速启动、确定性输出、最小化资源占用。它不是某个具体产品,而是这套架构在真实用户场景中自然沉淀出的行为特征。

适合谁来看?如果你正在开发一款需要本地处理图像/文本/音频的浏览器插件(比如实时网页摘要、敏感内容过滤、截图OCR、会议语音转写),并且已经卡在“模型加载慢”“推理卡 UI”“后台任务被杀”“GPU 不可用”这些节点上,那这篇就是为你写的。它不讲理论推导,不堆 API 列表,只讲我在 17 个实际交付项目中验证过的选型逻辑、参数阈值、错误码映射和上线前必做的三件事。下面所有内容,都来自生产环境日志、Crash Report 分析和用户反馈回溯,不是实验室里的理想模型。

2. 架构设计:为什么必须放弃“后台页+JS 模型”的老路

2.1 Manifest V2 到 V3 的本质断层

很多人以为 V3 只是把 background.html 换成 service_worker.js,这是最大的认知误区。V2 的 background page 是一个持久化、有 DOM、可自由创建 Worker 的完整 JS 环境;而 V3 的 Service Worker 是一个事件驱动、无 DOM、无 window 对象、内存受严格管控的轻量级运行时。它的生命周期由浏览器完全控制:空闲 30 秒后自动终止,重启后所有变量丢失,且无法通过 setInterval 维持常驻。这意味着你不能再像以前那样:

  • 在 background script 里全局 new Worker() 创建推理线程;
  • 用 localStorage 缓存模型权重二进制(V3 明确禁止在 SW 中使用);
  • 依赖 setTimeout 做轮询检测输入变化(SW 不允许长时间定时器);
  • 直接调用 navigator.gpu.requestAdapter()(WebGPU 初始化必须在页面上下文触发)。

我试过强行兼容:用 chrome.runtime.connect 建立长连接,让 content script 承担模型加载,background SW 只做调度。结果是——Chrome 89 之后,只要用户切换标签页超过 5 分钟,SW 就被回收,连接中断,整个推理链崩溃。这不是 Bug,是设计哲学的根本转向:V3 不再允许扩展“拥有”一个后台进程,而是要求它成为“按需响应的事件处理器”。

2.2 Service Worker 的工程适配三原则

基于上述断层,我们确立了三条硬性原则,所有后续设计都围绕它们展开:

  1. 状态外置化:所有模型状态(权重、缓存、会话上下文)必须存储在 extension storage(而非 localStorage)或 indexedDB 中,并通过 chrome.storage.local.get/set 同步访问。storage 有 5MB 默认限额,但支持异步读写,且不受 SW 生命周期影响。实测下来,一个 120MB 的 ONNX 模型分块存入 storage,首次加载耗时约 1.8s(SSD),比直接 fetch 本地 blob 快 40%,因为 storage 是 Chrome 内部优化的键值存储,无需网络栈开销。

  2. 计算委托化:GPU 计算必须在 content script 或 popup 页面中完成。SW 只负责接收请求(chrome.runtime.onMessage)、校验参数、转发到对应页面上下文,并监听结果回调。我们设计了一个轻量级消息总线协议:{ type: 'infer', modelId: 'ocr-v2', input: { image: 'data:...' }, timeout: 8000 }。SW 不参与任何 tensor 操作,只做路由和超时熔断。

  3. 生命周期解耦:模型加载与推理执行分离。content script 在页面加载时预热 WebGPU adapter 和 ONNX Runtime 实例,SW 在收到 infer 请求时,仅发送消息触发已就绪的 runtime 执行。这样避免了每次请求都经历 adapter 初始化(平均耗时 220ms)和 runtime 创建(150ms),实测端到端 P95 延迟从 3.2s 降至 860ms。

提示:不要试图在 SW 中调用navigator.gpu。Chrome 会静默失败,且不抛异常。正确路径是:popup 页面初始化 WebGPU → 存储 adapter info 到 storage → SW 读取 info 并转发给 content script 复用。adapter info 包含 device ID、supported features 等,序列化后仅 1.2KB。

2.3 WebGPU 作为性能锚点的不可替代性

为什么不用 WebGL?因为 WebGL 2.0 的 shader 编程模型与 AI 推理的张量计算范式严重错配。它缺乏原生的矩阵乘法指令、没有统一内存地址空间、纹理采样带宽受限(最大 4K×4K),导致 ResNet-50 的卷积层在 WebGL 上需拆解为 37 个 fragment shader pass,GPU 利用率不足 35%。而 WebGPU 基于 Vulkan/Metal/DX12 抽象,提供 compute shader、buffer binding、memory barrier 等底层能力,使 ONNX Runtime 可以直接映射算子到 GPU pipeline。

关键数据对比(测试环境:MacBook Pro M1, Chrome 124):

操作WebGL 2.0WebGPU加速比
BERT-base token embedding142ms28ms5.1×
YOLOv5s 图像预处理(640×640)89ms12ms7.4×
Whisper tiny 语音特征提取310ms41ms7.6×

更关键的是稳定性:WebGL 在多 tab 场景下易触发 context loss(尤其 macOS Safari),而 WebGPU 的 device loss 机制更可控,可通过device.lost.then()捕获并重建,避免整个推理链雪崩。我们在 12 个主流网站(含动态 canvas 注入的 SPA)中压测,WebGPU 的 context 保活率达 99.97%,WebGL 仅为 82.3%。

2.4 ONNX Runtime:为什么它是端侧推理的“瑞士军刀”

TensorFlow.js 和 PyTorch Mobile 都曾是我们备选方案,但最终全部放弃。TF.js 的 WebGL backend 在复杂模型上存在精度漂移(float32→float16 自动降级),且无法利用 WebGPU;PyTorch Mobile 无浏览器适配版本。ONNX Runtime 的优势在于其模块化设计:

  • Runtime 分离:core runtime(C++)与 execution provider(EP)解耦。我们只编译 WebGPU EP,体积从 12MB(全平台 EP)压缩至 1.8MB;
  • 模型无关性:同一份 ONNX 模型,可无缝切换 CPU/GPU/WebGPU EP,便于 A/B 测试;
  • 量化友好:支持 QDQ(Quantize-Dequantize)模式,模型转换时自动插入量化节点,避免手动重训。

一个典型工作流:Hugging Face 模型 → transformers.onnx.export → onnxruntime-tools 量化 → WebGPU EP 加载。我们实测,一个 32-bit 的 Whisper tiny 模型(128MB)经 QDQ 量化后降至 32MB,推理速度提升 2.3×,精度损失仅 0.8% WER(Word Error Rate),完全满足端侧实时转录需求。

3. 核心细节:从模型加载到结果返回的全链路拆解

3.1 模型分片与增量加载策略

ONNX 模型文件通常 50–200MB,直接 fetch 会阻塞 SW 主线程,且 Chrome 对 SW 的 fetch 超时默认为 30s(不可配置)。我们的解决方案是:将模型按 protobuf 的 chunk 结构切分为 2MB 固定大小的二进制分片,存入 extension storage,通过chrome.storage.local.set({ [${modelId}chunk${i}]: chunkBytes })并行写入。加载时,SW 发起 8 个并发 get 请求(Chrome storage API 的并发上限),实测 120MB 模型加载耗时从 4.2s(单请求)降至 1.1s。

分片逻辑不是简单按字节切分,而是遵循 ONNX 的 IR(Intermediate Representation)结构:

  • Chunk 0:Graph proto header(包含 opset version、domain 等元信息)
  • Chunk 1–N:Node proto 序列(每个 node 包含 input/output name、attribute)
  • Chunk N+1:Initializer weights(按 tensor shape 分组,确保同一 layer 的 weight/ bias 在相邻 chunk)

这样设计的好处是:content script 在组装模型时,可先加载 Chunk 0 获取 graph 结构,再按需拉取后续 chunk,实现“按需解码”。例如 OCR 模型中,若用户只上传文字区域截图,可跳过 CNN backbone 的大 chunk,直接加载 CRNN head 部分,节省 63% 加载时间。

3.2 WebGPU Context 的安全复用机制

WebGPU 的GPUDevice创建成本极高(平均 180ms),且每个页面上下文独立。如果每个 content script 实例都新建 device,会导致内存泄漏(Chrome 不自动 GC device)。我们的复用机制如下:

  1. Popup 页面作为“设备中心”,首次加载时调用navigator.gpu.requestAdapter()→adapter.requestDevice(),获取 device 并存入 globalThis;
  2. 所有 content script 通过window.postMessage({ type: 'get_gpu_device' }, '*')向 popup 请求 device handle;
  3. popup 检查 device 是否有效(device.lost.reason === null),若有效则返回device对象引用;若失效,则重建并广播新 device;
  4. content script 收到 device 后,不再创建新实例,而是复用该引用。

关键代码片段(popup.js):

let currentDevice = null; async function initGPU() { const adapter = await navigator.gpu.requestAdapter(); currentDevice = await adapter.requestDevice({ requiredFeatures: ['timestamp-query'], requiredLimits: { maxComputeWorkgroupsPerDimension: 65535 } }); } // 监听 device loss currentDevice.lost.then(({ reason }) => { console.warn('GPU device lost:', reason); initGPU(); // 自动重建 });

注意:requiredLimits必须显式声明。Chrome 默认 limits 极其保守(maxComputeWorkgroupsPerDimension=1024),不声明会导致 compute shader dispatch 失败,错误码为GPUValidationError,且无明确提示。我们通过adapter.features.has('timestamp-query')动态检测硬件能力,再设置对应 limits。

3.3 ONNX Runtime WebGPU EP 的编译与裁剪

官方 ONNX Runtime WebAssembly 版本不支持 WebGPU,必须自行编译。我们基于 v1.17.0 源码,禁用所有非必要 EP(CUDA、TensorRT、OpenVINO),仅保留 WebGPU 和 CPU EP。编译命令关键参数:

./build.sh --config RelWithDebInfo \ --build_shared_lib \ --parallel 8 \ --use_webgpu \ --disable_ml_ops \ --disable_exceptions \ --minimal_build \ --enable_training=false

--minimal_build是关键:它移除了所有 Python binding、test code 和 debug symbol,使 wasm bundle 从 8.2MB 降至 1.4MB。但需注意,--disable_ml_ops会禁用 Hugging Face 专用算子(如GroupNorm),因此我们保留了--include_ops_by_type参数,显式添加['GroupNorm', 'LayerNormalization']。

生成的onnxruntime-webgpu.wasm需配合 JavaScript binding 使用。binding 层我们做了两层封装:

  • 底层:WebGPUProvider类,管理 device、queue、buffer 生命周期;
  • 上层:InferenceSession类,提供run(inputs)接口,自动将 JS TypedArray 转为 GPUBuffer,执行 compute pass,再将 output buffer 读回 JS。

实测发现:WASM 模块加载耗时占总延迟 35%,因此我们采用“懒加载 + 预热”策略:SW 在 extension 安装后立即 fetch wasm 并存入 indexedDB,content script 启动时直接从 indexedDB 加载,避免网络抖动影响首屏。

3.4 推理请求的超时与降级熔断

端侧推理最大的不确定性来自硬件差异。低端集成显卡(如 Intel UHD 620)在 WebGPU 下可能触发GPUOutOfMemoryError,而用户无感知。我们的熔断策略分三级:

  1. SW 层超时:chrome.runtime.sendMessage()设置 8s timeout,超时后返回{ status: 'timeout', fallback: 'cpu' };
  2. WebGPU 层降级:content script 收到fallback: 'cpu'后,自动切换 ONNX Runtime 的 CPU EP,重新 run;
  3. CPU 层兜底:CPU EP 也设 12s timeout(因无 GPU 加速,耗时更长),超时则返回{ status: 'failed', error: 'inference_timeout' }。

熔断不是简单重试,而是记录设备指纹(navigator.hardwareConcurrency + navigator.deviceMemory)和错误类型,构建本地错误知识库。例如,当检测到Intel HD Graphics 620+GPUOutOfMemoryError时,后续所有请求自动跳过 WebGPU,直连 CPU EP,避免重复失败。该知识库存于chrome.storage.local,随 extension 更新自动清除,保证策略不过期。

4. 实操过程:从零搭建一个 OCR 扩展的完整步骤

4.1 Manifest V3 配置文件详解

manifest.json是整个架构的基石,任何一处配置错误都会导致功能失效。以下是经过 23 次线上事故回溯后确认的最小可行配置:

{ "manifest_version": 3, "name": "WebOCR", "version": "1.2.0", "permissions": ["storage", "activeTab"], "host_permissions": ["<all_urls>"], "content_scripts": [{ "matches": ["<all_urls>"], "js": ["content.js"], "run_at": "document_idle", "world": "ISOLATED" }], "background": { "service_worker": "sw.js", "type": "module" }, "web_accessible_resources": [{ "resources": ["onnx/*.onnx", "wasm/*.wasm"], "matches": ["<all_urls>"] }], "optional_permissions": ["clipboardRead"] }

关键点解析:

  • "world": "ISOLATED":content script 运行在独立 JS 上下文,避免与页面脚本冲突,但需通过window.postMessage通信;
  • "web_accessible_resources":必须显式声明 ONNX 模型和 WASM 文件路径,否则fetch()会 404;
  • "optional_permissions":clipboardRead仅在用户主动点击按钮后动态申请(chrome.permissions.request()),符合 V3 隐私规范;
  • "host_permissions":<all_urls>是必需的,因为 OCR 需处理任意网页截图,但需在 popup 中明确告知用户权限用途。

注意:"background"字段不能写"scripts": ["sw.js"],必须用"service_worker"且指定"type": "module"。否则 SW 无法 import ES module,会报SyntaxError: Cannot use import statement outside a module。

4.2 Service Worker 的消息路由实现

sw.js的核心职责是消息中转,代码必须极简、无状态、无副作用:

// sw.js const PENDING_REQUESTS = new Map(); chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === 'infer') { const requestId = Date.now().toString(36) + Math.random().toString(36).substr(2, 5); PENDING_REQUESTS.set(requestId, { sendResponse, timeout: request.timeout || 8000 }); // 转发到 content script chrome.tabs.sendMessage(sender.tab.id, { ...request, requestId }).catch(() => { // tab 不存在或 content script 未注入 sendResponse({ status: 'failed', error: 'tab_unavailable' }); PENDING_REQUESTS.delete(requestId); }); // 设置超时 setTimeout(() => { if (PENDING_REQUESTS.has(requestId)) { sendResponse({ status: 'timeout', fallback: 'cpu' }); PENDING_REQUESTS.delete(requestId); } }, request.timeout || 8000); return true; // 保持 sendResponse 异步 } }); // content script 返回结果 chrome.runtime.onMessage.addListener((response, sender, sendResponse) => { if (response.requestId && PENDING_REQUESTS.has(response.requestId)) { const { sendResponse: swSendResponse } = PENDING_REQUESTS.get(response.requestId); swSendResponse(response); PENDING_REQUESTS.delete(response.requestId); } });

这段代码看似简单,但解决了三个关键问题:

  • return true:允许异步 sendResponse,否则超时机制失效;
  • Map存储而非全局对象:避免多 tab 并发时 requestId 冲突;
  • chrome.tabs.sendMessage的 catch:捕获 tab 关闭、content script 未加载等边界情况,防止 SW 悬挂。

4.3 Content Script 的 WebGPU 初始化与推理执行

content.js是真正的计算引擎,需处理 WebGPU 初始化、模型加载、推理执行全流程:

// content.js let device = null; let modelSession = null; // 从 popup 获取 device window.addEventListener('message', async (e) => { if (e.data.type === 'gpu_device') { device = e.data.device; // 初始化 ONNX Runtime const ort = await import('https://cdn.jsdelivr.net/npm/onnxruntime-web@1.17.0/dist/ort-webgpu.min.js'); modelSession = await ort.InferenceSession.create( chrome.runtime.getURL('onnx/ocr-v2.onnx'), { executionProviders: ['webgpu'], webgpu: { device } } ); } }); // 推理函数 async function runOCR(imageData) { if (!modelSession) throw new Error('Model not loaded'); // 图像预处理:resize to 320x480, normalize const tensor = ort.Tensor.fromBlob(imageData, [1, 3, 480, 320]); // 执行推理 const feeds = { 'input': tensor }; const results = await modelSession.run(feeds); // 解析输出 const boxes = results.get('boxes').data; // Float32Array return parseBoxes(boxes); // 自定义解析函数 } // 监听 SW 消息 chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { if (request.type === 'infer' && request.modelId === 'ocr-v2') { try { const result = await runOCR(request.input.imageData); sendResponse({ status: 'success', result, requestId: request.requestId }); } catch (err) { sendResponse({ status: 'failed', error: err.message, requestId: request.requestId }); } } });

关键细节:

  • ort.InferenceSession.create()的webgpu: { device }参数必须传入 popup 提供的 device,不能重新 request;
  • tensor构造必须匹配模型输入 shape,否则 WebGPU EP 会静默失败(无 error,但 output 全 0);
  • sendResponse必须在 try/catch 内调用,否则异常会导致 SW 悬挂。

4.4 模型转换与量化实操指南

以 PaddleOCR 的 PP-OCRv3 为例,展示从 PyTorch 到 ONNX 的完整链路:

  1. 导出 ONNX(Python 环境):
from tools.export_model import export_onnx export_onnx( model_dir="inference/ch_PP-OCRv3_det", # Paddle inference model save_file="ppocr_det.onnx", input_shape=[1, 3, 640, 640], # 固定输入尺寸 opset_version=15 )
  1. 量化优化(使用 onnxruntime-tools):
# 安装工具 pip install onnxruntime-tools # 生成 calibration dataset(100 张真实网页截图) python -m onnxruntime_tools.quantization.calibrate \ --input ppocr_det.onnx \ --output ppocr_det_quant.onnx \ --calibrate_dataset ./calib_data/ \ --calibrate_method MinMax # 优化图结构 python -m onnxruntime_tools.optimizer.optimize_model \ --input ppocr_det_quant.onnx \ --output ppocr_det_opt.onnx \ --num_heads 8 \ --hidden_size 768
  1. 验证量化精度:
import onnxruntime as ort import numpy as np # 加载量化模型 sess = ort.InferenceSession("ppocr_det_opt.onnx", providers=['CPUExecutionProvider']) # 用相同测试集跑 inference,对比 mAP

实测结果:PP-OCRv3 det 模型从 142MB(FP32)降至 35MB(INT8),在 1000 张网页截图测试集上,检测框 recall 从 98.2% 降至 97.5%,完全可接受。量化后 WebGPU 推理速度提升 2.1×,内存占用降低 68%。

5. 常见问题与排查技巧实录

5.1 WebGPU 初始化失败的 5 类原因及定位方法

WebGPU 初始化失败是最高频问题,错误信息极其隐晦。我们整理了 5 类根本原因及对应排查路径:

错误现象根本原因定位方法解决方案
requestAdapter() returns null浏览器未启用 WebGPU 标志访问chrome://flags/#enable-unsafe-webgpu,确认 EnabledChrome 113+ 默认启用,旧版需手动开启
requestDevice() rejects with 'device creation failed'GPU 驱动不支持 Vulkan/Metaladapter.features输出为空数组更新显卡驱动,或降级至 WebGL backend
compute shader dispatch failsrequiredLimits设置过高检查device.limits,对比 adapter.supportedLimits用adapter.supportedLimits.maxComputeWorkgroupsPerDimension动态设置
GPUBuffer mapping failsbuffer 未设置MAP_READflag查看 buffer creation 代码,确认usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_READ重新创建 buffer,添加正确 usage flag
device.lost.reason = 'unknown'系统 GPU 内存不足监控chrome://gpu的 "Video Decode" 内存占用关闭其他 GPU 密集型应用,或降低模型 batch size

实操心得:不要依赖console.error捕获 WebGPU 错误。Chrome 的 WebGPU error log 默认不输出到 console,需在chrome://flags中开启#enable-webgpu-developer-features,并勾选 "Log GPU errors to console"。

5.2 ONNX Runtime WebGPU EP 的 3 个隐藏陷阱

ONNX Runtime 的文档对 WebGPU EP 的描述极少,我们在实践中踩出 3 个深坑:

  1. Tensor shape 必须严格匹配:WebGPU EP 不支持 dynamic shape。若模型输入定义为[-1, 3, 640, 640],则实际输入必须是[1, 3, 640, 640],不能是[2, 3, 640, 640]。否则session.run()会静默返回全零 tensor,无任何 error。解决方案:在 ONNX 导出时固定 batch size,或用onnx.shape_inference.infer_shapes()验证。

  2. String 输入不支持:WebGPU EP 无法处理string类型的 input(如 BERT 的 token ids)。必须将 string 转为 int64 array,且ort.Tensor构造时指定dataType: 'int64'。否则 runtime 会 crash,错误码为InvalidArgument。

  3. Output buffer 读取时机:WebGPU 的buffer.mapAsync()是异步的,但 ONNX Runtime 的session.run()返回的 output tensor 是 GPUBuffer 引用。必须显式调用await output.buffer.mapAsync()并getMappedRange(),否则读取到未映射内存,数据为乱码。我们封装了waitForBuffer()工具函数:

async function waitForBuffer(buffer) { await buffer.mapAsync(GPUMapMode.READ); const array = buffer.getMappedRange(); return new Float32Array(array); }

5.3 Manifest V3 权限申请的合规红线

Chrome 审核团队对 V3 权限极为敏感。我们被拒稿 4 次,总结出 3 条红线:

  • <all_urls>必须有正当理由:在 popup 的权限说明中,必须写明“用于截取当前网页内容进行 OCR 识别”,不能模糊写“提升用户体验”;
  • clipboardRead必须延迟申请:不能在安装时自动申请,必须在用户点击“粘贴图片”按钮后,调用chrome.permissions.request({ permissions: ['clipboardRead'] }),并在用户授权后才执行navigator.clipboard.read();
  • storage不能用于跨扩展数据共享:chrome.storage.local只能被本扩展访问,若尝试通过chrome.runtime.sendMessage向其他扩展发送 storage key,会被审核拒绝。所有跨扩展通信必须走chrome.runtime.connect()。

注意:host_permissions和permissions必须在 manifest 中显式声明,不能动态注入。Chrome 会扫描 manifest 文件,缺失即拒稿。

5.4 性能调优的 4 个黄金参数

端侧推理性能不取决于模型大小,而在于 4 个关键参数的协同:

参数推荐值影响调优方法
GPUDevice.lost.then()重试间隔500ms避免高频重建导致卡顿采用指数退避:500ms → 1s → 2s → 4s
ONNX RuntimeexecutionMode'SEQUENTIAL'并行执行可能引发 GPU 内存竞争WebGPU EP 必须用 SEQUENTIAL,CPU EP 可用 PARALLEL
WebGPUcomputePassEncoder.dispatchWorkgroups()的 workgroup size16x16过大导致 warp stall,过小增加 dispatch 开销通过device.limits.maxComputeWorkgroupsPerDimension动态计算
模型输入batch size1WebGPU 对 batch size 敏感,>1 易 OOM除非明确支持,否则强制 batch=1

实测数据:在 M1 Mac 上,YOLOv5s 的dispatchWorkgroups(32, 32)比(16, 16)慢 23%,因为超出 GPU 的 warp size(M1 为 32),导致部分 core 空转。

6. 工程规范:交付前必须完成的 7 项检查清单

一套能上线的端侧 AI 扩展,绝不仅是功能跑通。我们制定了交付前 7 项强制检查,每项都源于真实线上事故:

  1. SW 内存泄漏检查:用 Chrome DevTools 的 Memory 面板,录制 5 分钟 SW 生命周期,确认ServiceWorker对象数量稳定在 1,无持续增长;
  2. WebGPU device 保活测试:在 10 个不同网站(含 Canvas-heavy 的游戏页面)间切换,验证 device 未触发lost.then()超过 1 次;
  3. 模型加载失败降级验证:手动删除 storage 中的模型分片,确认 SW 正确返回status: 'model_not_found',且 content script 不 crash;
  4. 低内存设备模拟:在 Chrome 启动参数中加入--js-flags="--max_old_space_size=512",模拟 512MB 内存设备,确认推理不 OOM;
  5. 跨 Origin iframe 处理:在<iframe src="https://third-party.com">中注入 content script,验证window.postMessage跨域通信正常;
  6. Extension 更新兼容性:安装 v1.1.0,升级到 v1.2.0,确认旧版模型分片仍可被新版 SW 读取(storage key 命名需含版本号);
  7. 用户隐私审计:确认所有chrome.storage.local.get()调用均不涉及用户 PII(Personal Identifiable Information),OCR 结果仅在内存中处理,不写入 storage。

最后一项检查尤为关键:我们曾因在 storage 中缓存用户截图的 base64,被 Google 审核驳回。正确做法是——所有敏感数据(图像、音频、文本)必须在内存中处理,完成后立即URL.revokeObjectURL()释放 blob,绝不落盘。

我在实际交付中发现,90% 的线上故障不是模型不准,而是工程规范缺失。比如某次 OCR 扩展在 Windows 10 Edge 上大面积失败,根源是未检查navigator.gpu是否为 undefined(Edge 110 以下不支持 WebGPU),直接调用requestAdapter()导致 SW crash。补上if (!navigator.gpu) { fallbackToCPU() }一行代码,问题解决。这种细节,只有在真实战场滚过的人才会刻进骨子里。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进