☰
Manifest V3下浏览器扩展端侧AI推理实战:WebGPU与WASM性能优化
2026/10/8 10:16:08 网站建设 项目流程

浏览器扩展这个赛道,这两年因为Manifest V3的强制迁移,正在经历一次彻底的重构。以前大家写扩展,逻辑很简单:内容脚本抓DOM,后台脚本发请求,完事。但现在情况变了——越来越多的场景要求数据不出端,比如文档摘要、图片处理、语音转写、实时翻译,这些需求如果还走云端API,延迟、成本、隐私三座大山压下来,产品根本跑不通。于是端侧AI推理被推到了前台,而浏览器扩展恰好是承载它的一个天然容器:用户装完即用,不需要额外装客户端,模型跟着扩展走,推理在本地跑。

但问题也来了。浏览器扩展的运行环境跟原生应用、Node服务完全不是一回事。Service Worker有生命周期,随时可能被干掉;WebGPU虽然能用,但不同浏览器的支持程度参差不齐;ONNX Runtime Web的WASM后端在扩展的CSP策略下经常加载失败。这些坑我在过去一年里几乎踩了个遍,有些是文档里压根没写的,有些是官方示例跑通了但实际项目一上就崩的。这篇文章就把这套东西从头到尾拆一遍,讲清楚在Manifest V3的约束下,怎么把端侧AI推理系统稳稳当当地跑起来。

1. 为什么端侧推理在扩展里是个真需求

1.1 云端推理的三笔账算不过来

先说最直接的动机。假设你做一个网页内容摘要扩展,用户点一下按钮,把当前页面正文提取出来,生成一段200字的摘要。如果走云端,流程是:内容脚本抓正文→发给后台→后台调API→等返回→渲染结果。这里面有几个隐性成本。

第一是延迟。一次网络往返,加上服务端排队和推理时间,保守估计1.5到3秒。用户点完按钮盯着屏幕等三秒,体验已经很差了。如果遇到网络抖动,五秒以上也正常。端侧推理用WebGPU跑一个量化后的小模型,同样的摘要任务,首次加载模型可能要两三秒,但后续每次推理可以压到200到500毫秒,这个差距是数量级的。

第二是成本。云端API按token计费,一个日活一万的扩展,每人每天用五次,每次输入输出加起来算1000 token,一天就是五千万token。这个量级下,每个月的API账单足够养活一个小团队。端侧推理的边际成本是零,模型下载一次,后面随便跑。

第三是隐私。用户浏览的网页内容,很多是工作文档、内部系统、个人笔记。把这些内容发到第三方服务器,本身就是个合规风险。端侧推理数据不出设备,这一条在B端场景里几乎是硬性要求。

1.2 扩展作为端侧推理容器的独特优势

有人会问,端侧推理为什么不做成桌面应用或者原生插件?答案是分发成本。浏览器扩展的分发链路极短,用户从商店点一下安装,几秒钟就能用上。桌面应用要下载安装包、过安全软件、处理更新,每一步都在流失用户。而且扩展天然跟浏览器上下文绑定,能直接访问当前页面、标签页、书签、历史记录,这些能力在端侧AI场景里非常值钱。

另一个优势是跨平台。同一套扩展代码,在Windows、macOS、Linux、ChromeOS上都能跑,底层推理后端由浏览器统一抽象。你不需要为每个平台单独编译模型运行时,WebGPU和WASM帮你屏蔽了大部分差异。当然这个"屏蔽"是有代价的,后面会详细讲。

1.3 哪些场景适合端侧,哪些不适合

不是所有AI功能都适合塞进扩展里。我总结了一个简单的判断标准:

场景特征适合端侧适合云端
模型参数量小于1B,量化后小于500MB大于1B
延迟要求小于1秒可接受2秒以上
数据敏感度高,不能出端低,可脱敏
调用频率高频,每次交互都用低频,偶尔触发
任务复杂度分类、摘要、翻译、OCR复杂推理、长文本生成

举个具体例子。划词翻译这个功能,用端侧跑一个量化后的翻译模型,虽然质量比大模型差一点,但胜在即时、免费、离线可用。而如果是让AI帮你分析一份50页的PDF并生成报告,这种任务端侧跑不动,还是得走云端。

2. Manifest V3给端侧推理套上的三道枷锁

