元鉴
返回中文阅读流

GitHub Blog / Changelog

从延迟到即时:现代化 GitHub Issues 导航性能

GitHub Issues 团队如何利用客户端缓存、智能预取和服务工作者实现即时导航体验。文章首发于 The GitHub Blog。

中文内容

已翻译official company source英文原文2026-05-14

从延迟到即时:现代化 GitHub Issues 导航性能

GitHub Issues 团队如何利用客户端缓存、智能预取和 service workers,让导航体验近乎即时。

Mona sits on a raised green block beside another block displaying a circular refresh-style icon.
May 14, 2026
| 15 minutes
  • 分享:

当你处理积压任务时——打开一个 issue、跳转到相关联的讨论线程,然后再返回列表——延迟就不只是一个指标。它是一种上下文切换。即使是很小的延迟也会累积,并且在开发者正努力保持工作流状态的关键时刻影响最为明显。并不是说 GitHub Issues 本身“慢”;而是太多导航仍然要为重复的数据获取付出代价,一次又一次地打断工作流。

今年早些时候,我们着手解决这个问题——不是通过追逐后端上的边际收益,而是通过改变 issue 页面端到端加载的方式。我们的方法是将工作转移到客户端,并优化感知延迟:先利用本地可用数据即时渲染,然后在后台重新验证。为了实现这一点,我们构建了一个由 IndexedDB 支撑的客户端缓存层,添加了预热策略以在不造成请求泛滥的情况下提高缓存命中率,并引入了 service worker,使缓存数据即使在硬导航时仍然可用。

在这篇文章中,我们将介绍该系统的工作原理以及实践中发生的变化。我们会讲到我们优化的指标;缓存和预热架构;service worker 如何加速过去较慢的导航路径;以及真实使用场景下的结果。我们还会深入探讨其中的权衡——因为这种方法并非没有代价——以及还需要做些什么,才能让“快速”成为进入 Issues 的每条路径上的默认体验。如果你正在构建数据密集型 Web 应用,这些模式可以直接迁移:你可以将同样的模型应用到自己的系统中,在无需等待全面重写的情况下减少感知延迟。

思维的速度:2026 年的 Web 性能

2026 年,“足够快”已不再是一个具有竞争力的标准。对于开发者工具而言,延迟就是产品质量。当有人在分诊多个问题、审查功能请求或报告 bug 时,每一次可避免的等待都会打断工作流。

现代本地优先工具和经过极致优化的客户端,已经把标准从“1 秒内加载完成”推进到“感觉即时响应”。在这个世界里,用户不会拿我们和旧式 Web 应用作比较。他们会拿我们和自己每天体验过的最快体验作比较。

GitHub Issues 并不是一个小体量的功能界面。每周,全球有数百万人依赖 Issues 来保持其代码库顺畅运行。随着 Issues 也成为 AI 辅助工作的规划层,感知性能变得更加关键:如果从意图到反馈的循环很慢,整个系统都会显得缓慢。

我们从内部团队和社区听到了同样的问题:与那些把速度作为首要原则来构建的工具相比,Issues 显得过于笨重。瓶颈不在于功能深度或正确性,而在于架构和请求生命周期。太多常见路径仍然要付出服务器渲染、网络获取和客户端启动的全部成本,即使相关数据实际上此前已经被看到过。

我们的 Issues Performance 团队的职责就是缩小这一差距。目标直接且具有技术性:重新设计数据流和导航行为,让产品默认给人即时响应的感觉。

在改变架构之前,我们需要就用户语境中的“快”意味着什么以及如何衡量它达成一致。通用页面指标很有用,但对于 Issues 这样复杂的产品界面来说,它们还不够充分。

