TensorFlow.js端侧推理实战:浏览器AI落地的性能、兼容与工程化

发布时间:2026/10/4 15:10:48
TensorFlow.js端侧推理实战:浏览器AI落地的性能、兼容与工程化 1. 为什么说“让机器学习真正跑在用户的设备上”不是一句空话“TensorFlow.js 实战让机器学习真正跑在用户的设备上”——这个标题里“真正”两个字不是修辞而是技术落地的分水岭。过去五年我带团队做过二十多个前端AI项目从人脸美颜滤镜到工业质检辅助标注从教育类手写识别到医疗影像初筛提示所有成功上线的案例无一例外都绕不开一个核心判断模型能不能在用户打开网页的3秒内完成首次推理且全程不发一次网络请求如果答案是否定的那它就不是“真正跑在设备上”只是把服务器端的计算结果伪装成本地执行。TensorFlow.js 不是 TensorFlow 的 Web 端翻译器它是为浏览器环境彻底重写的推理引擎底层直连 WebGL、WebAssembly 和 Web Workers把 GPU 显存当内存用把 JS 堆栈当计算图调度器用。你看到的tf.loadLayersModel(model.json)背后是把 Keras 模型的权重二进制流解包成 TypedArray再逐层映射到 WebGL 纹理你调用的model.predict(input)实际触发的是 WebGL Shader 编译、纹理绑定、GPU Draw Call 调度——整个链路没有 HTTP 中间层没有服务端转发没有 token 鉴权跳转。这决定了它的适用边界适合处理单次输入 5MB、推理耗时 200ms、内存占用 128MB 的轻量级任务。比如用 MobileNetV2 做实时摄像头分类224×224 输入在 iPhone 12 上实测首帧 142ms后续帧稳定在 68ms但若换成 ResNet-50同一设备首帧直接卡顿到 1.2s用户已经切走了。所以“跑在设备上”的本质是用浏览器能力做算力兜底用模型压缩做体验守门员。它解决的不是“能不能做AI”而是“用户愿不愿意多等两秒”。关键词里反复出现的“谷歌浏览器”“chrome浏览器”“edge浏览器”“ios浏览器”恰恰说明这件事的成败不取决于算法多先进而取决于你有没有亲手在 Safari 的 WebKit 内核里调过tf.webgl.isWebGLAvailable()有没有为 iOS 15 的 Metal 后端补过tf.setBackend(webgl)的 fallback 逻辑有没有在安卓低端机上手动降级到cpubackend 并接受 8 倍推理延迟。这不是框架选型问题是工程交付的生死线。2. 核心设计思路为什么必须放弃“复刻 Python 生态”的幻想2.1 浏览器不是服务器更不是 Jupyter Notebook很多刚接触 TensorFlow.js 的开发者第一反应是“把 Python 代码改个 import 就行”。我见过最典型的错误是直接把 Keras 训练脚本里的model.fit()拿过来试图在浏览器里训练 ResNet。结果页面卡死、内存暴涨、风扇狂转——这不是 bug是根本性误判。浏览器环境有三道硬墙内存上限通常 ≤512MB、GPU 上下文隔离每个 tab 独立 WebGL context、无持久化存储IndexedDB 写入速度 ≈ 20MB/s。这意味着训练 ≠ 推理TensorFlow.js 官方明确标注tf.train.*API 仅用于极小规模微调如迁移学习中的最后两层且必须配合tf.data流式加载否则 OOM 是必然结局。真实项目中99% 的模型都是离线训练好导出为.json .bin权重文件再由前端加载。模型不是越大越好Python 里用tf.keras.applications.EfficientNetB3很自然但在浏览器里B3 版本参数量 12M加载解析耗时 1.8sChrome 115而 B0 版本 5.3M耗时 0.6s。我们曾为某教育 App 做过 A/B 测试B0 模型准确率下降 3.2%但用户留存率提升 27%因为首屏交互时间从 3.4s 缩短到 1.9s。精度换体验在端侧是铁律。数据管道必须重构Python 中ImageDataGenerator的实时增强在浏览器里得用 Canvas 2D API 手动实现。比如旋转操作不能调skimage.transform.rotate()而要将img绘制到canvas获取 canvas 的ImageData对像素数组做双线性插值计算写回 canvas转为tf.tensor3d()。这个过程在 CPU 上比 Python 慢 4~5 倍但好处是全程可控、无依赖、可调试。提示别迷信“自动转换工具”。TensorFlow.js Converter 虽然能将 SavedModel 转成 web 格式但它不会帮你做量化感知训练QAT。我们线上项目强制要求所有模型导出前必须在 Python 端完成 INT8 量化否则前端加载时tf.loadLayersModel()会因权重精度不匹配报错。量化不是可选项是必经工序。2.2 WebGL 是双刃剑性能跃升与兼容性深渊并存标题里特意点出“WebGL”因为它才是 TensorFlow.js 真正的性能心脏。但现实很骨感WebGL 1.0 和 2.0 在不同浏览器的表现差异远大于 Chrome 和 Safari 的版本差异。我们做过全平台测试覆盖 iOS 14~17、Android 10~14、Windows/macOS 主流浏览器关键发现如下设备/浏览器WebGL 1.0 支持WebGL 2.0 支持tf.webgl.isWebGLAvailable() 返回值实际可用 backendiPhone 13 / Safari 16✅❌iOS 限制truewebgliPad Pro / Chrome 114✅✅truewebglPixel 6 / Chrome 115✅✅truewebglWindows / Edge 116✅✅truewebgl旧款安卓机 / UC 13.5✅❌truewebgl降级模式某国产浏览器 / v12.3❌❌falsecpu注意isWebGLAvailable()返回 true不代表能用 full 功能。Safari 的 WebGL 1.0 不支持OES_texture_float扩展导致 float32 权重无法直接上传纹理必须降级为 float16 或 uint8而某些安卓 WebView如微信内置浏览器虽支持 WebGL 2.0但gl.getShaderPrecisionFormat()返回LOW_FLOAT意味着 shader 里不能用高精度计算。我们的解决方案是在 model 加载前强制运行一段 probe shaderfunction detectWebGLCapability() { const gl document.createElement(canvas).getContext(webgl); if (!gl) return { backend: cpu, precision: default }; // 检测 float32 支持 const ext gl.getExtension(OES_texture_float); const hasFloat ext gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS) 1024; // 检测 highp 支持 const shader gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(shader, precision highp float; void main() {}); gl.compileShader(shader); const isHighp gl.getShaderParameter(shader, gl.COMPILE_STATUS); return { backend: hasFloat ? webgl : cpu, precision: isHighp ? highp : mediump }; }这个 probe 过程耗时约 8~12ms但它避免了后续model.predict()报出难以定位的GL_INVALID_OPERATION错误。很多团队省掉这步结果在 iOS 上模型加载成功但 predict 时黑屏——因为 shader 编译失败而 TF.js 默认静默吞掉错误。2.3 端侧推理的三大不可妥协原则基于上百个真实项目踩坑我总结出端侧推理的“铁三角”原则违反任一条项目大概率延期或失败输入尺寸锁定原则浏览器里没有动态 shape。Python 中input_shape(None, 224, 224, 3)的模型在 TF.js 里必须指定固定尺寸如(1, 224, 224, 3)。我们曾遇到一个 Bug模型导出时用了tf.keras.layers.Resizing(224, 224)但 TF.js 不支持该 layer导致前端 resize 逻辑和后端不一致分类结果完全错误。解决方案所有预处理逻辑resize/crop/normalize必须在前端用纯 JS 实现并与训练时的 preprocessing.py 严格对齐。我们维护一个preprocess.ts文件里面包含resizeImage(img: HTMLImageElement, size: [number, number]): PromiseHTMLCanvasElementnormalizeTensor(tensor: tf.Tensor3D): tf.Tensor3D减均值除标准差数值与训练时完全一致toBatchTensor(canvas: HTMLCanvasElement): tf.Tensor4D增加 batch 维度内存生命周期管理原则TF.js 的 tensor 不是 JS 对象它绑定 WebGL 纹理或 WASM 内存页。tf.tensor([1,2,3])创建的 tensor若不手动tensor.dispose()就会永久占用 GPU 显存。我们在某电商 App 的商品图识别功能中因忘记 dispose 中间 tensor连续识别 15 张图后iPhone XS 内存占用飙升至 480MB系统强制 kill 页面。正确做法是async function predict(image: HTMLImageElement) { const input preprocess(image); // 返回 tf.Tensor4D const result model.predict(input) as tf.Tensor2D; const data await result.data(); // 触发同步获取 JS 数组 result.dispose(); // 必须释放输出 input.dispose(); // 必须释放输入 return decodeResult(data); }注意await result.data()是关键。它强制 tensor 数据从 GPU 同步到 CPU 内存此时再 dispose 才安全。如果先 dispose 再 await会报Tensor is disposed错误。降级策略兜底原则永远假设 WebGL 不可用。我们的标准初始化流程是async function initModel() { try { await tf.setBackend(webgl); await tf.ready(); model await tf.loadLayersModel(/model/model.json); } catch (e) { console.warn(WebGL init failed, fallback to CPU); await tf.setBackend(cpu); await tf.ready(); model await tf.loadLayersModel(/model/model.json); } }但这里有个陷阱CPU backend 的predict()比 WebGL 慢 10~30 倍。所以必须配套 UI 降级——当检测到 CPU backend 时自动隐藏实时摄像头预览改为上传图片后处理并显示“处理中…约需 3 秒”提示。用户感知从“卡顿”变成“可预期等待”体验差距巨大。3. 实操全流程拆解从模型导出到生产部署的 7 个关键环节3.1 Python 端训练后必须做的 4 项模型瘦身操作TensorFlow.js 的模型体积直接决定首屏加载时间。一个未优化的 MobileNetV2 模型SavedModel 格式约 18MB转成 TF.js 后达 22MB含权重 bin 文件。我们要求所有上线模型必须完成以下四步瘦身第一步移除训练专用节点Keras 模型默认包含trainingTrue/False分支TF.js 无法解析。导出前必须# 训练完成后创建 inference model inference_model tf.keras.models.clone_model(model) inference_model.set_weights(model.get_weights()) # 强制设为推理模式 inference_model.trainable False for layer in inference_model.layers: layer.trainable False然后用tf.keras.models.save_model(inference_model, inference_model, include_optimizerFalse)保存。第二步INT8 量化精度损失可控TF.js 官方推荐使用 TensorFlow Lite 的量化流程但我们发现 TFLite converter 对复杂自定义 layer 兼容性差。更稳的方案是用 TF 的tf.quantization.fake_quant_with_min_max_vars做量化感知训练QAT# 在训练循环中插入量化节点 quantized_model tf.keras.models.clone_model(model) for i, layer in enumerate(quantized_model.layers): if isinstance(layer, tf.keras.layers.Conv2D): quantized_model.layers[i] tf.keras.layers.Lambda( lambda x: tf.quantization.fake_quant_with_min_max_vars(x, min-6.0, max6.0, num_bits8) )(layer.input) # 用量化模型继续训练 10 个 epoch量化后模型精度通常下降 1~2%但体积减少 75%。实测 MobileNetV2 从 22MB 降至 5.3MB。第三步权重合并与压缩TF.js 导出默认生成多个.bin文件每层一个HTTP/1.1 下并发请求数受限。我们用tensorflowjs_converter的--weight_shard_size_bytes参数强制合并tensorflowjs_converter \ --input_formattf_saved_model \ --output_formattfjs_graph_model \ --weight_shard_size_bytes10000000 \ inference_model \ tfjs_model/10000000表示单个 shard 最大 10MB通常合并为 1~2 个文件。同时开启 gzip 压缩Nginx 配置gzip on; gzip_types application/json;5.3MB 模型压缩后仅 1.8MB。第四步删除冗余 metadataTF.js 模型 JSON 文件包含大量调试信息如 layer name、input shape 描述。生产环境可安全删除import json with open(tfjs_model/model.json, r) as f: meta json.load(f) # 删除非必要字段 for layer in meta[modelTopology][layers]: layer.pop(config, None) # config 已在 topology 中 layer.pop(name, None) # name 可从 topology 推导 with open(tfjs_model/model.json, w) as f: json.dump(meta, f)此操作可减少 JSON 体积 30%且不影响推理。3.2 前端加载如何让 model.json 在 1 秒内解析完毕模型加载慢80% 的原因是 JSON 解析阻塞主线程。tf.loadLayersModel(model.json)默认同步解析大型模型 JSON 达 2MBChrome 解析需 300~500ms期间页面完全卡死。我们的解决方案是Web Worker Streaming 解析// worker.js self.onmessage async ({ data }) { const response await fetch(data.url); const reader response.body.getReader(); let chunks []; while (true) { const { done, value } await reader.read(); if (done) break; chunks.push(value); } // 合并所有 chunk const uint8Array new Uint8Array(chunks.reduce((acc, chunk) { const newAcc new Uint8Array(acc.length chunk.length); newAcc.set(acc); newAcc.set(chunk, acc.length); return newAcc; }, new Uint8Array(0))); // 解析 JSON仍在 worker 线程 const jsonStr new TextDecoder().decode(uint8Array); const modelJson JSON.parse(jsonStr); // 加载权重仍需主线程但 JSON 已 ready self.postMessage({ type: json_ready, modelJson }); }; // main.js const worker new Worker(./model-loader-worker.js); worker.postMessage({ url: /model/model.json }); worker.onmessage ({ data }) { if (data.type json_ready) { // 此时 modelJson 已解析完毕立即启动权重加载 tf.loadLayersModel({ modelUrl: /model/model.json, weightsManifestUrl: /model/weights_manifest.json, weightLoader: async (manifest) { // 自定义权重加载可加 loading 进度 const promises manifest.map(async (group) { const res await fetch(/model/${group.paths[0]}); return res.arrayBuffer(); }); return Promise.all(promises); } }).then(model { window.model model; console.log(Model loaded in, performance.now() - start); }); } };此方案将 JSON 解析从主线程剥离实测 2MB JSON 解析时间从 420ms 降至 12msworker 线程主线程全程流畅。注意weightLoader回调必须返回ArrayBuffer不能返回Response否则 TF.js 内部会再次 fetch。3.3 输入预处理HTML 坐标系到 WebGL 坐标系的精准映射标题热词里提到“将html坐标系转化为webgl坐标系”这绝非理论问题而是实时姿态估计类项目的生死线。HTML 坐标系左上原点y 向下与 WebGL 坐标系左下原点y 向上方向相反且 WebGL 的 NDC归一化设备坐标范围是 [-1,1]而 Canvas 像素坐标是 [0,width]×[0,height]。常见错误是直接用canvas.width/height做缩放导致模型输出的关节点坐标偏移。正确映射公式webgl_x (html_x / canvas_width) * 2 - 1 webgl_y 1 - (html_y / canvas_height) * 2 // 注意 y 方向翻转但实际更复杂Canvas 的 CSS 尺寸 ≠ 实际像素尺寸。例如canvas idgl-canvas width640 height480 stylewidth: 320px; height: 240px;/canvas此时canvas.width640,canvas.height480但 CSS 渲染尺寸是 320×240缩放因子为 0.5。若忽略此点用clientX/clientY直接计算坐标会放大 2 倍。我们的标准预处理函数function htmlToWebGLCoords( clientX: number, clientY: number, canvas: HTMLCanvasElement ): [number, number] { // 获取 canvas 在视口中的真实位置 const rect canvas.getBoundingClientRect(); // 计算相对于 canvas 左上角的坐标考虑 CSS 缩放 const x clientX - rect.left; const y clientY - rect.top; // 转换为 canvas 像素坐标考虑 devicePixelRatio const dpr window.devicePixelRatio || 1; const pixelX x * dpr; const pixelY y * dpr; // 转换为 WebGL NDC 坐标 const glX (pixelX / canvas.width) * 2 - 1; const glY 1 - (pixelY / canvas.height) * 2; return [glX, glY]; }此函数已通过 AR 教学项目验证在 iPad Pro 上手指点击屏幕任意位置模型输出的 3D 关节点与点击点误差 3 像素物理距离 0.5mm。3.4 推理加速WebGL Shader 的定制化优化实践TF.js 的 WebGL backend 默认使用通用 shader对特定模型结构如 DepthwiseConv2D效率不高。我们曾为某手势识别模型含 12 个 DepthwiseConv做定制优化将推理速度从 42ms 提升至 28msiPhone 12。关键优化点合并 Conv BatchNorm ReLUTF.js 默认为每个 layer 生成独立 shader但 DepthwiseConv 后紧跟 BN 和 ReLU可合并为单个 shader。我们 fork TF.js在src/kernels/webgl/depthwise_conv2d_gpu.ts中修改_getKernelFunc将 BN 的 gamma/beta 参数注入 shader uniformReLU 逻辑直接写在 fragment shader 里// 合并后的 shader 片段 vec4 convResult ...; // depthwise conv 计算 convResult convResult * uGamma uBeta; // BN convResult max(convResult, 0.0); // ReLU纹理格式优化默认用RGBA格式存储权重但 DepthwiseConv 只需单通道。修改src/kernels/webgl/texture_util.ts对 depthwise 层权重使用R8格式显存占用减少 75%带宽压力骤降。避免 texture copyTF.js 默认每次 predict 都重新 upload input texture。我们 patchsrc/kernels/webgl/gpgpu_math.ts添加 input texture 缓存机制相同尺寸 input 复用 texture object减少gl.texImage2D调用频次。注意此类优化需深入 TF.js 源码建议只对核心模型做。我们团队维护一个 patched 版本通过npm install githttps://github.com/our-org/tfjs.git#ios-depthwise-opt安装。3.5 输出后处理从 raw tensor 到用户可理解结果的桥梁模型输出的tf.Tensor2D如[1, 1000]分类 logits不能直接给用户。必须做三步后处理Step 1Softmax 归一化TF.js 的tf.softmax()在 WebGL backend 下有精度问题尤其 iOS我们改用 CPU 计算const logits output.arraySync()[0]; // 同步获取 JS 数组 const expLogits logits.map(x Math.exp(x - Math.max(logits))); // 减去最大值防溢出 const sumExp expLogits.reduce((a, b) a b, 0); const probs expLogits.map(x x / sumExp);Step 2Top-K 筛选避免遍历全部 1000 类。用快速选择算法QuickSelect找 top-3function topK(arr: number[], k: number): number[] { const indices Array.from({length: arr.length}, (_, i) i); const partition (left: number, right: number, pivotIndex: number): number { const pivotValue arr[pivotIndex]; [arr[pivotIndex], arr[right]] [arr[right], arr[pivotIndex]]; let storeIndex left; for (let i left; i right; i) { if (arr[i] pivotValue) { [arr[i], arr[storeIndex]] [arr[storeIndex], arr[i]]; [indices[i], indices[storeIndex]] [indices[storeIndex], indices[i]]; storeIndex; } } [arr[right], arr[storeIndex]] [arr[storeIndex], arr[right]]; [indices[right], indices[storeIndex]] [indices[storeIndex], indices[right]]; return storeIndex; }; let left 0, right arr.length - 1; while (left right) { const pivotIndex Math.floor(Math.random() * (right - left 1)) left; const pivotNewIndex partition(left, right, pivotIndex); if (pivotNewIndex k) break; else if (pivotNewIndex k) right pivotNewIndex - 1; else left pivotNewIndex 1; } return indices.slice(0, k); }Step 3标签映射与置信度过滤建立labelMap.json1000 类 ImageNet 标签并设置阈值{ 0: tench, 1: goldfish, ... }const topIndices topK(probs, 3); const results topIndices .map(i ({ label: labelMap[i], prob: probs[i] })) .filter(r r.prob 0.1); // 低于 10% 置信度不显示3.6 跨浏览器兼容性实战Safari、Chrome、Edge 的差异化处理SafariiOS/macOSWebGL 2.0 不可用强制用 WebGL 1.0tf.webgl.setWebGLContext()必须传preserveDrawingBuffer: true否则canvas.toDataURL()截图为空IndexedDB 存储模型时iOS 15 有 50MB 限制需分片存储解决方案tf.setBackend(webgl)后立即检查gl.getParameter(gl.MAX_TEXTURE_SIZE)若 2048则降级到cpu。ChromeAndroid/DesktopWebGL 2.0 全面支持但低端安卓机如联发科 Helio P22的 Mali-G52 GPU 有 shader 编译 bug解决方案捕获gl.getProgramInfoLog()若含Compilation failed则 fallback 到cpu启用tf.env().set(WEBGL_PACK, true)开启矩阵打包优化提速 15%。EdgeWindows基于 Chromium行为与 Chrome 一致但默认禁用 WebAssembly SIMD解决方案tf.env().set(WASM_HAS_SIMD_SUPPORT, true)强制启用需用户手动开启浏览器 flag对于大模型Edge 的内存回收更激进需更频繁调用tf.memory().numTensors监控。3.7 生产监控如何在用户端埋点追踪模型健康度端侧模型没有日志系统必须主动埋点。我们定义 5 个核心健康指标指标名采集方式告警阈值业务含义model_load_timeperformance.now()记录loadLayersModel开始/结束 3000msCDN 或模型体积异常predict_latencyperformance.now()记录predict开始/结束 500msWebGL/ 3000msCPU设备性能不足或模型过重gpu_memory_usedtf.memory().numBytesInGPU 100MBWebGL 内存泄漏tensor_dispose_rate统计dispose()调用次数 /tensor()创建次数 95%内存泄漏风险backend_fallback_count记录setBackend(cpu)触发次数 5 次/小时兼容性问题集中爆发埋点代码集成在模型 wrapper 中class ModelWrapper { private model: tf.LayersModel; private backend: string; async load(url: string) { const start performance.now(); try { await tf.setBackend(webgl); this.model await tf.loadLayersModel(url); this.backend webgl; } catch (e) { await tf.setBackend(cpu); this.model await tf.loadLayersModel(url); this.backend cpu; this.report(backend_fallback_count, 1); } this.report(model_load_time, performance.now() - start); } async predict(input: tf.Tensor) { const start performance.now(); const result this.model.predict(input); const latency performance.now() - start; this.report(predict_latency, latency); this.report(gpu_memory_used, tf.memory().numBytesInGPU); this.report(tensor_dispose_rate, this.getDisposeRate()); return result; } private report(key: string, value: number | string) { // 发送到监控平台带 device info navigator.sendBeacon(/api/metrics, JSON.stringify({ key, value, device: getDeviceInfo(), // 包含 os, browser, model timestamp: Date.now() })); } }这套监控让我们在某次 iOS 16.4 更新后2 小时内发现 Safari 的 WebGL context 丢失率从 0.1% 飙升至 12%迅速定位是preserveDrawingBuffer缺失导致紧急 hotfix 上线。4. 常见问题排查手册21 个真实场景问题与根因分析4.1 模型加载失败类问题问题现象根本原因解决方案验证方法Failed to load model from URL服务器未配置 CORS或model.json返回 404检查 Network 面板确认model.json和.bin文件状态码为 200在响应头添加Access-Control-Allow-Origin: *curl -I https://your-domain.com/model/model.jsonUnexpected end of JSON inputmodel.json文件被截断或 Nginx gzip 压缩损坏关闭 Nginx gzip 对.json文件的压缩gzip_types application/json text/plain;用wget下载model.jsonhead -c 100查看开头是否为{Cannot find module tensorflow项目使用 webpack 5未配置resolve.fallback在webpack.config.js中添加resolve: { fallback: { fs: false, path: false, os: false, crypto: false } }构建后检查 bundle确认无fs.readFileSync调用Error: Unknown layer: CustomLayer模型含自定义 layer但前端未注册在加载前注册tf.serialization.registerClass(CustomLayer)查看 TF.js 源码src/serialization/serialization.ts确认 layer 名匹配4.2 推理异常类问题问题现象根本原因解决方案验证方法predict()返回全零 tensor输入 tensor shape 与模型期望不符TF.js 静默填充零用input.shape打印输入维度对比模型model.inputs[0].shapeconsole.log(Input:, input.shape, Expected:, model.inputs[0].shape)Cannot read property data of undefinedmodel.predict()返回tf.SymbolicTensor图模式非tf.Tensor确保模型是tf.LayersModel非tf.GraphModel或调用await result.data()console.log(result.constructor.name)WebGL: INVALID_OPERATION: drawArrays: no valid shader programSafari 15 的 WebGL context 丢失常因页面切换或后台挂起监听visibilitychange事件页面可见时重建 contextdocument.addEventListener(visibilitychange, () { if (!document.hidden) tf.engine().startScope(); })在 Safari 开发者工具中console.log(tf.webgl.getWebGLContext())是否为 nullOut of memoryiOSiOS WebKit 对 WebGL 纹理大小有限制通常 ≤ 16MB/texture降低模型输入分辨率或启用tf.env().set(WEBGL_RENDER_FLOAT_ENABLED, false)console.log(tf.webgl.getWebGLContext().getParameter(tf.webgl.getWebGLContext().MAX_TEXTURE_SIZE))4.3 性能瓶颈类问题问题现象根本原因解决方案验证方法首帧推理 800ms后续帧 120msWebGL shader 编译耗时首帧必须编译预热加载模型后立即执行 dummy predictmodel.predict(tf.zeros([1,224,224,3]));Performance 面板查看Compile Shader时间CPU 占用持续 90%tf.tidy()未包裹计算中间 tensor 未释放所有 predict 逻辑用tf.tidy(() { ... })包裹console.log(tf.memory().numTensors)在 predict 前后对比摄像头画面卡顿requestAnimationFrame与predict()同步阻塞将 predict 移至 Web Workerworker.postMessage({ type: predict, data: tensorData })使用 Chrome 的 Rendering 面板观察 FPS 是否稳定 60模型加载后内存不释放model.dispose()未调用或存在闭包引用在组件卸载时调用componentWillUnmount() { model?.dispose(); }Memory 面板录制堆快照搜索tf.Tensor实例数4.4 兼容性疑难杂症问题现象根本原因解决方案验证方法Android 微信内置浏览器白屏微信 X5 内