2.1 Service Worker的短命特性与模型加载的冲突

Manifest V3最大的变化是用Service Worker替代了原来的后台页面。Service Worker是个事件驱动的环境,没有DOM,而且会在空闲时被浏览器回收。官方文档说空闲30秒后可能被终止,实际测试下来,不同浏览器、不同负载下这个时间从十几秒到几分钟不等。

这对端侧推理是致命的。你想想,加载一个几百MB的ONNX模型,从磁盘读取、解压、初始化推理会话,这个过程本身就要好几秒。如果Service Worker在模型加载到一半的时候被回收了,下次事件来了又得从头来。更麻烦的是,WebGPU的device上下文在Service Worker重启后会丢失,所有GPU资源都要重新申请。

我最初的方案是把模型加载放在Service Worker的全局作用域里,用一个Promise缓存。结果发现,只要用户超过半分钟没操作,Service Worker一挂,缓存全没了。后来改成用chrome.storage存模型文件的ArrayBuffer,每次Service Worker启动时重新创建推理会话。这个方案能跑,但每次冷启动都要重新初始化,首次推理延迟很难看。

2.2 CSP策略对WASM和WebGPU的加载限制

Manifest V3对扩展页面的内容安全策略做了严格限制。默认情况下,扩展的CSP是:

script-src 'self'; object-src 'self'

这意味着你不能从远程加载任何脚本,包括WASM文件。ONNX Runtime Web的WASM后端需要加载一个.wasm文件,如果你把这个文件放在扩展包里,用相对路径引用,是可以的。但如果你图省事想从CDN加载,直接就被CSP拦了。

WebGPU的情况稍微好一点,它不需要加载外部脚本,但需要确保扩展的manifest里声明了正确的权限。另外,在Service Worker里使用WebGPU,需要在manifest的background字段里指定type为module,否则import语句会报错。

还有一个容易忽略的点:如果你用了WebAssembly的SharedArrayBuffer来做多线程推理,需要设置跨域隔离头。扩展页面默认是不满足跨域隔离条件的,需要在manifest里配置cross_origin_embedder_policy和cross_origin_opener_policy。这两个配置在MV3里是通过manifest的content_security_policy字段间接控制的,写起来比较绕。

2.3 扩展包体积与模型分发的矛盾

Chrome Web Store对扩展包体积有硬性限制,单个包不能超过2GB,但实际上传超过100MB审核就会变慢,用户安装时也会犹豫。而一个可用的端侧模型,量化后通常也在几十MB到几百MB之间。

这就产生了一个矛盾:模型太大,包体积超标;模型太小,效果不行。我的做法是把模型拆成两部分:一个基础的小模型放在扩展包里,保证安装后核心功能可用;一个增强的大模型放在远程,用户首次使用时按需下载,存到IndexedDB里。这样既控制了初始包体积,又给了用户选择权。

但这里有个坑:从远程下载模型文件,MV3的CSP允许吗?答案是允许的,因为模型文件不是脚本,是二进制数据。你可以用fetch下载ArrayBuffer,然后存到IndexedDB。但要注意,下载的域名需要在manifest的host_permissions里声明,否则fetch会被拦截。

3. 推理后端的选型:WebGPU、WASM还是WebNN

3.1 三种后端的实测性能对比

ONNX Runtime Web支持多种执行后端,最常用的是WASM和WebGPU。WebNN是新兴的标准,目前支持度还很低。我在同一台机器上(M1 MacBook Pro,Chrome 120)跑了一个量化后的MiniLM模型,输入长度128,对比结果如下:

后端首次推理耗时稳定后单次推理内存占用兼容性
WASM (单线程)800ms120ms180MB全平台
WASM (多线程)600ms45ms220MB需跨域隔离
WebGPU1500ms18ms350MBChrome/Edge 113+
WebNN未跑通--实验性

这个数据很说明问题。WebGPU的首次推理最慢,因为要编译shader、申请GPU资源,但一旦跑起来,单次推理速度是WASM的6倍以上。所以如果你的场景是高频调用,WebGPU是唯一选择;如果只是偶尔用一下,WASM的多线程版本反而更划算,因为启动快、内存小。

3.2 WebGPU在扩展里的初始化陷阱

WebGPU在普通网页里用起来很简单,navigator.gpu.requestAdapter()然后requestDevice()就完事了。但在扩展的Service Worker里,这套流程有几个坑。