我们使用 HPC(Highest Priority Content),这是一项与 Web Vitals LCP 高度一致的内部指标,用于衡量页面上的主要内容(用户关心的内容)何时首次渲染。与 LCP 一样,它锚定到由浏览器选择的单个 HTML 元素;在 issue 页面上,这个元素最常见的是 issue 标题或 issue 正文。如果该元素能够快速渲染,即使非关键页面区域仍在加载,体验也会让人感觉响应迅速。

在操作层面,我们使用 HPC 阈值对导航进行分桶:

  • 即时:HPC < 200 毫秒
  • 快速:HPC < 1000 毫秒
  • 缓慢:HPC >= 1000 毫秒

这些阈值为我们提供了一个用于衡量用户感知速度的实用模型,而不仅仅是原始后端延迟。<200 毫秒这一档对应于在真实工作流中感觉即时的交互,而 <1000 毫秒这一档则涵盖了仍然可以接受、但对用户来说已不再无感的体验。

这也是我们的衡量理念发生演变的节点。过去,我们投入大量精力跟踪 HPC 的 p90 和 p99,并尽量降低分布中最差的尾部。虽然这项工作仍然重要,但它本身并不能确保产品对大多数用户而言感觉很快。即使提升了 HPC 的 p99,中位数体验仍可能让人感觉迟缓。

在这项计划中,我们将重点转向分布质量:在整体用户群中,有多少次导航落入我们的快速和即时分档?目标不只是减少糟糕的离群值,而是让速度成为大多数会话的默认路径。

基线:在我们做出任何改变之前的导航构成

在实施优化之前,我们需要清楚地建模用户实际是如何到达 issues#show(查看 issue 的路由)的。将所有导航都视为同一类流量,会掩盖真正的瓶颈。

我们确定了三种主要导航类型:

  • 硬导航:完整的浏览器加载(冷启动或刷新),在此过程中需要承担网络、服务器渲染、资源加载、JavaScript 启动和 React 水合的全部成本。
  • Turbo 导航:Rails Turbo 转换,在不完全重新加载的情况下更新目标页面区域。它避免了一些硬导航开销,但仍然高度依赖服务器渲染的响应。
  • 软导航(React):在现有 React 运行时内部进行的客户端转换,通常可以避免完整页面引导的成本。

我们在该工作流开始时测得的分布如下:

Graph showing navigation mix for issues show route (57.6% hard, 37.5% react).

这种分布让一件事变得显而易见:占主导地位的路径也是最慢的路径。任何只关注 React 软导航的策略都可以改善部分体验,但仅凭自身无法充分提升整体感知性能。

Graph showing HPC distribution by navigation type (2.05 hard, 1.76 turbo, 1.04 react).

这一基线影响了我们接下来的架构决策:改进快速路径,并降低硬导航带来的性能惩罚,因为大多数用户正是在那里感受到了最高的延迟。

需要注意的一点是:GitHub 仍处于从 Rails 渲染页面迁移到 React 前端的过程中。在这一过渡期间,许多用户旅程会跨越 Rails/React 边界。当这种情况发生时——例如,从 Rails 页面导航到 Issues——浏览器通常必须执行一次完整的硬导航和冷启动。这种边界跨越是硬导航在我们的基线中占比最大的一个重要原因。

我们预计,随着更多界面转为 React-native,硬导航的占比会随着时间下降。但我们不能仅仅等待平台迁移来解决问题。我们首先优化了 React 软导航,因为在这方面我们能立即发挥架构上的优势,并能快速发布改进。

一旦我们对目标达成一致,策略就变得清晰:构建一种本地优先、采用 stale-while-revalidate 模式的应用模型。这意味着先立即使用本地可用数据进行渲染,以尽量减少用户可感知的延迟,然后再异步向服务器重新验证;如果存在更新的数据,则对 UI 进行协调更新。

步骤 1:使用 IndexedDB 进行客户端缓存

我们从最有把握、也希望未来将大部分流量迁移到的地方入手:React 软导航。在这一路径中,运行时已经处于活跃状态,因此主要成本通常是数据获取延迟,而不是应用启动。如果我们能在重复访问时移除网络请求,就能把很大一部分流量转移到即时响应这一档。

