不用装软件跑CAD?浏览器里18MB搞定

WASMCADWeb技术开源Emscripten

数据源:HN · HN

想象一下这个场景:你是一位建筑工程师,刚落地异地机场,甲方突然来电话要你紧急确认一张DWG图纸里的某条尺寸标注。你手边只有一台借来的笔记本电脑——没有AutoCAD,没有LibreCAD,甚至没有管理员权限去安装任何软件。你打开Chrome,输入一个网址,18MB的下载完成,三秒后,一个完整的2D CAD工作台出现在浏览器标签页里。你打开图纸、测量尺寸、标注修改、导出PDF,全部在一个网页里完成。关掉标签页,什么痕迹都没留下。

这就是2026年6月29日上线的一个技术案例。开发者magik6k将LibreCAD——一个GPL-2.0协议的开源2D CAD桌面软件——完整的C++代码库,通过Emscripten编译为WebAssembly,塞进了浏览器。

这件事值得认真讨论,因为它背后藏着一个更深的命题:当浏览器能够运行完整桌面应用时,「安装」这件事还有多大意义?

反派:桌面软件的”三座大山”

在聊技术细节之前,笔者想先界定一下WebAssembly要跨越的壁垒到底是什么。

桌面CAD软件有三道墙。第一道是安装:AutoCAD动辄几个G,即便是轻量的LibreCAD,在Arch Linux上也要82.7MB磁盘空间。第二道是授权:商业CAD按年订阅,个人用户掏不起;开源方案也需要包管理器、依赖解决、编译或二进制下载。第三道是兼容性:Windows上装的软件Mac打不开,Linux版本可能少几个功能,不同版本的DWG/DXF文件打开还可能丢数据——这是CAD行业几十年的顽疾。

Web的答案是:一个URL。没有安装、没有授权、没有平台绑定。任何有浏览器的设备都能访问。但代价也摆在明面上:你需要把几十万行C++代码塞进一个沙箱里运行,而这个沙箱没有真正的文件系统、不能阻塞主线程、没有操作系统窗口、所有I/O都必须异步。

这就是LibreCAD-WASM要解决的核心矛盾。

不是重写,是平移:Emscripten + Qt工具链

先明确一个关键事实:这不是一个JavaScript重实现,不是Web原生分支,更没有服务端渲染。 浏览器里跑的就是同一份LibreCAD的C++源码,通过Emscripten交叉编译为.wasm二进制。

流程大致是:Docker容器跑Ubuntu 24.04,装上Emscripten 4.0.7和Qt 6.9.3(从源码编译),用Qt官方的qt.toolchain.cmake替代原始Emscripten工具链文件(直接用后者会让find_package(Qt6)失败),然后cmake --build。Qt的WebAssembly平台插件接管了渲染——通过WebGL绘制整个UI,浏览器事件通过平台插件传回Qt的事件循环。工具栏、停靠窗口、绘图画布、鼠标键盘输入,原样呈现。

技术规格:wasm32架构(4GB内存上限),单线程,C++17,Qt 6.9.3。WASM二进制原始大小39MB,Brotli压缩后16MB;字体和填充图案数据包30MB,压缩后仅2.2MB;JS运行时胶水代码52KB。首载总传输量约18MB——对一个完整CAD应用来说,这个数字并不大。

到这里,90%的工作是机械的。真正让这个项目值得写一篇深度文章的是剩下的10%。

嵌套对话框:当exec()撞上浏览器沙箱

桌面GUI程序有一个基本假设:你可以阻塞主线程

在Qt的世界里,QDialog::exec()是最常见的模式——弹出一个对话框,程序停在那里等你操作,关闭对话框后代码继续往下走。这在一个桌面操作系统上完全没问题:GUI线程进入一个本地事件循环,等待用户输入。

但浏览器的规则是铁律:你不能阻塞主线程。 JavaScript是单线程的,HTML渲染、用户交互、网络请求全挤在同一条线上。如果一个调用阻塞超过几毫秒,整个标签页就会”冻结”,Chrome会弹出”页面无响应”的警告。对CAD程序来说,这意味着一旦触发QDialog::exec(),程序就永远卡住了——没有事件循环来”解除”这个阻塞状态。