第一个坑是adapter的获取可能返回null。在Service Worker环境下,某些浏览器对GPU访问有限制,requestAdapter可能直接返回null。这时候你需要fallback到WASM后端。我的做法是写一个探测函数,在Service Worker启动时先试WebGPU,失败就标记为WASM模式。

第二个坑是device lost的处理。WebGPU的device可能会因为驱动更新、系统休眠等原因丢失。一旦丢失,所有已创建的buffer和pipeline都失效。在扩展里,这个问题更严重,因为Service Worker本身就可能被回收,device lost和worker重启叠加在一起,排查起来很痛苦。我的方案是监听device.lost事件,一旦触发就重建整个推理会话,同时给用户一个"正在重新初始化"的提示。

第三个坑是shader编译的耗时。WebGPU的推理会话初始化时,ONNX Runtime会把模型的计算图编译成WGSL shader。这个过程在首次运行时可能耗时一两秒,如果放在Service Worker里同步执行,会阻塞其他事件处理。我后来改成在offscreen document里做初始化,Service Worker只负责转发消息,这样不会阻塞主线程。

3.3 WASM多线程的跨域隔离配置

WASM多线程依赖SharedArrayBuffer,而SharedArrayBuffer要求页面处于跨域隔离状态。在扩展里配置这个,需要在manifest.json里加两个字段:

{ "cross_origin_embedder_policy": { "value": "require-corp" }, "cross_origin_opener_policy": { "value": "same-origin" } }

加了这两个之后,扩展页面就满足跨域隔离条件了,SharedArrayBuffer可以正常使用。但要注意,这会影响扩展内所有资源的加载策略,如果扩展里有引用外部图片或字体,可能会被COEP拦截。解决办法是给这些资源加上Cross-Origin-Resource-Policy头,或者干脆把资源打包进扩展。

另外,WASM多线程的线程数不是越多越好。ONNX Runtime Web默认会用navigator.hardwareConcurrency作为线程数,但在扩展环境里,这个值可能不准。我实测下来,4到8个线程是比较合理的区间,再多的话线程调度开销会抵消并行收益。

4. 模型工程:从训练产物到扩展可用的资产

4.1 量化策略的选择与精度损失评估

端侧模型的第一个工程决策就是量化。FP32的模型太大,FP16在WebGPU上支持还行但在WASM上没优势,INT8是性价比最高的选择。ONNX Runtime提供了动态量化和静态量化两种方式。

动态量化不需要校准数据集,直接对权重做INT8量化,激活值在推理时动态计算scale。这种方式简单,但精度损失相对大一些。静态量化需要一批校准数据,提前算好激活值的scale,精度更好,但流程麻烦。

我的经验是,对于Transformer类模型,动态量化后精度损失通常在1%到3%之间,具体取决于模型和任务。如果任务对精度敏感(比如涉及数值计算),建议做静态量化,或者对敏感层保持FP16。ONNX Runtime的量化工具支持混合精度,可以把某些层排除在量化之外。

还有一个细节:量化后的模型在WebGPU上的表现和在WASM上不一样。WebGPU对INT8的支持是通过模拟实现的,实际计算时可能转成FP16或FP32。所以如果你主要用WebGPU,FP16量化可能是更好的选择,模型体积比INT8大一点,但省去了转换开销。

4.2 模型分片加载与IndexedDB缓存策略

前面提到,大模型不适合直接打包进扩展。我的方案是把模型切成多个分片,每个分片几MB,用户首次使用时按需下载。分片的好处是可以并行下载,而且如果某个分片下载失败,只需要重试那一个。

下载完成后,模型数据存到IndexedDB。IndexedDB在扩展环境里是持久化的,除非用户清除扩展数据,否则不会丢。但IndexedDB的读写速度比内存慢很多,所以每次Service Worker启动时,需要把模型从IndexedDB读到内存里,再创建推理会话。这个读取过程对于几百MB的模型来说,可能要一两秒。

为了优化这个体验,我做了一个预热机制:在用户安装扩展后,后台静默下载模型并存入IndexedDB。等用户第一次使用功能时,直接从IndexedDB读,省去了下载时间。另外,如果模型不大(小于50MB),可以考虑存在chrome.storage.local里,读取速度比IndexedDB快,但有容量限制。