我们的工作流前期分析显示出一种强烈的重复访问模式:用户在分诊和协作循环中会频繁重新打开相同的问题。基于这一行为,我们估算 issues#show 的潜在缓存命中率约为 30%,并将其作为初始可行性阈值。

Architectural diagram showing the client cache layer.

具体实现是用 IndexedDB 中的持久化客户端缓存来扩展我们现有的内存存储。

我们选择 IndexedDB 作为这一层的原因:

  • 它是持久化的浏览器存储,可在标签页关闭和浏览器重启后继续保留数据,这不同于仅基于内存的存储。
  • 索引化的对象存储模型,可对问题查询载荷进行高效的基于键的查找。
  • 相比 localStorage 具有更大的实用配额,使其适用于真实的工作集。

在该存储层之上,我们实现了 stale-while-revalidate 语义:

  • 读取路径:在软导航时,先尝试从本地缓存进行水合并立即渲染。
  • 重新验证路径:发起后台网络请求以检查新鲜度,并在数据发生变化时同步内存存储。
  • 失败行为:当网络状况不佳时,用户仍可从缓存获得可用页面;一旦连接恢复,再同步新鲜度,从而引入一种新的优雅降级模型。

其架构要点在于,这并不是“缓存或正确性”的二选一,而是在同一次导航中,以延迟优先进行渲染,并异步执行一致性检查。

初始生产结果验证了该模型。在向所有用户广泛推出后,约 22% 的 React 导航变为即时完成,高于发布前的 4%,约占总请求量的 15%。观察到的缓存命中率约为三分之一(~33%),这与此前的重访分析一致。

Graph showing HPC distribution after cache rollout.

主要权衡在于可控的陈旧性。我们测得服务器/缓存之间的差异约为 4.7%,并将其视为一个明确的运行边界:在具有限制用户可见不一致性的保护措施下,这对于软导航带来的感知速度提升是可以接受的。

提升缓存命中率

缓存的效果取决于其缓存命中率。基于 IndexedDB 的 SWR(Stale-While-Revalidate)层为我们提供了一个强有力的第一步,但三分之一的命中率也暴露出了下一个限制:大多数导航到达时,数据仍然尚未就绪。

显而易见的朴素做法是:尽早预取每一个可能的下一个 issue。我们探索了这个方向,并很快遇到了真正的约束;它不是实现复杂度,而是容量。在 issue 列表、仪表盘和项目等高扇出界面上,激进预取会放大请求量,产生类似 N+1 的访问模式,并将不必要的计算推给系统,用于那些用户可能永远不会打开的页面。

因此,我们改变了目标。我们不再试图让预取的数据始终保持最新,而是针对一个成本更低、可扩展性更好的条件进行优化:确保在用户点击时,已有一些可用数据在本地就绪。

Flow diagram showing preheating process. Steps are: Look at issues index, For each issue in the list trigger a preheat request, Is data in the cache present? if yes, add to IndexDB. If no, fetch data, then add to IndexDB.

这就是预热。预热会主动遍历高意图的 issue 引用,并在导航发生前准备缓存条目,但只有当该 issue 尚未存在于客户端缓存中时才会访问网络。如果已有可用数据,预热就会停止。这使它从根本上不同于传统的预加载。它是缓存填充逻辑,而不是新鲜度强制逻辑。

这是在新鲜度和容量使用之间做出的明确权衡。如果这能让导航本身几乎瞬时完成,我们愿意提供可能略有陈旧的数据,因为一旦用户打开该 issue,我们仍然可以在后台重新验证,并收敛到最新的服务器状态。

为了高效支持该模型,我们在 IndexedDB 前引入了一个内存缓存版本。IndexedDB 能够在标签页和会话之间提供持久化,但它仍然是异步的,因此在关键路径上并不是零成本。内存层位于活跃的内存存储和持久化存储之间,使热门 issue 载荷能够以同步方式提供,而无需承担 IndexedDB 读取成本。实践中,这从软导航中移除了另一个异步边界,并显著提高了直接从内存渲染的概率。