Emscripten的第一个方案叫Asyncify。它的原理是在编译阶段重写二进制:在可能阻塞的调用点,Asyncify把当前整个调用栈序列化、展开到浏览器事件循环、然后”休眠”。当异步操作完成,它再反序列化调用栈、恢复执行。这听起来巧妙,但它有个致命限制:同一时间只能挂起一层调用深度。 一个对话框弹出来,没问题。但在对话框里点一个下拉框——下拉框自己也要挂起——第二层嵌套就没地方去了,整个应用卡死。

对于一个CAD程序来说,这不是一个”边缘情况”。LibreCAD的首选项面板里充斥着颜色选择器和下拉菜单——从首选项对话框里打开颜色选择器,就是两重嵌套。这意味着Asyncify方案下,整个首选项面板完全不可用。

解决方案是Chrome在2024年推出的JSPI(JavaScript Promise Integration)。与Asyncify在应用层”模拟”挂起不同,JSPI是浏览器引擎原生的挂起机制——V8引擎直接支持WebAssembly栈的挂起与恢复,嵌套任意深度都没有问题。Qt 6.9可以通过-device-option QT_EMSCRIPTEN_ASYNCIFY=2来启用JSPI后端,但需要同时启用原生Wasm异常(-fwasm-exceptions),而预编译的Qt包两者都不带——所以必须从源码构建整个Qt。

故事远没结束。JSPI有一个精妙的限制:只有通过”promising function”进入的WebAssembly栈才能挂起。 Emscripten默认只把main()标记为promising。但在Web环境中,main()必须快速返回(不能像桌面那样死循环跑事件),之后每一个浏览器事件——每一次鼠标点击、每一次键盘输入——都是在一个全新的、没有promising标记的栈上执行。结果就是任何用户交互触发的挂起都会抛出trying to suspend without WebAssembly.promising错误。

magik6k的修复需要三管齐下:把Qt的DOM事件处理器注册为emscripten::async()函数(让每个事件都在promising帧内运行);同样包装Qt的定时器和posted-event回调;把main()重构为异步形态——创建应用后立即返回,由浏览器驱动事件循环。

三处改动之后,QDialog::exec()、下拉框、嵌套颜色选择器、右键菜单,任意深度的嵌套全部正常工作,一行应用层代码都没改——平台层扛下了所有脏活。

渲染性能:从5帧到可用,一个像素格式的差距

第一版能跑起来之后,帧率是个灾难——可用窗口尺寸下只有4-5fps。对CAD来说,这意味着画布缩放和平移像幻灯片。

性能分析的结论出人意料:几乎全部帧时间花在Qt的一个函数上——blend_untransformed_generic_rgb64。根本原因是Qt WebAssembly后端的像素格式选了RGBA8888(HTML Canvas需要的格式),但这个格式不在Qt的光栅快速路径里。结果每次把图层合成到窗口上,都走了一条64位逐像素的通用混合路径——每帧三次。

修复出奇简单:把后备存储切换为预乘Alpha的ARGB32——这是Qt最优化过的格式,与正在绘制的图层格式完全匹配,合成走SIMD快速路径。在刷新时做一次格式转换输出Canvas需要的RGBA字节就行。帧率大约翻了三倍,而且是引擎级别的收益,不依赖任何Canvas技巧。

这是笔者非常欣赏的一类优化:问题的根源和解决方法都出在平台层的一个不起眼的配置选择上。

文件系统、字体和持久化:在没磁盘的地方造磁盘

浏览器没有真实的文件系统。Qt的辅助API(getOpenFileContent / saveFileContent)在JSPI构建下不可靠——打开文件拿回空缓冲区,保存尝试走分块可写流但从来不触发下载。

解决方案是绕过Qt的API,直接写了一个薄薄的JavaScript桥接层:打开文件——浏览器文件选择器获取File对象,JavaScript读取字节直接写入Emscripten的内存文件系统(MEMFS),然后按路径加载;保存——序列化到MEMFS,JavaScript取出字节构造Blob,生成下载链接触发浏览器下载。