4.3 模型版本管理与增量更新

模型不是一成不变的,你可能需要更新模型来修复bug或提升效果。如果每次更新都让用户重新下载整个模型,体验很差。我的做法是给模型文件加版本号,更新时只下载变化的文件。

具体实现上,我把模型拆成配置文件(描述模型结构、输入输出、预处理参数)和权重文件。配置文件很小,每次更新都重新下载;权重文件按层或按块分片,用哈希值做版本比对,只下载变化的片。这样一次小更新可能只需要下载几MB,而不是几百MB。

版本管理还有一个坑:如果用户正在使用旧版本模型,后台悄悄更新了模型文件,可能会导致推理结果不一致。我的方案是双缓冲,新模型下载到临时目录,等当前推理会话结束后再切换。切换时给用户一个提示,避免困惑。

5. 扩展架构设计:Service Worker、Offscreen与内容脚本的协作

5.1 为什么推理要放在Offscreen Document

Service Worker里跑推理有两个问题:一是生命周期短,二是没有DOM,某些库可能依赖DOM API。Offscreen Document是MV3引入的一个隐藏页面,生命周期跟扩展绑定,不会因为空闲被回收,而且有完整的DOM环境。

我的架构是:Service Worker负责事件分发和状态管理,Offscreen Document负责模型加载和推理执行,内容脚本负责页面交互和数据采集。三者之间通过chrome.runtime.sendMessage通信。

这个架构的好处是职责清晰。Service Worker被回收了没关系,Offscreen Document还在,模型不用重新加载。Offscreen Document里可以用完整的Web API,包括WebGPU、WebAssembly、IndexedDB,不受Service Worker的限制。

但Offscreen Document也有代价。它本质上是一个隐藏的网页,会占用内存。如果模型很大,Offscreen Document的内存占用可能达到几百MB。对于内存紧张的设备,需要做降级处理,比如在低内存设备上只用WASM单线程,或者干脆走云端。

5.2 消息传递的序列化开销与优化

Service Worker、Offscreen Document、内容脚本之间的通信,底层是结构化克隆。如果你传递的是大对象,比如图片的ImageData或者模型的中间结果,序列化和反序列化的开销不可忽略。

我实测过,传递一个1MB的ArrayBuffer,序列化加传输加反序列化,大概要5到10毫秒。如果推理结果本身就是几MB的向量,这个开销会累积。优化方案有几个:一是用Transferable Objects,把ArrayBuffer的所有权直接转移,避免拷贝;二是把大结果存到IndexedDB,只传一个引用ID;三是用MessageChannel建立专用通道,减少消息路由开销。

Transferable Objects在扩展的sendMessage里支持有限,不是所有浏览器都实现了。我目前用的是IndexedDB方案,推理结果先存到IndexedDB,然后发一个ID给内容脚本,内容脚本再根据ID去读。这样虽然多了一次读写,但避免了消息通道的阻塞。

5.3 推理任务的队列管理与优先级调度

用户可能同时触发多个推理任务,比如一边划词翻译一边做页面摘要。如果这些任务都往Offscreen Document里塞,会互相争抢GPU资源,导致所有任务都变慢。

我的做法是在Service Worker里维护一个任务队列,按优先级排序。交互式任务(比如划词翻译)优先级最高,后台任务(比如页面摘要)优先级低。同一时刻只执行一个推理任务,其他的排队。如果队列太长,直接拒绝低优先级任务,给用户一个提示。

队列管理还有一个细节:任务超时。如果某个推理任务卡住了(比如模型加载失败或者GPU hang),不能让队列一直堵着。我给每个任务设了超时时间,交互式任务10秒,后台任务30秒,超时就取消并释放资源。

6. 性能调优与踩坑实录

6.1 首次推理延迟的拆解与优化

首次推理延迟是端侧AI体验的最大杀手。我把这个延迟拆成几部分:模型加载(从磁盘或IndexedDB读)、会话创建(ONNX Runtime初始化)、shader编译(WebGPU)、预热推理。

模型加载这块,如果用IndexedDB,读取速度大概在100MB/s左右,一个200MB的模型要2秒。优化方法是把模型文件用gzip压缩存储,读取时解压,虽然多了CPU开销,但IO时间减少,总体更快。另外,可以用Streams API边读边解压,减少内存峰值。

会话创建和shader编译是WebGPU特有的开销,大概1到2秒。这部分很难完全消除,但可以提前做。我的方案是在Offscreen Document创建后立即开始初始化,不等用户触发。这样等用户真正用时,模型已经准备好了。

预热推理是指用一个小输入跑一次推理,触发所有懒加载的资源和shader编译。这一步能把后续推理的延迟降低30%以上。预热可以在初始化完成后立即做,用户无感知。

6.2 内存泄漏的常见来源与排查方法

端侧推理是内存大户,稍不注意就泄漏。我遇到过的泄漏来源有几个:

一是ONNX Runtime的session没有正确释放。每次创建InferenceSession都会分配GPU和CPU内存,如果旧的session不释放,内存会持续增长。解决方法是显式调用session.release(),并且在创建新session前确保旧session已经释放。

二是WebGPU的buffer和texture没有销毁。WebGPU的资源需要手动destroy,否则即使JavaScript对象被回收,GPU内存也不会释放。我写了一个资源管理器,统一跟踪所有创建的GPU资源,在会话结束时批量销毁。

三是事件监听器没有移除。在Offscreen Document里,如果给window或document加了监听器,页面关闭时不会自动移除。虽然Offscreen Document生命周期跟扩展绑定,但频繁创建销毁会导致监听器累积。

排查内存泄漏,Chrome DevTools的Memory面板是主要工具。可以拍堆快照,对比不同时间点的对象数量。对于GPU内存,可以用chrome://gpu页面查看,或者用WebGPU的API查询。

6.3 不同浏览器下的兼容性差异

Chrome和Edge对WebGPU的支持最好,版本113以上就默认开启。Firefox从版本141开始支持WebGPU,但在扩展环境下的表现还不稳定,我测试时遇到过device创建失败的情况。Safari从版本18开始支持WebGPU,但只限于macOS Sonoma以上,而且扩展环境下的支持更晚。

WASM的兼容性最好,所有现代浏览器都支持。但WASM多线程需要跨域隔离,Safari对跨域隔离的支持比较晚,而且配置方式跟Chrome不一样。

我的兼容性策略是:优先尝试WebGPU,失败则降级到WASM多线程,再失败降级到WASM单线程。降级过程对用户透明,但会在设置页面显示当前使用的后端,方便排查问题。

还有一个坑是模型格式的兼容性。ONNX Runtime Web在不同浏览器上对算子支持不一样,某些算子在WebGPU上支持但在WASM上不支持,反之亦然。我建议在模型导出时做一次算子兼容性检查,把不支持的算子替换掉或者用自定义实现。

7. 从开发到上架的工程化实践

7.1 本地开发调试的完整工作流

扩展的开发调试比普通网页麻烦,因为涉及多个上下文。我的工作流是这样的:

首先,用Vite或者Webpack做构建,把TypeScript编译成JavaScript,把模型文件复制到dist目录。开发模式下开启source map,方便调试。

然后,在Chrome里加载未打包的扩展,打开Service Worker的DevTools、Offscreen Document的DevTools、内容脚本的DevTools。这三个DevTools是独立的,需要分别打开。我习惯把三个窗口并排,方便观察消息传递。

调试推理性能时,用Chrome的Performance面板录制,可以看到WebGPU的kernel执行时间。如果WASM后端,可以用console.time手动打点。

还有一个技巧:在Offscreen Document里暴露一个全局对象,把推理会话、模型、缓存都挂上去,方便在DevTools里直接查看和操作。这个对象只在开发模式下暴露,生产环境要移除。

7.2 模型文件的打包与按需加载配置

打包配置的核心是把模型文件排除在主包之外,作为独立资源。如果用Vite,可以把模型放在public目录,构建时自动复制到dist。然后在manifest的web_accessible_resources里声明这些文件,让Offscreen Document可以访问。

按需加载的配置需要指定模型的远程地址和本地缓存策略。我一般把模型放在自己的CDN上,配置好CORS头,然后在扩展里用fetch下载。下载时显示进度条,让用户知道在干什么。

如果模型文件很大,可以考虑用Range请求分片下载。这样即使网络中断,也能从断点续传。但Range请求需要服务器支持,配置起来稍微麻烦。

7.3 商店审核中容易卡住的几个点