Diagram showing the in-memory cache layer.

在操作层面,预热由 issue 列表、仪表板、项目和依赖视图等高意图界面触发。请求在低优先级 worker 上运行,受到严格的速率限制,并由熔断器保护,因此该机制会在压力下退避。用户发起的工作始终优先于推测性获取,使我们能够避免 noisy-neighbor 问题,并在提升真实用户导航缓存命中率的同时保持系统稳定。

Graph showing HPC distribution after preheating rollout.

结果是分布发生了显著变化。在广泛推出预热后,issues#show 的即时导航总体上增加到约 30%。对于 React 导航而言,最高约 70% 变为即时导航。缓存命中率上升到约 96%。

这种权衡是可以接受的。我们花费了少量受控的后台容量,将很大比例的真实用户导航移出了受网络限制的路径。

扩展快速路径:优化 turbo 导航和硬导航

我们对 React 导航带来的提升感到满意,但软导航并不是全部。即使 GitHub 越来越多地从 Rails 转向 React,硬导航也将始终存在——刷新、新标签页、直接 URL 和入站链接。这些冷启动仍然很重要,因此我们也希望缓存数据能在这些场景中发挥作用。

我们选择的机制是 service worker。

Service worker 是由浏览器管理的脚本,它在页面本身之外运行,并且可以在网络请求到达服务器之前拦截这些请求。从概念上讲,它位于浏览器与源站之间,充当一个可编程的中间人。这使它成为少数能够影响硬导航、且不要求页面的 JavaScript 运行时已经处于活动状态的 Web 平台原语之一。

对于 issues#show,我们的 service worker 扩展了我们为 React 导航构建的同一本地优先模型。当浏览器开始对某个 issue 页面发起导航请求时,service worker 会拦截该请求,并检查本地缓存中是否已经有该 issue 数据。如果有,worker 会为传出的请求添加一个特定 header,用来告诉服务器它可以跳过大量工作。

Diagram showing service worker interception flow.

当 service worker 检测到缓存命中时,它会通过请求 header 向服务器发出信号。此后,该导航会分成两条路径:

  • 缓存命中路径:返回一个精简的 HTML shell(布局 + 最少标记 + JS),并让 React 从本地缓存的 issue payload 进行渲染。
  • 缓存未命中路径:返回正常响应(服务器加载数据并对页面进行 SSR)。

这是一项严格的优化:如果缓存是冷的、已过期,或者 Service Worker 不可用,行为会回退到标准的服务器渲染路径。

这对 Turbo 导航产生了尤其显著的影响,因为 Turbo 路径仍然受到服务器响应时间的严重限制。一旦 Service Worker 能够发出信号表明问题数据已经存在,服务器用于计算应用片段的时间就会大幅减少,Turbo 几乎会立即从后端工作量的降低中受益。

Graph showing HPC distribution for Turbo navigations after service worker rollout.

硬导航的收益是真实存在的,但不如 Turbo 的收益那样立刻可见:在缓存命中的硬导航中,我们用客户端渲染来替代 SSR 时间。关键路径现在变成了 JavaScript 的下载和执行。

为降低这一成本,我们使用 React.lazy 和动态路由预加载按路由拆分代码,因此只有当前路由所需的代码会被预先获取。我们在组件层面也应用同样的原则,只加载初始视图所必需的内容,并推迟加载非关键模块。例如,只有当用户进入编辑模式时,我们才会获取 issue 编辑器 bundle,并使用基于意图的预取(如悬停)来隐藏这段延迟,同时避免增大初始 bundle。

Distribution showing HPC for Hard navs.

结果

