慣性聚合 高效追蹤和閱讀你感興趣的部落格、新聞、科技資訊
閱讀原文 在慣性聚合中打開

推薦訂閱源

博客园 - 司徒正美
V
V2EX
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
Blog — PlanetScale
Blog — PlanetScale
A
About on SuperTechFans
月光博客
月光博客
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python)
如何共享 GraphQL 片段無聲地摧毀了我們的列表性能
Ryo Tsugawa · 2026-05-24 · via DEV Community

感覺有點慢

我們的產品積壓清單頁面反應遲鈍。沒有明顯崩潰 — 只是… 不對勁。那種在你從一個新鮮的示範租戶切換到一個有實際數據的真實租戶時才會注意到的緩慢感.

所以我測量了一下:

  • 8 項目206毫秒 TTFB (首個字節的響應時間)
  • 67 項目675毫秒 TTFB

那樣看來+469ms 差異,或大約為8ms 每項的線性退化。每一項延遲都增加了 ~8ms 到回應時間。不怎麼好.

第一個直覺是責怪前端 — 或許組件樹在過度重新渲染。但TTFB立即排除了這個可能性。瀏覽器甚至還沒開始渲染;服務器回應花了那麼長的時間。

所以瓶頸在API伺服器與資料庫之間。是時候深入挖掘了

我們的預載運作方式

我們在Cloud Run上運行Go + gqlgen + GORM,與Cloud SQL (PostgreSQL) 通信。GraphQL層面設計圍繞著一個簡單的原則:

客戶僅請求它需要的欄位。伺服器僅預載所請求的內容。

實際上,我們的解析器檢查傳入的 GraphQL 選擇集並將其翻譯成 GORM Preload() 呼叫。如果前端請求 tasks { assignees { user } },後端忠誠地預載 Tasks → Assignees → User。如果它僅僅請求 tasks { backlogStatus },我們僅預載 Tasks → BacklogStatus

這是「正確」的設計。後端信任前端能準確宣告其資料需求,並回應正好是那樣 — 不多不少。

理論上。

实际发生的情况

這是我们列表頁面發送的 GraphQL 查詢(簡化版):

query GetProductBacklogItems($projectId: ID!) {
  productBacklogItems(projectId: $projectId) {
    edges {
      node {
        id
        name
        storyPoint
        priority
        progress
        backlogStatus { id name status }
        tasks {
          ...SubTaskFields
        }
      }
    }
  }
}

全屏模式 退出全屏模式

以及SubTaskFields 看起來這樣:

fragment SubTaskFields on Task {
  id
  name
  backlogStatus { id name status }
  assignees {
    user { id givenName familyName }
  }
}

進入全螢幕模式 離開全螢幕模式

發現問題了嗎?清單頁面正在請求 tasks { ...SubTaskFields },而 SubTaskFields 包含 assignees { user { ... } }

清單頁面不顯示任務指派人員。 那個頁面上沒有任何組件讀取task.assignees。但查詢仍然要求它。

在後端,這觸發了一個4級預載鏈 — 每個待處理項目執行一次:

預載
1 Tasks
2 Tasks.BacklogStatus
3 Tasks.Assignees
4 Tasks.Assignees.User

對於67個項目,那就是67 × 4級聯數據庫查詢。每一個都通過網絡打擊Cloud SQL。怪不得它是線性的.

為何沒人發現

這是最令人沮喪的部分:Fragment確實正在做它應該做的事 — 只是在不同的上下文中.

我們對待回滯項目有兩種視圖:

  1. 列表視圖 — 顯示名稱、狀態、故事點、進度。沒有任務級別的指派資訊.
  2. 詳情視圖 — 顯示所有內容,包括每個子任務指派給誰.

詳情視圖確實需要assignees { user }SubTaskFields內。那個 Fragment 是為詳情視圖編寫的,在那裡是正確的。

清單視圖只是…借用了它。相同的 Fragment,不同的上下文,性能特徵卻大相徑庭.

在規模較小時,沒人注意到。8 項目 × 4 預加載可能增加 64 毫秒。這只是噪音。但一旦達到 50+ 項目,線性成本就變得令人痛苦地顯而易見.

還有一個因素:本地的開發機器太快了。 在開發階段,資料庫要麼是本地的,要麼是在附近的 Docker 容器中 — 網絡迴路基本上是零。每次預載的成本是微秒級別,所以即使有 4 個層級也感覺是即時的。在生產環境中,每個預載都是在 Cloud Run 和 Cloud SQL 之間的一個網絡跳躍。這個每跳延遲,乘以 4 個層級 × N 個項目,就是將一個看不見的開銷轉變成一個真正的瓶頸的因素。

