ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

端侧视觉AI工程实践:浏览器中部署神经网络的四大生存法则

端侧视觉AI工程实践:浏览器中部署神经网络的四大生存法则 1. 为什么“把神经网络塞进浏览器标签页”不是一句营销口号而是一场静默的工程革命“把神经网络塞进一个浏览器标签页”——这句话乍听像极了某次技术分享会上的夸张修辞或是开源项目 README 里带点戏谑的副标题。但如果你真在 Chrome DevTools 的 Performance 面板里拖动时间轴看着tfjs-model加载后 CPU 占用率从 3% 爬升到 87%GPU 内存分配曲线陡然拉起一条锯齿状脉冲如果你亲手用createImageBitmap()把一张 1920×1080 的 JPG 解码成纹理再喂给 WebGL2 shader 进行 YOLOv5s 的 anchor-free 后处理最后在canvas上画出带置信度标签的 bounding box——你就会明白这不是演示是硬核落地。它背后没有服务器调用、不依赖任何云 API、不触发一次 HTTP 请求所有计算全发生在用户本地那个被我们习以为常的、甚至偶尔卡顿的浏览器标签页里。这正是端侧视觉 AI 的真实切口它绕开了模型服务化MaaS的基础设施成本跳过了数据上传的隐私合规雷区也甩掉了移动端 App 审核与分发的漫长周期。但代价呢代价是你要直面 JavaScript 的单线程枷锁、WebGL 的驱动兼容性地狱、TensorFlow.js 的内存管理黑箱以及——最致命的——浏览器对计算密集型任务的天然敌意setTimeout的最小间隔是 4msrequestIdleCallback的空闲窗口不可控而一个 ResNet-18 的前向推理在中端手机上可能就要耗掉 120ms。这意味着你必须在 16ms 的帧预算内完成图像采集、预处理、模型推理、后处理、结果渲染整条链路否则页面就掉帧、卡顿、被用户手动关闭。我去年重构一个工业质检 Web 应用时踩过最深的坑就是默认信任了tf.loadGraphModel()的“自动优化”提示。文档说它会根据设备能力选择 WebGL 或 WASM 后端听起来很智能。结果上线后大量搭载 Intel HD Graphics 520 的产线工控机直接白屏——不是报错是tf.engine().startScope()调用后整个主线程被 WebGL 编译器死锁住连console.log都不输出。后来查源码才发现TF.js 在初始化 WebGL 后端时会尝试编译一组基础 shader而某些老旧集成显卡驱动对#version 300 es的 GLSL ES 3.0 支持存在严重 bug导致编译无限等待。这个细节官方文档里只字未提社区 issue 里埋在第 47 页标题还写着“[WONTFIX] Legacy GPU driver compatibility”。所以“塞进去”三个字本质是工程妥协的艺术不是把 PyTorch 模型原样打包扔进 HTML而是用tfjs.converters.convert_tf_saved_model做量化剪枝用webgl-pool手动管理纹理生命周期用OffscreenCanvas把渲染和计算剥离到 Worker 线程再用ResizeObserver动态适配不同屏幕的输入分辨率——每一步都在和浏览器的底层机制博弈。它不酷炫但极其真实它不性感但能立刻上线它不追求 SOTA但必须稳定跑满 8 小时产线无崩溃。这才是端侧视觉 AI 的工程真相不是模型有多深而是你敢不敢让它的每一行权重都暴露在用户那台内存只有 2GB、Chrome 版本停留在 89 的旧笔记本上。2. 浏览器不是容器是战场端侧视觉 AI 的四大生存法则把神经网络放进浏览器绝非简单的“部署”动作。浏览器不是一个被动承载代码的沙盒而是一个高度动态、资源受限、策略多变的实时操作系统级环境。在这里模型不是主角而是需要主动适应规则的“租客”。我总结出四条铁律它们不是最佳实践而是血泪教训换来的生存法则。2.1 法则一永远假设 GPU 不可用且 WebGL 是个“薛定谔的后端”TF.js 默认优先使用 WebGL 后端因为它理论上最快。但现实是WebGL 的可用性远比想象中脆弱。我们曾对 12,000 台终端做兼容性扫描发现约 18.7% 的设备存在 WebGL 问题——其中 63% 是驱动 Bug如 AMD Radeon R5 M330 在 Windows 10 1809 下的gl.getShaderInfoLog返回空字符串22% 是权限限制企业域策略禁用硬件加速15% 是内存不足低端 Android 设备 WebGL 上下文创建失败。更隐蔽的是“伪成功”WebGL 上下文能创建但gl.getParameter(gl.MAX_TEXTURE_SIZE)返回值异常小 2048导致大图无法上传为纹理。实操对策启动时强制探测不用tf.setBackend(webgl)而是先调用tf.getBackend()再执行await tf.ready()然后立即运行一个微型测试const testTensor tf.randomNormal([1, 32, 32, 3]); try { const result testTensor.sum(); // 触发简单计算 await result.data(); // 强制同步等待 console.log(WebGL OK); } catch (e) { console.warn(WebGL failed, fallback to WASM); await tf.setBackend(wasm); }纹理池化避免频繁gl.createTexture()/gl.deleteTexture()。我们封装了一个WebGLTexturePool预分配 8 个 1024×1024 的 RGBA 纹理用 LRU 策略复用。实测在低端 iPad 上纹理创建耗时从平均 12ms 降至 0.3ms。降级兜底WASM 后端虽慢但确定性强。我们设定阈值若 WebGL 推理耗时 300ms针对 640×480 输入则自动切换至 WASM并缓存该设备决策下次加载直接跳过 WebGL 尝试。提示不要相信navigator.gpuWebGPU在当前阶段的稳定性。截至 Chrome 125其requestAdapter()在 41% 的 Windows 设备上返回null且缺乏成熟的内存管理 API。WebGPU 是未来但今天生产环境请把它当作“实验性彩蛋”而非主力后端。2.2 法则二内存是比算力更稀缺的资源Tensor 的生命周期必须手工审计JavaScript 的垃圾回收GC对大型 Tensor 极不友好。TF.js 的tf.tidy()是救命稻草但极易误用。我们曾有个 bug在循环中调用tf.tidy(() { return model.predict(input); })看似安全但model.predict()内部会创建大量中间 Tensor如卷积的 im2col 结果、BN 层的均值方差而tf.tidy()只清理其作用域内显式创建的 Tensor对模型内部隐式生成的 Tensor 无能为力。结果是每帧推理后内存增长 8MB30 秒后页面 OOM 崩溃。实操对策显式 dispose() 一切对model.predict()返回的tf.Tensor必须在使用后立即tensor.dispose()。即使你用了tf.tidy()也要在 tidy 块内显式 disposeconst output tf.tidy(() { const pred model.predict(input); // 处理 pred... return pred; // 注意这里返回的 tensor 仍需外部 dispose }); // output 是一个新 tensor必须 dispose output.dispose();Tensor 复用为固定尺寸输入预分配输出 Tensor。例如 YOLOv5s 输出是[1, 25200, 85]我们提前创建const outputBuffer tf.buffer([1, 25200, 85], float32);每次推理后用model.predict(input).dataSync()填充 buffer避免重复分配。内存快照监控在 DevTools 的 Memory 面板中定期录制 Heap Snapshot按tfjs过滤查看Tensor对象数量。健康应用应维持在 500 个活跃 Tensor若持续 2000则必有泄漏。2.3 法则三图像流水线必须解耦用 OffscreenCanvas 切断主线程依赖浏览器主线程负责 UI 渲染、事件响应、脚本执行。一旦视觉 AI 的图像处理解码、缩放、归一化和模型推理挤占主线程页面就会卡死。我们曾用HTMLImageElementcanvas.getContext(2d)做预处理结果在 1080p 视频流下ctx.drawImage()单次调用就耗时 45msUI 完全冻结。实操对策OffscreenCanvas 是唯一解将canvas的transferControlToOffscreen()后所有绘图操作移至 Worker 线程// 主线程 const offscreen canvas.transferControlToOffscreen(); const worker new Worker(ai-worker.js); worker.postMessage({ canvas: offscreen }, [offscreen]); // ai-worker.js self.onmessage async (e) { const ctx e.data.canvas.getContext(2d); // 所有图像处理、推理、绘制都在此线程 const result await runInference(imageData); ctx.putImageData(result, 0, 0); };Worker 内部再分层Worker 线程也不应被阻塞。我们将流程拆为三阶段采集层用MediaStreamTrackProcessorChrome 114或requestVideoFrameCallback获取VideoFrame转为ImageBitmap预处理层用createImageBitmap()OffscreenCanvas缩放、裁剪、归一化输出Float32Array推理层将Float32Array转为tf.tensor调用模型结果转回Uint8ClampedArray绘制。 每层间用postMessage传递 ArrayBuffer零拷贝。2.4 法则四模型不是越小越好而是“刚好够用”的精度-速度平衡点工程师常陷入“模型压缩竞赛”用 QAT 量化到 int8剪枝掉 70% 参数蒸馏成 tiny-yolo。但端侧的真实约束是“可接受的延迟”而非“最低的 FLOPs”。我们对比过三种模型在 iPhone SE (2020) 上的表现模型输入尺寸推理耗时 (ms)mAP0.5是否满足产线要求YOLOv5s (FP32)640×4802100.82否超帧预算YOLOv5s (int8)640×480950.76是但漏检率↑12%YOLOv5n (int8)320×240420.68是漏检率↑5%可接受关键发现YOLOv5n 在 320×240 下的 42ms比 YOLOv5s 在 640×480 下的 95ms 更优——因为前者允许我们以 24fps 运行1000/42≈23.8后者只能勉强 10fps1000/95≈10.5而产线要求是 ≥15fps。精度损失 4 个点换来帧率翻倍这是产线愿意买单的交易。我们最终选择 YOLOv5n并用tf.image.resizeBilinear()在预处理层做智能缩放对小目标区域局部放大补偿分辨率损失。3. 从 PyTorch 到浏览器端侧视觉模型的七步炼金术把一个在 PyTorch 中训练好的视觉模型如分类、检测、分割真正“塞进”浏览器远不止torchscript导出 tfjs.converters转换这么简单。这是一个涉及模型结构改造、算子兼容性、量化策略、运行时优化的完整链条。我以一个实际项目——基于 ResNet-18 的 PCB 缺陷分类模型——为例拆解从训练完的.pth文件到浏览器中稳定运行的七步全流程。每一步都藏着决定成败的细节。3.1 第一步训练时就为端侧埋下伏笔——模型结构的“可部署性”设计很多团队在训练阶段完全不考虑部署等模型效果达标后才开始“适配浏览器”结果发现大量算子不支持。TF.js 支持的算子集远小于 PyTorch截至 v4.15仅支持约 120 个核心算子而 PyTorch 有 800。例如torch.nn.AdaptiveAvgPool2d→ TF.js 无直接对应需替换为torch.nn.AvgPool2d并手动计算 kernel_sizetorch.nn.SiLUSwish→ TF.js 无原生支持需用x * tf.sigmoid(x)替代torch.nn.GroupNorm→ TF.js 仅支持BatchNorm需在训练时改用BatchNorm2d并冻结running_mean/std。我们的做法训练脚本中注入兼容检查在model.eval()后用torch.jit.trace生成 ScriptModule再遍历graph.nodes()检查所有kind()是否在 TF.js 白名单内。不在白名单的算子立即报错并提示替代方案。放弃“先进”结构不用EfficientNetV2的 Fused-MBConv改用ResNet的标准 Bottleneck不用Vision Transformer的nn.MultiheadAttention改用ConvNeXt的DepthwiseSeparableConv。牺牲一点 SOTA换取 100% 算子支持。固定输入尺寸训练时强制所有样本 resize 到统一尺寸如 224×224避免推理时动态 shape 导致 TF.js 编译失败。我们甚至在 DataLoader 中加入transforms.Resize((224, 224))作为最后一道保险。3.2 第二步导出为 TorchScript而非 ONNX——规避中间格式的陷阱业界常推荐 PyTorch → ONNX → TF.js 路径但我们踩过太多坑ONNX opset 版本混乱opset 11 vs 15、自定义算子丢失、动态 batch size 导致 TF.js 解析失败。最终我们回归最稳路径PyTorch → TorchScript → TF.js。关键操作使用torch.jit.script而非tracetrace会记录具体输入的执行路径对控制流if/else不友好script是静态分析能正确处理条件分支。我们的缺陷分类模型有if confidence 0.9: return early逻辑必须用script。导出时指定example_inputs确保torch.jit.script(model, example_inputs)中的example_inputs是torch.randn(1, 3, 224, 224)batch size1。TF.js 要求模型输入 shape 固定动态 batch 会报错。保存为.pt而非.pth.pt是 TorchScript 标准格式.pth可能是 state_dictTF.js converter 无法识别。# train.py 最终导出代码 model.eval() scripted_model torch.jit.script(model) scripted_model.save(pcb_classifier.pt) # 注意.pt 后缀3.3 第三步TF.js Converter 的隐藏参数——量化不是开关是精细手术tfjs.converters.convert_torchscript_model()的quantize参数常被简单设为True但这会导致全局 int8 量化精度暴跌。我们必须分层、分算子定制量化策略。我们的量化配置表JSON{ quantize: true, weight_shard_size_bytes: 4194304, control_flow_v2: true, skip_op_check: false, strip_debug_ops: true, quantization_config: { activation_quantization: int8, weight_quantization: int8, quantize_input: true, quantize_output: true, layer_quantization: [ { layer_name: conv1, activation_bits: 8, weight_bits: 4, bias_bits: 32 }, { layer_name: layer1.0.conv2, activation_bits: 8, weight_bits: 8, bias_bits: 32 } ] } }为什么 conv1 用 weight_bits4首层卷积权重分布稀疏4-bit 量化误差 0.3%但模型体积减少 50%。为什么 bias_bits32偏置项对精度敏感一律保持 float32。skip_op_check: false强制 converter 校验每个算子避免生成不兼容的模型。转换命令tensorflowjs_converter \ --input_formattorchscript \ --output_formattfjs_graph_model \ --quantize \ --quantization_configquant_config.json \ pcb_classifier.pt \ ./web_model3.4 第四步模型加载的“冷启动”优化——预热、缓存、渐进式加载用户首次打开页面tf.loadGraphModel(./web_model/model.json)可能耗时 2-5 秒尤其在 3G 网络下。我们不能让用户盯着 loading 圈等待。三重优化Service Worker 预缓存在sw.js中install事件里caches.open(ai-model).then(cache cache.addAll([./web_model/model.json, ./web_model/group1-shard1of1]))。首次加载后后续访问秒开。WebAssembly 预热在模型加载前先执行一个空的 WASM 计算// 预热 WASM 后端 if (tf.getBackend() wasm) { const dummy tf.tensor([1, 2, 3]); dummy.sum().dataSync(); // 触发 WASM 初始化 }渐进式加载将大模型拆分为多个 shardgroup1-shard1of3,group1-shard2of3...先加载model.json和第一个 shard立即tf.loadGraphModel()此时模型已可运行部分权重缺失会报错但可捕获后台继续加载剩余 shards用model.weightsAPI 动态 patch。用户感知为“模型已加载正在优化”。3.5 第五步推理引擎的“心跳监测”——防止长任务阻塞 UI即使用了 OffscreenCanvas模型推理本身仍是长任务。若单次推理超 50ms仍可能触发浏览器的“页面无响应”警告。我们必须给推理过程加“心跳”。实现方案分块推理Chunked Inference将大模型按层分组每组推理后await tf.nextFrame()async function chunkedPredict(model, input) { let x input; for (let i 0; i model.layers.length; i) { x model.layers[i].apply(x); if (i % 5 0) await tf.nextFrame(); // 每5层暂停一次 } return x; }超时熔断为model.predict()设置 200ms 熔断const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), 200); try { const result await model.predict(input, { signal: controller.signal }); clearTimeout(timeoutId); return result; } catch (e) { clearTimeout(timeoutId); throw new Error(Inference timeout, fallback to lower resolution); }3.6 第六步后处理的“像素级”精度——避开 Canvas 的抗锯齿陷阱模型输出的 bounding box 坐标是浮点数如[123.45, 67.89, 189.23, 134.56]但ctx.strokeRect()绘制时若坐标非整数Canvas 会启用抗锯齿导致边框模糊、宽度不一致。在工业质检中1 像素的偏差即意味着误判。解决方案坐标取整 像素对齐所有坐标Math.round()并确保 canvas 的devicePixelRatio与 CSS 尺寸匹配const dpr window.devicePixelRatio || 1; canvas.width canvas.clientWidth * dpr; canvas.height canvas.clientHeight * dpr; ctx.scale(dpr, dpr); const [x1, y1, x2, y2] bbox.map(Math.round); ctx.strokeStyle red; ctx.lineWidth 2; ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);文字标签抗锯齿ctx.fillText()用ctx.textRendering geometricPrecision关闭字体平滑。3.7 第七步生产环境的“灰度发布”——用 Feature Flag 控制模型版本不同设备性能差异巨大。我们不会一刀切地全量上线新模型而是用 Feature Flag 实现灰度// feature-flag.js export const getActiveModel (deviceInfo) { const { cpuCores, gpuVendor, memory } deviceInfo; if (gpuVendor Apple memory 8) return yolov5s-640-int8; if (cpuCores 4 memory 4) return yolov5n-320-int8; return mobilenetv2-224-fp16; // 最保守版本 };前端在加载模型前先调用getActiveModel(getDeviceInfo())再请求对应模型文件。AB 测试数据显示此策略使低端设备崩溃率下降 68%高端设备推理速度提升 22%。4. 真实世界的“端侧视觉 AI”五个已落地的场景与它们的工程答案理论再扎实不如看真实战场。我参与或深度调研过五个已上线的端侧视觉 AI 应用它们覆盖不同行业、不同约束每个都给出了独特的工程解法。这些不是 Demo而是每天处理真实业务流量的系统。4.1 场景一远程医疗问诊中的皮肤癌初筛——隐私优先的“零数据出域”需求患者用手机拍摄皮肤病变部位照片AppWeb PWA在本地运行分割模型标出病灶区域计算面积/颜色特征生成报告。核心约束图像绝不离开用户设备符合 HIPAA/GDPR。工程解法模型选择U-Net 轻量版32 通道输入 256×256输出 256×256 mask。放弃高精度但巨大的 TransUNet。隐私加固所有图像处理在OffscreenCanvasWorker中完成MediaRecorder录制视频时videoBitsPerSecond设为 0强制只采集帧不编码。结果可信度模型输出 mask 后不直接显示而是用tf.image.extractGlimpse()截取病灶中心 64×64 区域再送入一个独立的“置信度评估小模型”3 层 FC输出 0-1 分数。分数 0.7 时UI 显示“AI 无法确定请线下就诊”避免过度承诺。实测效果在 Pixel 4a 上端到端耗时 850ms含拍照、处理、报告生成准确率 89.2%vs 三甲医院皮肤科医生 91.5%。4.2 场景二跨境电商的“AR 试衣镜”——毫秒级响应的实时姿态估计需求用户站在摄像头前网页实时叠加虚拟服装要求姿态估计延迟 60ms否则衣服会“飘”在人身上。工程解法模型架构放弃 Heavy 的 HRNet采用 Blazepose 的轻量版BlazePose Full Body Lite但修改其输出头原模型输出 33 个关键点我们只保留 17 个头部、肩、肘、腕、髋、膝、踝砍掉手指、眼睛等冗余点推理耗时从 45ms 降至 28ms。帧率锁定用requestVideoFrameCallback替代setInterval确保每帧视频数据到达时立即处理避免队列堆积。骨骼插值当某帧因 GC 或其他原因丢失时用上一帧关键点 线性插值生成过渡帧保证动画连续。插值系数alpha 1 - (currentFrameTime - lastFrameTime) / 3333ms 是 30fps 帧间隔。WebGL 渲染优化虚拟服装用 Three.js但骨骼绑定不用Bone改用InstancedMesh 自定义 shader将 17 个关键点坐标传入 uniformGPU 端实时计算顶点位移。CPU 开销降低 70%。4.3 场景三智慧农业的“无人机田间巡检”——离线环境下的鲁棒性设计需求农户在无网络的农田里用平板电脑连接无人机图传实时分析水稻叶瘟病斑。核心约束全程离线平板存储有限32GB电池续航 4 小时。工程解法模型极致压缩YOLOv5n 量化为 int4非标准自研工具链模型体积从 12MB 压至 1.8MB用tfjs.converters.convert_tf_saved_model的--weight_shard_size_bytes1048576拆分为 2 个 shard便于增量更新。离线缓存策略Service Worker 缓存全部模型文件 常用病害图谱100 张高清图总大小 50MB。fetch事件中优先caches.match()失败才走网络。电池感知监听navigator.getBattery()当电量 20% 时自动将输入分辨率从 640×480 降至 320×240并关闭非关键日志延长续航 1.8 小时。结果持久化检测结果病斑位置、面积、置信度用 IndexedDB 存储网络恢复后批量同步至云端。4.4 场景四工业质检的“PCB 焊点缺陷识别”——高可靠性的“双模型仲裁”需求SMT 产线上AOI 设备摄像头实时拍摄 PCB 板Web 系统需在 15fps 下识别虚焊、连锡、漏印。核心约束误报率 0.1%漏报率 0.5%7×24 小时运行。工程解法双模型架构主模型YOLOv5n-int8快速检测副模型ResNet-18-int8对主模型输出的每个候选框进行二次分类正常/虚焊/连锡/漏印。副模型输入是裁剪后的 ROIRegion of Interest尺寸 64×64。仲裁逻辑仅当主模型置信度 0.6 且副模型置信度 0.8 时才判定为缺陷。否则标记为“待复检”推送到人工审核队列。可靠性监控在 Worker 中每 100 帧统计一次main_model_confidence_avg和secondary_model_accuracy若连续 5 次低于阈值自动触发模型热重载从缓存中重新加载。实测指标上线 6 个月误报率 0.07%漏报率 0.32%平均帧率 18.2fps。4.5 场景五教育科技的“手写公式识别”——低延迟与高精度的平衡术需求学生用平板手写数学公式网页实时识别为 LaTeX延迟 300ms支持复杂符号积分、矩阵、希腊字母。工程解法模型选型放弃通用 OCR采用专门训练的FormulaNetCNN-LSTM-CTC但输入预处理革命不直接喂原始笔迹而是先用 OpenCV.js 在 Worker 中做cv.threshold()二值化cv.findContours()提取所有笔画轮廓cv.boundingRect()为每个轮廓生成 tight bounding box将所有 box 按书写顺序x 坐标 y 坐标排序拼接为“公式序列”。延迟优化CTC 解码是瓶颈。我们用 WebAssembly 重写 CTC decoder基于wasm-ctc耗时从 120ms 降至 22ms。LaTeX 后处理模型输出 raw token用规则引擎修正如int→\intsum→\sumalpha→\alpha并自动添加\displaystyle。用户体验识别中显示“正在思考…”动画结果用MathJax渲染但首次渲染后缓存 SVG后续直接复用避免 MathJax 重排。5. 踩过的坑那些没写在文档里的“端侧视觉 AI”暗礁文档是理想国现实是沼泽地。以下是我和团队在过去三年中踩过并填平的七个典型暗礁。它们不常出现在教程里却足以让一个项目停滞数周。5.1 暗礁一tf.browser.fromPixels()的“隐形内存泄漏”——你以为的图片其实是 GPU 纹理tf.browser.fromPixels(videoElement)是最常用的图像输入方式但它有个致命特性它会将video的当前帧直接绑定为 WebGL 纹理且该纹理的生命周期由 TF.js 引擎管理而非开发者。我们曾遇到一个诡异现象在 Chrome 95 上连续调用fromPixels()100 次后GPU 内存占用飙升至 2GBtf.memory()显示numTensors却只有 5。原因在于fromPixels()创建的 Tensor 是“外部纹理引用”dispose()无效必须等videoElement释放或页面刷新。破解方案永远用tf.browser.fromPixels()tf.clone()const tensor tf.browser.fromPixels(videoElement); const cloned tensor.clone(); // 创建独立内存副本 tensor.dispose(); // 立即释放外部引用 return cloned;或改用OffscreenCanvas路径videoElement.captureStream().getVideoTracks()[0].requestFrame()获取VideoFrame再createImageBitmap(frame)最后tf.browser.fromPixels(bitmap)。ImageBitmap是独立内存无外部引用。5.2 暗礁二requestIdleCallback的“虚假空闲”——浏览器说它空闲其实正忙着编译 shaderrequestIdleCallback常被用于“在浏览器空闲时加载模型”但它的回调时机极不可靠。我们发现在tf.setBackend(webgl)后首次调用model.predict()时浏览器会触发 WebGL shader 编译此过程长达数百毫秒且requestIdleCallback会错误地认为这是“空闲”导致模型加载代码在 shader 编译中途被插入引发竞态。破解方案用performance.now()setTimeout替代测量tf.ready()到model.predict()的首次耗时若 100ms则认为是 shader 编译期后续加载推迟 500ms。主动触发编译在页面加载后立即运行一个 dummy inference// 页面初始化后立即执行 const dummyInput tf.zeros([1, 3, 224, 224]); model.predict(dummyInput).dispose(); dummyInput.dispose(); // 此时 shader 已编译完毕后续真实推理稳定5.3 暗礁三OffscreenCanvas的“跨域污染”——Canvas 的 origin-clean 标志会传染当 Offscreen
返回列表