Chrome Web Store的审核对权限很敏感。如果你的扩展申请了host_permissions,审核员会仔细看你要这些权限干什么。端侧AI扩展通常需要访问所有网站的权限,因为用户可能在任意页面触发功能。这个权限申请的理由要写清楚,最好在隐私政策里说明数据不出端。

另一个容易卡住的是远程代码。MV3明确禁止执行远程代码,包括eval和new Function。ONNX Runtime Web的某些版本可能内部用了eval,需要确认清楚。如果用了WASM,WASM是允许的,但WASM文件必须打包在扩展里,不能从远程加载。

还有一点是模型文件的版权。如果你用的模型是开源的,要在扩展的描述里注明来源和许可证。如果是自己训练的,要确保训练数据没有版权问题。

8. 几个真实场景的落地案例拆解

8.1 划词翻译:低延迟要求的极致优化

划词翻译对延迟极其敏感,用户期望是点完就出结果。我的优化方案是:模型用最小的量化版本(小于20MB),后端优先WebGPU,预热推理在扩展启动时就做。输入文本先做长度截断,超过50个字符的直接走云端,因为端侧模型处理长文本会慢。

实测下来,从用户松开鼠标到翻译结果出现,平均延迟在300毫秒左右,其中推理本身只占50毫秒,剩下的是消息传递和渲染。这个体验已经接近原生应用了。

8.2 页面摘要:长文本的分块与流式输出

页面摘要的输入是整篇网页正文,可能几千字。端侧模型的处理长度有限,需要分块。我的做法是按段落切分,每块不超过模型的最大输入长度,然后逐块推理,最后合并摘要。

流式输出是个提升体验的好办法。虽然端侧推理是一次性出结果,但可以在推理完成后,把结果按句子逐步渲染到页面上,模拟流式效果。用户看到文字一个个蹦出来,感知延迟会低很多。

8.3 图片OCR:WebGPU的纹理上传优化

图片OCR的瓶颈在图片预处理。一张1080p的图片,从ImageData转成模型需要的张量格式,涉及大量的数据搬运。用WebGPU的话,可以把图片直接上传为GPUTexture,在GPU上做resize和归一化,避免CPU和GPU之间的数据往返。

这个优化做下来,预处理时间从200毫秒降到了20毫秒。但要注意,WebGPU的纹理格式和模型期望的输入格式可能不一致,需要写shader做转换。这部分代码比较底层,调试起来要有耐心。

9. 端侧推理在扩展里的边界与未来

9.1 当前方案的性能天花板在哪里

以目前的技术条件,端侧推理在扩展里的性能天花板大概是:模型参数量1B以内,量化后500MB以内,单次推理延迟100毫秒以内(WebGPU),内存占用1GB以内。超过这个范围,要么体验崩坏,要么设备扛不住。

这个天花板不是固定的,随着WebGPU的成熟和硬件的发展,会逐步抬高。但短期内,端侧推理还是适合做轻量级任务,重任务交给云端。

9.2 模型小型化与推理加速的演进方向

模型小型化是端侧AI的核心驱动力。从BERT到DistilBERT到TinyBERT,再到现在的各种1B以下的小模型,效果越来越好,体积越来越小。量化技术也在进步,从INT8到INT4,甚至二值化网络,都在探索中。

推理加速方面,WebGPU的shader优化空间还很大。ONNX Runtime Web的WebGPU后端还在快速迭代,每次版本更新都有性能提升。另外,WebNN标准如果落地,会提供更底层的硬件加速接口,性能可能比WebGPU更好。

9.3 什么情况下应该果断放弃端侧方案

端侧不是万能的。如果遇到以下情况,我会果断放弃端侧:模型超过1B参数、任务需要多轮复杂推理、设备内存小于4GB、用户对精度要求极高。这些情况下,云端方案虽然成本高,但体验有保障。

还有一个现实问题:端侧推理的调试和维护成本很高。不同浏览器、不同设备、不同模型版本,组合起来是个巨大的测试矩阵。如果团队没有足够的工程资源,建议先从云端做起,等产品验证了再考虑端侧。

我在实际项目里的体会是,端侧推理在扩展里的价值不在于替代云端,而在于补充云端。把高频、轻量、敏感的任务放在端侧,把低频、重量、非敏感的任务放在云端,两者配合,才能做出体验和成本都说得过去的产品。这个平衡点需要根据具体场景反复调试,没有一刀切的答案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询