全新 Raycast 技术深度解析
原文链接:https://www.raycast.com/blog/a-technical-deep-dive-into-the-new-raycast 作者:Petr Nikolaev, Thomas Paul Mann 发布日期:2026-05-14
Raycast 跨平台重写的幕后故事,以及那些让它感觉快速、愉悦且熟悉的技术细节。
我们刚刚发布了 Raycast 2.0 的公开测试版。这是自 2020 年我们首次推出 Raycast 以来最大的版本更新,也是第一个同时支持 macOS 和 Windows 的版本。
Raycast 2.0 在 macOS 和 Windows 上
为了实现这一目标,我们从零开始重写了整个应用。全新的架构和一套混合了 TypeScript、Swift、C#、Rust、Node 和 React 的技术栈。Web 技术从一开始就是 Raycast 的一部分,支撑着扩展和 Notes 功能。在 v2 中,我们加倍投入 Web 技术,同时让应用保持一如既往的本地原生感和高速体验。
如果发布博文讲的是有什么新功能,那么这篇讲的是它是如何构建的。重写的幕后故事,我们一路上做出的决策,以及完成如此大规模的重写需要什么。困难的部分不是让 Raycast 运行起来,困难的是让它感觉对味。
起点
Raycast v1 本质上是一个使用 Swift 基于 AppKit 构建的原生 macOS 应用。我们几乎从不使用标准的 UI 组件。它们不是为我们所关注的键盘优先、高级用户工作流设计的,所以我们自己构建了所有组件。每一行列表、每一个快捷键、每一个默认行为都由我们亲手处理。我们也几乎没有使用 SwiftUI。它与 Raycast 并行发展,在性能和控制力方面从未达到我们的标准。v1 中唯一使用 SwiftUI 的地方是年度 Wrapped 功能,它与应用的其他部分完全隔离。
Raycast v1 概览
扩展生态系统则建立在完全不同的技术栈上。React、TypeScript 和 Node.js,UI 以声明式描述,由原生应用渲染。Felix 在这里详细描述了架构。为第三方开发者选择熟悉的技术栈,是应用商店如今拥有数千个扩展、覆盖人们使用的几乎每一种工具的重要原因。API 的设计也注重可移植性。扩展的代码没有任何假设它只在 macOS 上运行,这使我们去年能够将很大一部分扩展目录带到 Windows。
Raycast Notes 是我们第一次在应用中使用 WebView 来构建一个主要功能。编辑器是一个挂载在原生窗口 WebView 内的 React 应用。这是对”我们能否完全用 Web 技术构建一个功能界面,同时不破坏应用其他部分的使用体验”的一次测试。它成功了,Notes 每天都有大量 macOS 和 iOS 用户使用。
虽然 v1 的核心是原生应用,但当 Web 技术是合适的选择时,我们总是会采用它们。归根结底,人们喜欢 Raycast 是因为它给人的感觉,而不是底层用了什么技术。
为什么重写
2023 年底,我们开始考虑将 Raycast 带到 Windows。这是从第一天起就有的计划,但在早期我们只想专注于一个平台,先把体验做到极致,然后再考虑扩展。
到那时候,Raycast 已经从一个启动器成长为一个更广泛的生产力平台,拥有 AI Chat、Notes、扩展、同步、文件搜索等功能。最初为启动器构建的架构开始限制我们下一步能构建什么。编译时间逐渐变长,AppKit 越来越频繁地成为我们前进路上的障碍,而能找到深入研究原生 macOS 的人才也变得越来越难。即使没有 Windows 的计划,我们也需要重新思考大部分架构。
于是我们着手为新的 Windows 客户端和现有的 macOS 客户端确定技术栈。但首先,任何这样的项目都需要一个好的代号。我们叫它”X-Ray”,代表跨平台 Raycast(cross-platform Raycast)。
选择技术栈
我们首先研究了 Windows 上构建原生应用有什么可用方案——坦率地说,Windows 原生 UI 框架的现状远不尽如人意。微软有推出 UI 框架然后转向新框架的历史。WPF、UWP,以及现在仍然相当年轻、未经广泛实战检验的 WinUI 3。如果说在 macOS 上用 AppKit 构建一个精致的原生应用已经很有挑战性,那么在 Windows 上用 WinUI 3 做同样的事情感觉风险更大。而且,考虑到 Raycast 的大部分扩展应该在两个平台上表现一致,运行两个独立原生应用的想法也让我们不安。维护两套独立的 UI 技术栈意味着两倍的工作量,但速度并不会更快。
这很快就排除了完全原生的路线。而且,由于 Raycast 的大部分代码库是 UI,我们不能只共享后端、为每个平台构建独立的前端。这使我们转向了基于 Web 的技术栈。它默认就能跨平台渲染 UI,拥有庞大的库生态系统,优秀的开发者体验,以及比原生桌面开发大几个数量级的人才储备。Raycast 扩展已经建立在 Web 技术栈上,对我们来说一直运行良好,所以探索将其用于整个应用感觉很自然。即使我们只开发 Windows 版本,Web 也是一个合理的选择(微软将混合应用列为构建桌面应用的推荐路线之一)。它天生跨平台的特性也让我们考虑将其用于 macOS。
所以我们评估了三个选项:Electron、Tauri,以及构建我们自己的混合技术栈。
Electron 会是一个显而易见的选择。老实说,对于大多数公司来说,它可能是构建桌面应用的正确选择。它维护良好、经过实战检验,拥有庞大的生态系统。VS Code、Linear 或 Superhuman 等应用证明你可以用它构建出色的产品。苹果和微软并没有让用大型团队为它们的平台构建复杂的桌面应用变得容易,这就是为什么 Electron 填补了这个空白。我们真心认为这是一件好事。
但对于 Raycast 来说,它不是最佳选择。我们的应用与操作系统深度集成。我们依赖全局热键、剪贴板管理、无障碍 API、窗口管理、悬浮在其他应用之上且不会窃取焦点的自定义面板,等等。我们需要访问低层原生代码,以对我们的应用行为进行精细控制。即使是像内部面板的透明度这样的小细节对我们来说也非常重要。Electron 可以实现其中一些功能,但 Web 代码和原生代码之间的边界可能会很痛苦。我们也不想在 macOS 上捆绑 Chromium,因为我们可以使用系统自带的 WebKit。简单来说,我们需要确保我们掌控技术栈的每一部分,并且可以在需要时轻松回退到原生方案。Electron 不是这个需求的最佳选择。
Tauri 有类似的限制。它在原生端的控制更少,而且在当时它还足够年轻,我们不想把公司的命运押在它上面。所以我们很快就排除了它。
这使我们选择了混合方案。构建我们自己的原生外壳来包裹系统 WebView,结果证明给了我们 exactly 需要的东西。macOS 上是一个完整的 Xcode 项目,Windows 上是一个 Visual Studio 项目。完全访问平台 API。使用系统自己的 WebView 渲染 UI。完全控制每个部分如何相互通信。为了验证这是否真的可行,我们很早之前就构建了一个原型。我们能实现半透明窗口吗?WebView 内容上方能有原生工具提示吗?它看起来和感觉起来会像 Raycast 吗?原型出来后看起来几乎和原生应用一模一样。透明的 WebView 与窗口背景融合,原生覆盖层用于工具提示和操作面板等功能。本质上是我们花了多年时间构建的相同视觉语言。
不过,它不是银弹。这种方法有真正的开销。除了你的应用之外,你实际上是在构建和维护 Electron 开箱即用的基础设施。WebView、原生外壳和 Node.js 后端之间的 IPC 需要在每个平台上搭建、调试和优化。没有社区为你解决这些问题。我们选择它是因为 Raycast 的工作方式。这个权衡对于大多数其他桌面应用来说没有意义。Electron 处理得足够好,并且能为你节省数月的基础设施工作。
我们还看了其他几个选项:Flutter、Qt、桌面版 React Native,以及在两个平台上运行 Swift(向 The Browser Company 的勇气致敬,但我们没有他们那么大胆)。但我们很早就排除了它们。它们要么缺乏我们需要的原生控制力,要么对我们的用户群体来说不够成熟,或者两者兼有。
如何构建
从高层次来看,Raycast 2.0 由四个部分组成:
- 宿主应用: 每个平台都有自己的应用,macOS 上用 Swift + AppKit 编写,Windows 上用 C# + .NET 8 + WPF。应用控制所有必须是平台原生的东西,比如设置窗口、监听全局热键、配置菜单栏或托盘等。它们还在平台的 WebView 中加载 Web 前端(macOS 上是 WKWebView,Windows 上是 WebView2)并管理 Node 后端。
- Web 前端: 前端是一个 React + TypeScript 项目,同时发布到两个平台。它包含所有 UI 代码,并为每个窗口构建单独的入口点(启动器、AI Chat、Notes、设置等)。两个操作系统上的代码库是相同的。
- Node 后端: 一个长期运行的 Node 进程拥有应用的业务逻辑,比如数据库访问、扩展运行时、其他长期运行的服务等。Node 是两个平台都与之对话的共享层,这意味着功能开发只需做一次。
- Rust 核心: Rust 用于性能和可移植性比便利性更重要的地方。我们的数据层可以与 iOS 应用共享。云端同步与其服务器端对应部分共享 schema。我们的自定义文件索引器经过重度优化,可以在几秒钟内扫描整个硬盘。
在多个运行时(Swift/C#、Node、WebView)共同工作的情况下,不同层需要相互通信。我们混合使用平台消息处理程序和 stdio 传输来连接一切。为了安全地工作,接口在一个地方声明,并为每个端生成类型化的客户端。这为我们提供了跨所有四个运行时的编译时保证。
Raycast v2 技术栈
实际上,团队大部分人在 Web 前端和 Node 后端工作。那是功能构建的地方。原生外壳在我们需要从操作系统暴露新功能或优化原生体验时才会被触及(下面会讲到)。一旦四个部分之间的边界确定下来,大部分产品工作不需要跨越它们。
新的文件索引器
在 v1 中,文件搜索依赖 Spotlight 元数据。它(大多数时候)能用,但我们受限于 Spotlight 已经索引的内容,而且它根本不能在 Windows 上工作。在 v2 中,我们用 Rust 从头构建了自己的文件索引器。它作为一个独立进程运行,直接扫描文件系统,构建搜索索引,并通过文件系统事件保持更新。
在 Windows 上,以正常方式遍历 NTFS 文件系统对于我们需要的扫描速度来说太慢了。所以我们构建了一个专门的 NTFS 扫描器,直接读取主文件表(Master File Table)——这是将整个驱动器在几秒钟内而不是几分钟内索引的唯一实用方法。
索引器是 Rust 的性能优势最显著的地方之一。扫描数十万个文件并构建搜索索引需要在后台进行,不能影响应用的其他部分。可预测的内存使用量和没有 GC 停顿使这成为可能。
让它感觉像原生应用
当 UI 运行在 WebView 中时,“原生感”到底意味着什么?对我们来说,它归结为一个简单的测试:如果有人在不知道 Raycast 用什么技术构建的情况下使用它,他们会认为它是一个普通的 Mac 应用吗?如果任何感觉不对的地方——错误的动画、不属于自己的悬停状态、在窗口边缘被裁剪的弹出层——我们就没有完成工作。
Raycast v2 概览
我们的一位 Windows 工程师说得很好:我们不是在一个 Web 应用上撒了一些原生钩子。我们是一个使用 Web 技术做 UI 的原生应用。这种区别塑造了我们投入时间的地方。下面的工作大部分不是让东西看起来对,而是让东西行为对。
平台惯例
让 Web 应用在桌面上感觉不对的最简单方式,就是在原生惯例存在的地方遵循 Web 惯例。以下是我们刻意匹配或避免的一些东西:
- 交互控件上没有
cursor: pointer。 桌面应用不会这样做。这是一个小细节,但它会立即发出”这是一个网站”的信号。 - 大多数控件上没有悬停高亮。 在 macOS 上,按钮和列表项不会像在 Web 上那样在悬停时高亮。
- 设置在一个单独的原生窗口中打开,而不是模态框或侧面板。
- 弹出层和工具提示渲染为原生窗口,而不是 WebView 内的 DOM 元素。 它们可以超出窗口边界,就像原生弹出层一样。
- 在 macOS Tahoe 上,我们采用了苹果新的液态玻璃材质(Liquid Glass),让 Raycast 从第一天起就与系统更新的视觉语言融为一体。
- 视图出现或过渡时没有闪烁。 这是 Web 应用的常见特征,我们做了很多工作来消除它。
这些是显而易见的东西。不太明显的工作在下面。
与 WebKit 合作(并绕过它)
WebKit 是一个出色的渲染引擎,但它是为网页浏览构建的,不是为一个每天显示和隐藏数百次的桌面应用。开箱即用,它做出了一些对 Safari 来说完全合理、但对我们造成问题的假设。我们花了很多时间学习如何绕过它们。
- 节流(Throttling)。 WebKit 在它认为视图不可见时会节流
requestAnimationFrame、CSS 动画和定时器。对于一个不断被显示和隐藏的启动器来说,这会破坏功能。我们通过将窗口前置但保持视觉隐藏(alphaValue = 0),并禁用 WebKit 的遮挡检测(windowOcclusionDetectionEnabled = false)来解决这个问题。在显示窗口之前,我们在一个requestAnimationFrame中触发渲染,以避免闪烁。 - 被遮挡的帧渲染。 当 Raycast 从紧凑模式扩展到完整大小时,WebKit 会在之前隐藏的区域留一两帧的空白——它正在节流它认为是”视口外”的区域。我们通过让 WKWebView 的 frame 始终保持在扩展后的大小来解决这个问题,即使窗口本身是紧凑的。WebView 会渲染到窗口可见边界之外,所以当窗口扩展时,内容已经在那里了。
- 窗口大小调整。 WebKit 在动画调整窗口大小时会暂停绘制,这会导致可见的卡顿。我们通过覆盖
NSWindow.setFrame并将动画调用替换为隐式 Core Animation 来解决这个问题,这样 WebView 在窗口调整大小时继续渲染。 - 窗口打开时的闪烁。 我们使用
_doAfterNextPresentationUpdate(一个用于将渲染状态与原生展示同步的 WebKit API)来确保 WebView 在窗口可见之前完成绘制。没有它,你会看到一闪而过的陈旧或空白内容。 - Emoji 渲染。 我们的 Emoji 选择器最初很慢,因为 WebKit 对每个 Emoji 字形都回退遍历字体链。修复方法很简单——在启动时预热 Emoji 字体——但花了一段时间才弄清楚到底发生了什么。
我们还构建了基础设施来在运行时切换 WebKit 功能标志(与 Safari 开发菜单中可用的那些相同)。我们在内部使用它来解锁 60 FPS 上限,并为非关键工作的调度启用 requestIdleCallback。
在 Windows 上
WebView2 基于 Chromium,Chromium 对节流、渲染和进程管理有自己的想法。让亚克力(acrylic)模糊背景效果与自定义标题栏协同工作需要原生外壳和 WebView2 运行时之间的精心协调。我们自己控制所有初始化参数,这使我们避免了 WebView2 应用在启动时常见的白矩形闪烁。
管理多个窗口也比在 macOS 上更复杂——每个窗口都需要配置自己的 WebView2 环境,使用正确的亚克力效果、自定义边框和输入处理组合。我们还必须做特定的工作来确保 Chromium 不会在窗口未聚焦时对我们的 WebView 进行节流,因为 Raycast 经常需要在位于其他应用后面时更新。
内存与性能
对基于 Web 的桌面应用最常见的批评是它们慢、臃肿、吃内存。这是一个合理的担忧,我们想诚实地回应。
简短版本:是的,Raycast v2 比 v1 使用了更多内存。增加是真实的,但它也是有界的、可测量的,而且我们可以持续改进。团队将性能和内存视为一等优先事项,不是以后才会处理的事情。
数据
Raycast v1(完全原生 UI,Node 后端用于扩展)在使用一段时间后的内存通常在 200-300 MB 左右。Raycast v2 在类似场景下大约在 350-450 MB。确切数字取决于你有多少扩展、使用了哪些功能以及加载了多少内容。
这更高,我们不想隐瞒。这些数字也不是最终版本,因为内存优化是一个正在积极关注的领域,我们预计随着脱离测试版,内存使用量会进一步降低。以下是 v2 在主窗口隐藏时(这是 Raycast 大部分时间所处的状态)内存的大致分布:
- WebView(WebContent):约 120-200 MB
- Node.js 后端:约 150-200 MB
- 原生应用(Swift shell):约 40 MB
- WebKit GPU 进程:约 18 MB
- WebKit Networking:约 12 MB
原生外壳是轻量级的。当窗口隐藏时,WebKit GPU 进程降至 20 MB 以下(在活跃使用 Raycast 时可能会飙升更高,但当你关闭窗口时这部分内存会被释放)。两个主要开销是 WebView 和 Node 后端。
作为对比,空 WebView(无内容)的基线开销约为 50 MB,一个没有任何导入的裸 Node.js 进程约为 12 MB。这些基线是权衡的一部分。其余的是我们的应用代码、已加载的模块、图标和缓存资源,这是我们可以控制并持续优化的。
内存并非都一样
这并不意味着更高的占用量无关紧要,但它有助于解释你在活动监视器中看到的数字。当你在 Mac 上打开活动监视器时,每个进程显示的数字并不像看起来那么简单。macOS 会积极使用可用内存——它缓存文件、压缩非活跃页面,并将内容保留在内存中以使系统更快。
有几件事值得了解:
- 压缩内存。 当物理内存不足时,macOS 会压缩非活跃页面而不是写入磁盘。这很快,意味着一个看起来使用了 200 MB 的进程实际上可能只消耗了少得多的实际资源。一个内存堆庞大的空闲 Node 后端可以很好地被压缩。
- 脏页与干净页。 并非所有常驻内存的代价都相同。干净页(如映射的二进制代码)可以丢弃并从磁盘重新读取,无需代价。脏页(如 V8 堆或解码后的图片)才是真正消耗资源的。我们二进制文件在磁盘上大部分体积是操作系统可以瞬间回收的干净内存。
- 共享框架。 活动监视器将系统框架内存(WebKit、系统库)分摊到每个使用它们的进程上。当你将 Raycast 各进程的内存数字相加时,你在重复计算共享页面。真实的系统成本低于活动监视器显示的数字。
- 内存压力才是真正重要的。 活动监视器内存选项卡底部的图表是判断你的 Mac 是否吃力的真正指标。如果是绿色的,系统有足够空间,即使单个进程的数字看起来很高。操作系统正在做它的工作——使用可用内存来保持快速,当其他东西需要时随时准备释放。
这些都不是对内存 careless 的借口。我们跟踪 phys_footprint(最接近活动监视器显示的指标)并积极努力减少它。我们已经在开发过程中大幅削减了 v2 的内存占用——早期构建的数字比现在的数字高得多。我们还在低内存机器上特别测试,因为那才是最重要的。但我们希望读者在看到这些数字时有正确的心智模型。
除了内存之外,v2 在某些方面明显比 v1 更快。
- 搜索。 v2 的根搜索包含了由我们新的基于 Rust 的文件索引器驱动的完整文件搜索。在 v1 中,文件搜索只能通过单独的命令使用,并且依赖 Spotlight 元数据。新的索引器直接搜索你的文件,不依赖 Spotlight,同时保持搜索体验的响应性。
- 文本渲染。 AI Chat 和任何涉及富文本渲染的功能都是 WebKit 真正闪耀的地方。数十年来对 Web 文本布局和渲染的优化在这里体现——滚动浏览长对话、渲染 Markdown、处理带语法高亮的代码块。macOS 上的 TextKit 很有能力,但 WebKit 在这种工作负载上投入了更多。
我们还没有完成。内存和性能是正在积极关注的领域,我们知道还有改进空间。团队正在进一步降低稳态内存占用,让前端和后端更多地惰性加载,优化图标和图片处理,并收紧 V8 堆。毕竟,它还在测试版。
权衡
没有任何重写是免费的。以下是什么变得更好了,什么变得更困难了。
什么更好了
先从好的方面说起。以下是 Raycast 第二个版本让我们感觉有所改善的地方:
- 开发速度。 这是最大的一个。热重载意味着 UI 变化在不到一秒内就能看到,而 v1 中需要重新编译 Swift 目标并重启应用。我们可以更快地原型设计、迭代和修复 bug。这直接造福用户——功能更快上线,修复更快落地。
- 一个团队,两个平台。 大部分产品工作在共享的 Web 前端和 Node 后端中完成。当我们发布一个功能时,它在 macOS 和 Windows 上都能工作。在 v1 中,每个 UI 更改本质上都是仅限 macOS 的。作为额外收获,移动团队将从 Rust 模型层和新的同步引擎中受益。
- 招聘。 找到能使用 React、TypeScript 和 Node 的工程师比找到有深入 AppKit 经验的工程师容易得多。这不意味着我们不再需要原生工程师——我们仍有专门的 Swift 和 C# 工程师在宿主应用上工作——但大部分产品工作不再需要专门的特定平台知识。
- 更丰富的 UI。 有些东西用 Web 技术栈构建起来更容易做好:富文本编辑、Markdown 渲染、带有动画的复杂布局。Notes 和 AI Chat 都从中受益。它还为我们提供了成熟的构建块,用于编辑、解析和渲染等领域,同时仍然让我们掌握那些让 Raycast 感觉像 Raycast 的部分。
- 扩展变得更简单了。 由于 Node.js 现在捆绑在应用中,你第一次从商店安装扩展时不再需要单独下载它。而且因为应用本身与扩展运行在相同的技术栈上(React、TypeScript、Node),构建内部功能和构建扩展的感觉几乎相同。
什么更困难了
并非一切都是完美的,以下是更复杂的技术栈带来的缺点:
- 更高的内存基线。 如上一节所述,v2 比 v1 使用了更多内存。WebView 和 Node 进程增加了一个完全原生应用没有的基线成本。用 Web 技术栈保持低内存是可以做到的;只是需要更多刻意的努力。我们正在积极努力缩小差距,并预计随着脱离测试版,这些数字会下降。
- 技术栈复杂性。 四个运行时(Swift 或 C#、Node、WebView、Rust)意味着更多活动部件。调试一个问题可能需要从 React 前端穿过 IPC 到 Node 后端,再进入 Rust 模块。类型化的 IPC 代码生成有助于保持同步,但技术栈客观上比单一语言的原生应用更复杂。
- Windows 的多样性。 Windows 是一个比 macOS 更多样化的平台。用户运行不同的操作系统版本、硬件配置和显示设置——在较老 CPU 的 4K 显示器上使用 8GB 内存并不罕见。使用系统 WebView 还意味着 WebView2 版本可能在不同机器上不同,因此我们需要考虑不同的渲染行为和 API 可用性。需要测试的表面更多,边缘情况也更多。
- 一些原生细节更难处理。 在 AppKit 中免费获得的东西——比如某些无障碍行为、拖放的边缘情况或输入法(IME)处理——在 WebView 中需要显式的工作。我们处理了重要的部分,但还有一长串小的平台行为需要关注,我们仍在处理中。
- 按需窗口启动。 在 v1 中,像 AI Chat 和 Notes 这样的窗口一旦调用就会一直保留在内存中,所以当你按下热键时它们会立即出现。在 v2 中,我们更积极地回收不活跃窗口的内存,这意味着当你冷启动打开它们时会有短暂的延迟。我们正在努力找到正确的平衡——增加宽限期,让窗口在你快速切换它们时保持”温热”,同时在你不使用时仍然回收内存。
我们认为这些权衡是值得的。不是因为缺点不重要,而是因为开发速度、跨平台覆盖和招聘方面的收益会直接转化为随着时间的推移更好的产品。困难的部分可以通过工程努力解决。更好的部分用其他方式会很难获得。
未来方向
如果你读到这里,你可能在期待一个关于哪种方法”最好”的结论。我们并不是这样想的。我们将代码视为达到目的的手段。对我们重要的是产品,而不是技术栈。我们是自己的用户,我们在自己拥有的每台机器上每天使用 Raycast,如果感觉不对,我们不会发布。这就是标准,也是重写花了这么长时间的原因。
Raycast 2.0 现在处于公开测试版。如果有什么感觉不对、感觉慢、或者感觉不像 Raycast,请告诉我们。这种反馈正是我们现在需要的。
快速感谢完成这一壮举的团队。从一个原型开始,现在到了每个想尝试的人手中。没有大量的努力和注重细节,这是不可能完成的。
我们的目标是继续推动桌面生产力的意义,特别是在 AI 正在改变人们与机器交互方式的当下。有了这个新代码库,我们可以快速行动,在两个平台上发布高质量的应用,并贴近用户真正需要的东西。还有更多东西在路上。回头见!