學習的教訓: GraphQL 片段是為開發者提供便利,而不是關於數據需求的合約。當你在顯示需求不同的視圖中共享一個片段時,你正在無聲地選擇你不需要的預加載。

解決方案

步驟 1:從列表查詢中刪除不必要的字段

最顯然的步驟 — 停止在列表查詢中請求 tasks { ...SubTaskFields }

query GetProductBacklogItems($projectId: ID!) {
  productBacklogItems(projectId: $projectId) {
    edges {
      node {
        id
        name
        storyPoint
        priority
        progress
        backlogStatus { id name status }
        # tasks removed — list page doesn't render subtask details
      }
    }
  }
}

進入全螢幕模式 離開全螢幕模式

但是有個關鍵。

步驟 2:Theprogress這個領域仍然需要任務數據

這個progress每個待辦事項的百分比是伺服器端計算 從其子任務中獲取。具體來說,它計算已完成任務數量與總數量的比例。為此,伺服器需要載入 TasksTasks.BacklogStatus.

如果前端停止請求 tasks,後端停止預載它們,而 progress 對所有內容靜默地返回 0。那比慢還糟糕。

第 3 步:EnhanceFields 模式

我們已經有了一套既定的模式:欄位注入輔助工具 來確保必要的預載資料無論前端要求什麼都存在。我們對專案統計和團隊統計使用相同的方法。

這個想法很簡單:在將請求的欄位傳遞到預載層之前,檢查服務器端計算所需的欄位是否存在。如果不存在,就注入它們。

func enhanceFields(fields []string) []string {
    // If the client didn't ask for "tasks" at all,
    // inject the minimum needed for progress calculation.
    if !contains(fields, "tasks") {
        fields = append(fields, "tasks", "tasks.backlogStatus")
    }
    return fields
}

進入全螢幕模式 離開全螢幕模式

在解析器中,這個輔助工具位於選擇集合解析器和預載建構器之間:

func (r *resolver) ListItems(ctx context.Context, ...) {
    fields := enhanceFields(getRequestedFields(ctx))
    // fields is now guaranteed to include "tasks" + "tasks.backlogStatus",
    // but NOT "tasks.assignees" or "tasks.assignees.user"
    items := r.repo.FindAll(ctx, filters, fields)
    // ...
}

進入全螢幕模式 離開全螢幕模式

關鍵洞見:列表和詳細視圖現在具有非對稱的預載深度。詳細檢視仍然獲得所有 4 級別(前端要求它們)。清單檢視僅獲得 2 級別 — 足夠計算進度,不拖入指派者數據,它從不顯示.

結果

之前 之後
前端請求 tasks { backlogStatus, assignees { user } } (不請求任務)
後端預加載 任務 → 營運狀態 → 指派人員 → 使用者 任務 → 營運狀態
預載深度 4 2
TTFB (67 項目) 675毫秒 135毫秒

5倍更快. 這個~8毫秒/項目的線性退化已經基本上消失了。

而且最好的部分:詳細視圖完全不受影響。它仍然請求tasks { ...SubTaskFields },而且EnhanceFieldshelper 是一個無操作當前hasTasks這已經是事實了。詳細頁面沒有任何行為變化。

我會做不同的選擇

從這次除錯中得到的幾點心得:

  • TTFB 是你的第一個診斷。 若 TTFB (Time to First Byte) 與項目數量成線性關係,問題在於伺服器端。在你排除 API 問題之前,不要浪費時間分析 React 渲染。

  • 片段是為了提升開發者體驗 (DX) 而設,不是數據合約。 當你在渲染不同字段集的視圖之間共享片段時,你就在這些視圖的性能表現之間建立了無形的耦合。考慮使用 SubTaskFieldsListSubTaskFieldsDetail 而不是一個共享的 Fragment.

  • "伺服器只載入您要求的部分" 只有在您正確要求時才有效. GraphQL 精確數據抓取的承諾依賴於 查詢作者 的精確性。伺服器忠實地執行它被告知的內容 — 問題始終在於您告訴它的內容.

  • 線性退化隱藏在小數據集中。 8ms/項目在8個項目時不可見。它在100個時像一堵牆。如果你只用種子數據測試,你永遠不會抓到這類問題。用生產規模數據進行分析。


這篇文章基於一個真實的性能修復,發生在Lasimban,一個為Scrum團隊構建的任務管理SaaS。它是免費使用的 — 不需要信用卡。

免費試用Lasimban → lasimban.team

Lasimban - 專注於Scrum的任務管理工具

讓Scrum開發更直觀、更愉快。Lasimban是一個專注於Scrum的任務管理工具,為您的團隊指明正確的方向。

favicon lasimban.team


你在 GraphQL 設定中是否遇到過類似的 Fragment 共享陷阱?我很想知道其他團隊如何處理清單與細節預載的拆分。