47个CAD字体文件(.lff)和填充图案被打包为30MB的数据包预加载到MEMFS。应用设置通过IndexedDB持久化——关掉标签页再打开,你的工具栏布局和首选项都在。

一个务实的妥协:“保存”等于”下载”。 浏览器不能写回你原始打开的文件,这是Web平台的安全模型决定的。用户拿到的是一个下载副本,需要手动覆盖磁盘上的原文件。

为什么只有Chrome?以及这个局限意味着什么

这个移植目前只能在Chromium系浏览器(Chrome/Edge 137+)上运行。Firefox和Safari完全不支持。HN上有评论直言:“每次看到只能在Chrome上跑的东西我都挺失望的,感觉开发者不在乎其他浏览器。”

但这次不是开发者选边站——JSPI是V8引擎的原生特性,Firefox的SpiderMonkey和Safari的JavaScriptCore还没实现。这是WebAssembly标准推进过程中的一个典型阶段:Chrome作为先行者推出实验性API,其他引擎观察、争论、最终跟进(或提出替代方案)。CanIUse的数据显示JSPI目前仅Chrome支持,Firefox和Safari的时间表尚未公开。

这件事的讽刺之处在于:WebAssembly本应是跨浏览器的开放标准,但WebAssembly之上最有价值的特性(JSPI、原生异常、线程)正在重演”只支持Chrome”的旧戏码。这是Web作为应用平台必须面对的身份危机:开放标准的理想和单引擎先行的现实之间的张力。

开源社区的另一种声音

HN上一位用户mrsssnake的评论值得全文引用(大意):

“几年前我用pacman -S librecad装了LibreCAD,82.7MB磁盘,没账号、没后台服务、完全免费。它从不弹更新提示,因为总是跟着系统一起升级。这么多年它安静地待在硬盘里,等待需要它的那一刻。我理解把桌面程序塞进浏览器的吸引力,尤其是用别人电脑的时候。但我不理解为什么有时候大家对桌面程序有这么大的排斥。”

这是一记清醒的提醒。桌面不是原罪。“浏览器里跑一切”有它的场景——临时使用、公用设备、快速预览——但对于日常使用者来说,原生应用在性能、离线能力、系统集成方面依然有明显优势。两者不是替代关系,是互补。

更有意思的是:这个过程是AI驱动的

magik6k在文章里坦承:整个过程是通过在OpenCode里向GLM-5.2模型发prompt完成的。“非常hands-off而且’容易’“——但紧接着补充,这份”容易”建立在Qt团队对WASM支持的巨大投入和整个WASM生态的成熟之上。模型在边界上挣扎:它差一点就搞不定,需要一定的人工引导。如果有原生的视觉能力(截图并理解渲染结果),模型可以自主调试更多问题。

一个非CAD用户,用AI辅助,花了几天时间,把一个存在了十几年的C++桌面CAD应用搬进了浏览器。 这个事实本身比任何技术细节都更能说明2026年WebAssembly生态的成熟度。

谦逊的收尾

笔者不是CAD用户,也不是WebAssembly内核开发者。这篇文章建立在对magik6k公开博客、HN讨论和相关技术文档的阅读之上。文中关于JSPI机制、Asyncify局限性和Qt渲染管线的分析来自原文的技术细节,笔者没有独立验证。如果你是一位Qt维护者或V8引擎工程师,可能已经发现了本文在某些实现细节上的不精确——欢迎指正。

LibreCAD-WASM的意义不在于它是不是最好的CAD(显然不是——SolveSpace在几何约束求解方面更强,FreeCAD在三维领域是另一个量级),而在于它拆解了一个完整的技术路径:一个C++/Qt桌面应用,如何一步步克服阻塞模型、渲染性能和文件系统的障碍,最终在浏览器沙箱里以可用的帧率运行。 这条路径上有Emscripten的编译魔法,有JSPI的原生挂起,有对像素格式的一行修复,也有JavaScript桥接层的务实妥协。

浏览器正在成为比操作系统更重要的应用平台。这个论断在2026年听起来依然激进,但每多一个像LibreCAD这样的案例,它就往现实靠近一步。

参考链接: