在浏览器端跑深度学习这件事我一度以为是个玩具级方案直到被一个真需求逼着用 TensorFlow.js 上了生产环境才意识到自己对它的认知错得有多离谱。那是一个图像鉴伪项目用户上传照片后要求在本地完成识别图片不允许离开浏览器延迟不能超过 3 秒——这意味着模型推理、算力调度、内存回收都得在浏览器端真实落地而不是在服务端跑通再咔一个接口。所以这篇我打算把 TensorFlow.js 的架构内幕、浏览器端的算力调度机制以及我在生产级实战中踩过的那些坑一次性讲透。如果你也想在浏览器端搞深度学习但还在观望这篇文章值得看完。1. 为什么非要在浏览器端做深度学习而不是老老实实上服务器先说一个反直觉的结论浏览器端推理并不是为了省服务器钱更多时候是被数据隐私、实时性和产品形态逼出来的选择。1.1 三个真实场景让你理解浏览器端不是噱头第一个场景是医疗影像辅助质检。基层机构拍完片子如果上传到云端做分析光合规审批就要拖很久更别提患者的隐私敏感程度。把模型用 TensorFlow.js 打包进网页后影像数据从头到尾不出本地设备识别结果只在浏览器里呈现这在产品合规层面几乎是降维打击。第二个场景是电商平台的图片鉴伪和客服辅助审核。用户可能拍的是合同、身份证、发票传服务器意味着要承担数据泄露风险而浏览器端推理可以在不落盘的情况下实时反馈——用户照片还在本地模型已经告诉你有没有问题。第三个场景更日常在线抠图、AR 美颜、智能画板这类工具型产品。它们对交互延迟极其敏感一次推理想让用户感受到即时服务端往返至少 100ms 起步而浏览器端可以做到几毫秒到几十毫秒的本地计算而且没有带宽成本。1.2 服务端推理与端侧推理的边界在哪里我自己的判断标准是看四样东西模型体积、设备算力水位、数据敏感性、迭代频率。判断维度偏向浏览器端偏向服务端模型体积单模型低于 100MB量化后更低更理想超过 1GB 且必须全精度延迟要求毫秒级交互不能接受网络往返秒级任务用户能接受转圈数据敏感度隐私高不能出设备可脱敏允许上传模型更新频率低频随版本发布高频灰度、A/B、数据回流如果模型动不动几百 MB又依赖 GPU 集群才能跑那强行塞进浏览器就是在折磨用户设备反过来一个小巧的 CNN 分类器数据又敏感放服务器就是自找麻烦。TensorFlow.js 擅长的是那条中间地带模型能压缩、推理走本地、数据不出门。2. TensorFlow.js 的前中后三段式架构一条张量是如何跑起来的很多人写 TensorFlow.js 只知道tf.tensor()和model.predict()但真正决定你能不能在生产环境控场的关键在于你懂不懂它内部的三层结构。2.1 API 层、内核注册表与后端引擎的三角关系TensorFlow.js 从抽象到具体分三层API 层对外暴露的tf.matMul、tf.conv2d、tf.layers等接口支持链式调用看起来像一个简洁的张量计算库。内核注册表Kernel Registry每个算子在不同后端下都有一个独立实现。比如MatMul在 CPU 后端、WASM 后端、WebGL 后端下分别对应不同的内核函数内核按opName backendName的方式注册。后端引擎真正负责张量数据存储、内存回收、算子调度和着色器编译。TensorFlow.js 会在初始化时选一个可用后端然后所有算子都分发到该后端的实现里。这个三段式设计借鉴了 TensorFlow 本身的分层思想但做了一层明显简化。它的直接后果是你在 API 层写的代码和底层执行引擎是解耦的——同样的代码在 WebGL 后端跑和在 WASM 后端跑走的完全不是同一条执行路径。2.2 延迟执行理解了这个模型才算真正入门TensorFlow.js 一个非常反直觉的地方是你调用tf.matMul(a, b)时计算不一定立刻发生。WebGL 后端会把算子包装成一个个待执行的指令放入一个执行队列等真正需要出结果的时候再统一触发。这个设计很像餐厅的后厨你点单时菜不是马上进锅而是先交给配菜间由后厨统一调度、保证灶台利用率最高。TensorFlow.js 的 WebGL 后端之所以要搞延迟执行核心目的只有一个——减少 CPU 与 GPU 之间的数据拷贝。每次 GPU 算完一个中间结果如果立刻读回 CPU就是一个昂贵的同步操作。延迟执行让多个算子可以连续在 GPU 上完成只在最后一步把结果读回。这个机制的名称在官方文档里叫lazy execution它也是性能优化的第一课提示如果你在推理链路中间调用了.dataSync()或.arraySync()会强制触发一次 GPU 到 CPU 的同步回读。这个操作会打碎延迟执行的优化管线性能直接掉一个量级。2.3 张量生命周期前端内存管理中反直觉的一环在正常的 JavaScript 开发里你不需要手动释放变量V8 有垃圾回收。但在 TensorFlow.js 里这一套完全失效——张量底层的数据可能存在 GPU 纹理里而 GPU 纹理资源并不归 JS 引擎管V8 的 GC 根本不知道这块 GPU 内存需要回收。所以 TensorFlow.js 提供了两把手动管理的内存工具tf.tidy()在一段函数体里创建的所有张量函数执行完自动释放。这是日常推理最推荐的写法。tf.dispose()手动释放某个张量及其底层数据。我在项目里定的规矩是predict 循环里的每一次推理必须用tf.tidy()包住任何人从模型 predict 拿到的输出张量用完后立刻 dispose不让中间张量活着跨越下一次循环。tf.memory()是排查内存问题的入口它会告诉你当前有多少个张量、占用多少字节。如果一个循环跑 20 次后numTensors线性上涨不用看代码也能断定有张量没释放。你还可以在predict前后各调一次tf.memory()对比张量数量差就能找出泄露发生在哪一层。3. 算力调度WebGL、WebGPU、WASM 三条执行路径的取舍浏览器端的算力调度和服务器完全不同。服务器上的 GPU 是透明的、你的进程独占或者被 CUDA 管理、显存也稳定浏览器端则要跟页面渲染争 GPU同一台机器开几十个标签页你的推理只是其中之一。3.1 TensorFlow.js 如何决定用哪个后端TensorFlow.js 内置四类后端WebGL、WebGPU、WASM、CPU纯 JS 实现。初始化时它会按注册顺序依次寻找可用后端默认优先级大致是 WebGPU WebGL WASM CPU同时受浏览器 API 支持度、特性检测结果和设备硬件限制影响。你可以在代码里强制指定import * as tf from tensorflow/tfjs; // 强制走 WebGL await tf.setBackend(webgl); // 或强制走 WASM常用于无 GPU 或 GPU 驱动不稳定的设备 await tf.setBackend(wasm);生产环境我不会让 SDK 自己选。原因很简单自动选择只考虑可用不考虑性能和稳定性。在某些 Android WebView 里WebGL 上下文虽然创建成功但浮点纹理支持不完整跑出来的结果直接不对——纯靠自动选择就是赌博。3.2 WebGL 的算力调度内幕纹理即显存WebGL 后端是整个 TensorFlow.js 里最复杂、也最能体现算力调度的部分。它的数据存储方式是纹理即显存每个张量被编码成一张或多张纹理算子在 GPU 上以片元着色器程序执行。这带来几个隐藏影响数据是异步的除非你主动调用 readback否则中间结果一直躺在 GPU 上不占 JS 堆内存你在tf.memory()里看到的字节数不是全部事实。纹理尺寸有硬上限WebGL 的MAX_TEXTURE_SIZE决定了单张纹理的最大尺寸常见设备是 4096 或 2048。超大张量会被内部切块执行这会让耗时出现非线性增长你很难从模型参数直接估算出推理时间。着色器编译开销不能被忽略同一个算子第一次执行时WebGL 需要编译着色器这个过程可能耗时上百毫秒。生产环境必须做预热推理把模型跑一遍热身后续推理才能真正体现正常性能。3.3 WASM没有 GPU 时的兜底选择WASM 后端适合两条路线一是用户设备没有可用的 WebGL 2二是你在桌面端更看重 CPU 的稳定性能。TensorFlow.js 的 WASM 后端通过 XNNPACK 优化算子并支持多线程需要 COOP/COEP 响应头和跨域隔离在纯 CPU 场景下通常比纯 JS 的 CPU 后端快 5 到 10 倍。我经常用的判定方式是移动端中低端设备上WebGL 经常因为驱动问题表现不稳定而 WASM 虽然慢但结果确定性高。遇到线上反馈同一版本页面在华为手机上识别结果不对我第一反应就是切 WASM 对比同一输入输出十有八九是浮点纹理精度问题。3.4 WebGPU 的下一步真正意义上的浏览器端计算统一WebGPU 是目前最接近原生计算能力的浏览器端方案。它用 compute shader 而不是片元着色器做计算数据存储用 buffer 而不是纹理理论上可以处理更大规模的任务也不需要像 WebGL 那样把张量塞进各种纹理格式里。TensorFlow.js 的 WebGPU 后端实际跑起来在支持的浏览器里性能比 WebGL 稳定不少尤其在大矩阵乘法和卷积场景下提升明显。但 WebGPU 的支持度还处于推进期桌面端浏览器覆盖在逐步到位移动端和 WebView 的覆盖差异较大。生产环境里我的建议是把它作为有提升空间的可选项而不是默认选项。4. 生产级避坑实录跑通 Demo 只是前戏压测才见真章说句实话TensorFlow.js 的官方示例都很漂亮但如果照着示例直接上生产你会撞得头破血流。下面这些坑是我们团队一个个踩出来、又一个个填平的。4.1 内存泄漏的五个典型模式浏览器端的 GPU 内存泄漏通常不是忘了 dispose 一个张量这种简单问题而是藏在循环、回调、异步链路里的结构性泄漏。我总结过五个典型模式在requestAnimationFrame或定时器回调里直接创建新张量没有tf.tidy()包裹。拿到predict的输出后没有 dispose而每次推理输出张量都被外部引用。.dataSync()拿到的是 GPU 回读的 JS 数组副本原张量不释放副本又占内存双份泄漏。使用tf.image.resizeBilinear等辅助函数产生的中间张量在链式调用里没有收回。动态创建卸载模型但没有调用model.dispose()导致整条计算图留在后端。排查手段不复杂const before tf.memory().numTensors; for (let i 0; i 50; i) { const output model.predict(input); // 忘写 output.dispose() 的话numTensors 会持续涨 } const after tf.memory().numTensors; console.log(after - before); // 差值是泄漏量4.2 GPU 上下文丢失与设备重启WebGL 最隐蔽的杀手前端深度学习最隐蔽的坑是WebGL context lost。用户切到后台、手机锁屏、或者 GPU 进程被系统回收都可能触发上下文丢失。恢复后你之前创建的所有纹理、编译好的着色器全部失效但 JavaScript 侧的张量对象可能还活着。处理方式是在初始化时监听webglcontextlost事件触发后清理整个 TensorFlow.js 后端实例再重建canvas.addEventListener(webglcontextlost, async (e) { e.preventDefault(); await tf.disposeVariables(); // 销毁后端实例 const backend tf.backend(); backend.dispose(); // 重新初始化 await tf.setBackend(webgl); });如果不做这个处理最典型的现象是长时间挂着页面的用户某次触发识别时页面直接白屏或者卡死 10 秒然后报一个texture allocation failed的错。4.3 主线程卡成 PPTWeb Worker 迁移的正确姿势浏览器端推理最大的性能杀手不是计算本身而是主线程被大量同步操作阻塞。模型加载要解析权重推理要走 GPU 管线这两个操作如果都在主线程做用户会明显感到页面掉帧。在生产项目里我们最终把所有计算逻辑迁到了 Web Worker核心模式是这样// worker.js importScripts(tf.min.js); self.onmessage async (e) { const { imageData } e.data; const input tf.browser.fromPixels(imageData); const output model.predict(input.expandDims(0)); const result output.dataSync(); self.postMessage({ result: Array.from(result) }); };主线程用postMessage把图片数据传进去计算全部在 Worker 里完成主线程只接收结果。这样推理耗时再长页面也不会出现未响应的状态。一个关键细节如果推理结果还需要继续在 GPU 上操作比如渲染画布叠加靠 postMessage 传递 ArrayBuffer 会多一次拷贝。这时候可以用OffscreenCanvas在 Worker 里直接渲染把最终渲染结果一个 Bitmap 转移回主线程省掉中间环节。4.4 模型转换链路从 TensorFlow 到浏览器端不是导入导出那么简单服务端训练好的模型进浏览器不是直接tf.loadLayersModel(model.json)就能用的。TF2 的 SavedModel 需要走tensorflowjs_converter转成 TF.js 格式pip install tensorflowjs # SavedModel - GraphModel tensorflowjs_converter \ --input_formattf_saved_model \ --output_formattfjs_graph_model \ --signature_nameserving_default \ ./saved_model \ ./web_model这里我要提醒几个容易踩的雷训练图里别带 optimizer 和 metrics转换前保存模型时用model.save_weights还是model.save要想清楚。完整模型里的训练状态会让转换器生成一堆无用算子最后模型体积虚胖。动态 shape 是坑源模型如果包含None维度比如动态 batch转换出来的 GraphModel 在浏览器端解析时可能会插入额外的 resize 算子导致某些设备上算出来错位。生产环境尽量把模型固定 shape。归一化参数别漏训练时用的是[0,1]归一化转换后不会自动帮你做。浏览器端必须手动同步偏置和缩放的数值否则输出全错。4.5 浏览器兼容性矩阵别等到线上用户截图投诉才发现我在兼容性上吃过最大的亏是 iOS Safari 的浮点纹理支持。同一套 WebGL 后端在 Chrome 里跑得好好的到 iPhone 上就出现 NaN 或者结果整体偏色。排查下来是设备不支持真正的浮点纹理TensorFlow.js 内部做了降级导致精度受损。给团队用的兼容性矩阵是这样的环境首选后端兜底后端注意事项桌面 Chrome/Edge 最新版WebGL 2 / WebGPUWASM关注 context lost 恢复桌面 FirefoxWebGL 2WASM同一输入与 Chrome 对照验证iOS Safari 主流版本WASMWebGL 1 兼容路径浮点纹理精度风险必须抽测Android Chrome 最新版WebGL 2WASM中低端设备驱动不稳定Android WebView先探测再决定WASM版本碎片化严重统一按旧核对待每个发行环境上线前我都强制跑一遍同一输入的对照组测试输出结果误差超过1e-4就切后端。5. 性能调优实录一个 CNN 分类器从 450ms 到 78ms讲完避坑聊点正向的。拿我们生产环境里的一个图像分类模型举例输入 224x224 的三通道图模型是个轻量 CNN初始推理耗时在主力测试机上约 450ms。第一版代码跟官方案例几乎没有区别压测一上就露馅。完整调优链路记录如下。5.1 第一步定位瓶颈禁止拍脑袋优化我先用tf.profile跑了一次完整推理把每个算子的耗时拉出来const profile tf.profile(() { const output model.predict(input); output.dataSync(); }); console.log(profile.kernelMs); // 每个内核耗时 console.log(profile.totalBytes);结果很直观conv2d和matMul各占 10% 左右最离谱的是dataSync()占了几乎 60% 的时间——说明模型推理本来可以很快但我们在中间或输出环节反复把 GPU 数据读回了 CPU。5.2 第二步消除无谓 readback整个链路留在 GPU优化第一步就是删掉推理中间环节的所有.dataSync()和.arraySync()。如果只需要分类结果的 top-1 索引不要让 GPU 把完整概率数组读回 CPU而是在 GPU 上直接做argMax把最终结果拿回来。同时把输入侧的前处理缩放、转 tensor、归一化全部改成 TensorFlow.js 算子表达而不要先在 Canvas 上循环改像素再fromPixels。Canvas 像素循环是纯 CPU 计算挪到 GPU 算子后整条推理链就是GPU 进、GPU 出。这一步跑完推理时间从 450ms 降到了 180ms 左右。5.3 第三步预热、量化与后端三连然后做了三件事。一是预热机制页面加载完成、用户还没触发识别前提前跑一小段随机输入把 WebGL 后端的着色器编译成本支付掉。这一步对首次推理延迟影响极大第一次推理从 180ms 降到 130ms。二是把模型从 float32 量化到 float16。TensorFlow.js 的 WebGL 后端可以在纹理层面用半精度存储模型体积直接砍半推理速度还能再快一些。量化后推理约 100ms。三是针对 WebGL 还是 WASM 做了 AB 测试。测试机上 WebGL 更快但一旦 GPU 驱动不稳定耗时反而飙升。最终我们采用运行时探测优先 WebGL若 context lost 或多次推理标准差超阈值自动切 WASM 兜底。最终整条推理链路在主力测试机上稳定在 78ms 左右识别一台机器的耗时完全在用户交互感知阈值内。整个过程里最有价值的经验是调优不是堆配置而是靠 profile 数据一段段指哪打哪。6. 关于浏览器端深度学习我最后的几点个人经验如果有读者准备在真实项目里用 TensorFlow.js我想给你三个我自己沉淀下来的经验。第一不要迷信任何一个后端的宣传性能一切都以你的目标设备矩阵为准。同一款手机不同系统版本上的 WebGL 驱动差异可能让推理时间相差两倍。线上压测必须覆盖真实设备而不是只在 Chrome 开发者工具里跑。第二内存管理不是优化项而是正确性项。浏览器端的 GPU 内存是硬性资源泄漏不会像服务端那样有明确的 OOM 报错它只会在某个随机时刻变成卡顿、白屏或者上下文丢失。这类问题线上极难排查宁可在一开始就强制约束团队写tf.tidy()和dispose()。第三把模型能不能在浏览器端跑当成一个产品决策而不是技术决策。如果你的核心用户是手持旧 Android 机、网络环境复杂的人群哪怕模型再轻量也要预留服务端回退的通道。浏览器端深度学习最大的价值是给了产品一个新的选择面而不是把服务端那条路堵掉。下次如果还有人跟你说浏览器端跑深度学习不靠谱你至少可以给他看一个线上跑着的生产案例。
企业数字化 ERP 产品动态
相关推荐
TFLite内存规划器详解:复用张量按生命周期压峰值内存 做端侧推理的兄弟应该都体会过这种场景:模型导出很顺利,板子上一跑就 OOM;或者只是把输入分辨率从 320 提到 416,内存直接翻倍。遇到这种问题,大多数时候不是算力不够,而是中间张量被无谓地“一人占一个坑”… · 2026/11/3 13:28:18
不买转接盒:Android车机静态工程评测与接收端实现 1. 拆解这个标题背后的真实需求 第一次看到“DiPlay 静态工程评测”这个说法,很多人会以为又是一个车机投屏的教程。但把标题拆开看,核心其实落在三个词上: 静态工程评测 、 不买转接盒 、 Android 车机扮演接收端 。这三个词组合在一起… · 2026/11/3 13:28:18
只检查放行项,还不知道漏掉了什么 2026-11-02更新:0.2.0新增有限输入集合的precision和recall逻辑界限。下面保留0.1.0原始发布记录,新版方法与限制见末节。
0.1.0:原始发布记录
我发布了 Rejected Check Audit,一个用来检查多阶段筛选分母的离线工具。它保留前置… · 2026/11/3 13:28:18
SQLAlchemy核心功能与Python数据库开发实践 1. 为什么选择SQLAlchemy作为Python ORM 作为一名长期使用Python进行数据库开发的工程师,我几乎尝试过所有主流的Python ORM工具。SQLAlchemy之所以能成为我的首选,核心在于它独特的"双模式"设计理念: 核心层(SQLAlchemy Core) &… · 2026/11/3 16:39:01
从Vibe Coding到规范驱动开发:AI编程如何从碰运气走向可复现 1. 从“感觉对了”到“有据可依”:为什么我开始警惕 Vibe Coding前阵子跟几个做独立开发的朋友吃饭,聊到 AI 编程这个话题,桌上几乎所有人都在用 Cursor、Windsurf 或者 VS Code Copilot 这类工具。有个哥们儿说他现在写代码基本就是“跟着感… · 2026/11/3 16:39:01
飞书机器人免@自动回复技术实现与优化 1. 项目背景与核心价值 最近在技术社区看到不少开发者讨论飞书机器人自动回复的配置问题,特别是如何实现免触发回复的场景。这让我想起去年在OpenClaw项目中折腾飞书机器人的经历——当时为了给团队搭建一个智能问答系统,需要让机器人能自动识别群消息关… · 2026/11/3 16:39:01
OpenSpec规格驱动开发:让AI编码助手告别返工 规格驱动开发这个词最近在团队里被反复提起,起因很简单:我们用 AI 编码助手写代码,写得越快,返工越多。需求在脑子里,提示词随手一写,AI 生成一大坨代码,跑起来看着像那么回事,一对接… · 2026/11/3 16:39:01
变容二极管调频电路课程设计:从指标拆解到电路交付的完整方案 简介:这份课程设计文档面向电子信息、通信工程等专业学生及电子电路初学者,围绕变容二极管调频电路设计展开,帮助读者理解直接调频方案的原理与实现路径。内容涵盖方案选择、FM调制原理、变容二极管直接调频原理分析,以及LC电容三… · 2026/11/3 16:39:01
美业小店数字化转型:轻量化解决方案与应用实践 1. 美业小店数字化转型的必然趋势 2026年的美业市场已经发生了翻天覆地的变化。作为一名在美发行业摸爬滚打8年的从业者,我亲眼见证了从"流量为王"到"效率制胜"的转变。现在的消费者不仅对服务质量要求更高,对消费体验的便捷性也愈发… · 2026/11/3 16:37:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46