首页 / 文章 / Linear 为什么这么快?技术深度解析
← 返回
AI技术

Linear 为什么这么快?技术深度解析

✍️ zhirenhun 📅 2026/6/8 👁 89 阅读 ⏱ 16 分钟
Linear 为什么这么快?技术深度解析

引言:毫秒级的秘密

在 Linear 中更新一个 issue 只需要几毫秒。传统的 CRUD 应用做同样的事情需要大约 300 毫秒。他们是怎么做到的?性能没有灵丹妙药。真正的答案是,它从底层开始就建立在正确的基础之上,然后通过无数次的决策不断改进。我的目标就是带你了解一些让 Linear 体验如此流畅的技术,并帮助你实现同样的效果。

浏览器中的数据库

大多数 Web 应用都陷在同一个循环里:用户点击 → 浏览器发起 HTTP 请求 → 服务器查询数据库并返回 → 浏览器重新渲染。最终的结果就是,用户在几百毫秒内看着 spinner、骨架屏或者冻结的 UI,等待网络响应。

Linear 颠覆了这种传统关系。 UI 读取的实际数据库就在浏览器中,存在 IndexedDB 里。变更先在本地应用,然后异步推送到服务器,服务器再通过 WebSocket 将增量广播回其他客户端。

在我看来,这是 Linear 性能最核心的部分。当你的目标是构建一个快速的 Web 应用时,最大的瓶颈就是网络。任何在客户端和服务器之间传输的数据都要花费数百毫秒。最好的方法就是彻底消除网络请求——而这正是 Linear 所做的。

这里有一个例子,展示了 Linear 的请求有多么简洁:

// 传统 Web 应用更新服务器
async function updateIssue({ issue }) {
  showSpinner();
  const response = await fetch(`/api/issues/${issue.id}`, {
    method: "PATCH",
    body: JSON.stringify({ title: issue.title }),
  });
  const updated = await response.json();
  setIssue(updated);
  hideSpinner();
}

// Linear 的方式
issue.title = "Faster app launch";
issue.save();

第一行 issue.title = "Faster app launch" 更新了内存中的数据存储(在 Linear 中是通过 MobX observable)。第二行 issue.save() 将一个事务加入队列,由同步引擎批量处理后刷新到服务器。关键在于,UI 是同步地基于本地内存中的更新进行重新渲染的。

Linear 的联合创始人 Tuomas 在 2024 年的一次会议上说过:"我写的第一行代码就是同步引擎,这对创业公司来说非常不寻常。"从第一天起,Linear 就明确了他们要采用的方法以及需要做出的权衡。

我知道大多数人不会像 Linear 那样构建自定义同步引擎来让应用变快,而且他们也不需要。大多数场景下,使用 Tanstack QuerySWR 这类库配合乐观更新(optimistic updates)已经可以做到非常接近的效果。核心思想很简单:UI 的响应速度不应该依赖于网络延迟。

// 使用 SWR 的乐观更新
mutate(
  `/api/issues/${issue.id}`,
  { ...issue, title: "Faster app launch" },
  false
);

Linear 的技术栈一览

Linear 建立在你能找到的最简单的技术栈之上:React、TypeScript、MobX、Postgres、CDN。没有边缘数据库,没有 React Server Components,没有花哨的框架。

前端: React + react-dom、MobX(observable 图,颗粒级重新渲染)、TypeScript、Rolldown-Vite + plugin-react-oxc、ProseMirror + y-prosemirror(富文本编辑器,Yjs CRDT 实时协作)、Radix UI(弹窗、菜单、焦点管理)、Emotion + StyleX、Comlink(Worker RPC)、idb(IndexedDB 封装)、graphql-request(GraphQL 传输)、Sentry(错误监控)、Inter Variable 字体

后端: Node.js + TypeScript、PostgreSQL on Cloud SQL(issue 表 300 路分区)、Memorystore Redis(事件总线 + 缓存 + 同步游标)、turbopuffer(相似 issue 检测)、Kubernetes on GCP、Cloudflare Workers(多区域边缘代理)

让首次加载感觉瞬间完成

Linear 的构建工具演进史:Parcel → Rollup → Vite → Rolldown。每次迁移都是出于同一个目标:减少 JavaScript 和 CSS 的体积,并改善开发者体验。根据他们自己的博客文章,结果是:代码量减少 50%,压缩后体积减小 30%,冷缓存页面加载速度提升 10% 到 30%,活动 issue 视图的首屏渲染时间在 Safari 上降低了 59%,内存使用量降低了 70% 到 80%。

即使有了所有这些优化,Linear 仍然需要传输相当数量的代码:大约 21 MB 的压缩后 JavaScript。区别在于,它被激进地拆分成数百个按需加载的路由级别代码块。