部署这些变更后,我们希望退一步审视其累积影响。我们分析了整个发布周期内的 HPC 指标——从最初的 IndexedDB 缓存,到预热、内存分层,再到 service worker——趋势清晰且持续:分布正向快速方向转移。

Chart showing the HPC drop over various percentiles.

我们没有挑选某个表现好的单周,而是观察了完整时间窗口,以分享近几个月的一些具体成果。以下是所有 issues#show 流量的 HPC 百分位数:

  • P10:约 600 毫秒 → 70 毫秒——最快的导航已稳稳进入即时区间,远低于 200 毫秒。
  • P25:约 800 毫秒 → 120 毫秒——现在四分之一的导航可在 120 毫秒内完成,低于此前近一整秒的水平。
  • P50:约 1,200 毫秒 → 700 毫秒——中位数体验降至一秒阈值以下,从慢速区间进入快速区间。
  • P75:1,800 毫秒 → 1,400 毫秒——上四分位数下降超过 400 毫秒,缩短了可感知延迟的长尾。
  • P90:2,400 毫秒 → 2,100 毫秒——即使是最慢的导航也有所改善,不过这个尾部仍然最清楚地表明了还需要进一步优化的方向。

最突出的模式是低百分位数的改善幅度格外显著。P10 和 P25 大幅收缩,因为缓存和预热后的导航现在主导了分布的这一部分。中位数有了明显改善,但仍受冷启动流量影响。而上尾部虽然有所好转,但反映的是硬导航路径,在这些路径中,JavaScript 启动和客户端渲染现在成为瓶颈——这正是我们接下来要重点解决的领域。

数字讲述了优化的过程,但最终真正重要的是对用户的影响。下面的视频展示了这些改动在实践中的实际体验——在真实会话中全速浏览不同 issue:

后续工作

GitHub Issues 如今比以往任何时候都更快。通过软导航、预热路径以及由 service worker 加速的流程,我们已经实质性地改变了用户感知延迟的分布,并将更大比例的流量转移到了即时响应的范畴。

与此同时,我们的工作还没有完成。依赖 SSR 的冷启动仍然是一个实际障碍,尤其是在服务器端工作量减少后,客户端启动和 JavaScript 执行成为主要成本时。

下一阶段的重点是解决更大的难题。我们计划对后端堆栈的部分组件进行有针对性的重写,明确针对低延迟交付进行优化,并正在投入建设一个更现代、位置更接近边缘的 UI 交付层,以减少往返次数并进一步提升响应时间。

性能仍然是一项持续的系统性投入,而不是一次性项目。架构在改进,瓶颈在变化,我们将持续迭代,直到快速成为所有导航路径上的默认体验。

查看 GitHub Issues 快速入门指南 >

标签:

  • 工程
  • 正文:GitHub Issues

作者

Alexander Lelidis

正文:Alexander Lelidis

正文:@alexus37

Alexander 是 GitHub Issues 团队的高级软件工程师。他拥有计算机图形学、机器学习和地理空间软件方面的多元背景。他目前工作中最喜欢的部分,是寻找富有创意的方法,让开发者的日常工作流感觉即时完成。

相关文章

Decorative background featuring two Copilot figures moving among abstract green and translucent blocks.

Copilot Applied Science 中的智能体驱动开发

我使用编程智能体来构建智能体,自动化了我部分工作。以下是我关于如何更好地与编程智能体协作的经验。

探索更多来自 GitHub 的内容

Docs

文档

掌握 GitHub 所需的一切,尽在一处。

GitHub

正文:GitHub

在 GitHub 上构建未来;这里让来自任何地方的任何人都能构建任何事物。

Customer stories

客户案例

认识使用 GitHub 进行构建的公司和工程团队。

The GitHub Podcast

正文:GitHub Podcast

收听 GitHub podcast,了解这档专注于 GitHub 上开源开发者社区内外的话题、趋势、故事和文化的节目。

原文标题

From latency to instant: Modernizing GitHub Issues navigation performance