// vite.config.ts(与实际代码块图匹配)
export default defineConfig({
  plugins: [react()],
  build: {
    target: "esnext",  // 没有旧语法,没有 polyfill
    cssMinify: "lightningcss",
    modulePreload: { polyfill: false },
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes("node_modules")) {
            const pkg = id.match(/node_modules\/([^/]+)/)?.[1];
            if (pkg) return `vendor-${pkg}`;
          }
        },
      },
    },
  },
});

教训不在于选择哪个构建工具,而在于:放弃对老旧浏览器的支持、使用原生 ESM、以及激进地进行代码拆分。每一步都很小,但叠加起来,Linear 的首屏 JavaScript 减少了一半,构建时间降低了一个数量级。

加载后的预加载

Linear 的做法是:在 JavaScript 运行之前,浏览器就已经看到完整的依赖列表,并并行发出请求。等到入口脚本执行到第一个 import 时,这些代码块已经缓存在浏览器中了。

<script type="module" crossorigin src="..."></script>
<link rel="modulepreload" crossorigin href="...">
<!-- 后面还有很多... -->

Service Worker 让速度更进一步: Linear 其余的代码块由 Service Worker 在后台缓存,在首次页面加载后在后台静默拉取。登录后几秒钟内,整个应用就已经在缓存中了。这意味着后续导航完全跳过网络,即使用户离线也能正常使用。

渲染优先,认证靠后

Linear 的做法很简单:内联启动脚本只检查 localStorage.ApplicationStore 是否存在。如果存在,说明用户以前用过,工作区已经在 IndexedDB 中了。如果不存在,就直接显示登录布局。实际的会话令牌在 cookie 里。包逻辑从不尝试去解析它,只是渲染已有的数据,然后让下一个请求在会话过期时返回 401。客户端信任本地数据,服务器是正确性的最终来源,两者异步协调。

同步引擎

让 Linear 快起来的核心,归根结底是一个决策:服务器是同步目标,而不是 UI 的真相来源。

1. 数据已经在本地了: 应用启动时不会从服务器获取工作区,而是从 IndexedDB 水合到内存中的 MobX 对象池。Issue 和 Comment 两个最大的表是按需惰性水合的——这是"数据层面的代码拆分"。

2. 变更不等待网络: 当你更改一个 issue 的状态时,三件事几乎同时发生:MobX observable 更新让 UI 反映变更、变更写入 IndexedDB 中的持久化事务队列、变更排队等待服务器处理。用户永远不会等待看到自己的变更效果。

3. 一个增量,更新一个单元格: 当服务器确认一个变更时,由于 Linear 中每个模型的每个属性都是自己的 observable,MobX 确切知道哪些组件依赖哪些字段。一个字段的更新只重新渲染读取该字段的组件,而不是父列表、侧边栏。50 个 issue 的更新就是 50 个单元格的重新渲染,而不是列表的重新渲染。

为速度而设计

Linear 的另一个基石是它将键盘作为导航和完成工作的主要工具。每个常见操作都有快捷键。命令面板一键打开。命令面板(⌘K)允许用户搜索几乎任何 Linear 中的操作。它非常快,因为它搜索的是本地 MobX 对象池,而不是服务器。

动画

浏览器有三层属性变化:合成属性(transformopacity)交给 GPU 处理;触发重绘的属性(colorbackground-color)跳过布局但仍需重绘像素;触发布局的属性(widthheightmargin)强制浏览器重新计算每个后续元素的位置。永远不要动画化这些属性。

/* Linear 的做法 */
.row:hover {
  background-color: var(--color-bg-hover);
  transition: background-color 0.12s;
}
.icon-arrow {
  transform: translateX(0);
  transition: transform 0.15s;
}

Linear 的大部分动画都引用了它们的源位置。持续时间保持短促:大多数过渡在 0.1s 到 0.25s 之间,远低于行业标准的 200-350ms。

总结

没有什么单一的东西能让一个应用性能卓越。它是成百上千个正确决策的累积。Linear 选择服务器作为同步目标而不是真相来源,数据库在浏览器中,变更本地优先应用后台协调,首次加载用更小的块传输更少的代码,Service Worker 在后台预缓存,认证基于本地状态假设,动画保持在 GPU 上执行。难的不是实现,而是日复一日对工艺的坚持。

——

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

← 上一篇
通过 SQL 注入绕过机场安检
下一篇 →
LLM 正在侵蚀我的软件工程职业生涯,我不知道该怎么办

📌 相关推荐

📄
Rhombus 1.0 正式发布
2026/6/24
📄
艾尔登法环的低技术AI
2026/6/24
提示注入的理论基础:角色混淆(Prompt Injection as Role Confusion)
2026/6/23
← 返回文章列表