慣性聚合 関心のあるブログ、ニュース、テクノロジーを効率的に追跡
原文を読む 慣性聚合で開く

おすすめ購読元

博客园 - 司徒正美
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件 : 206ms TTFB(最初のバイトまでの時間)
  • 67件 : 675ms TTFB

それは+469ミリ秒の差異、またはおおよそ8ミリ秒ごとの線形劣化です。バックログの項目を一つ追加するごとに約8ミリ秒がレスポンス時間に加わりました。あまり良くありませんでした

。最初の直感はフロントエンドを責めることでした——コンポーネントツリーがあまりに積極的に再レンダリングされていないかも。しかしTTFBはそれをすぐに否定しました。ブラウザはまだレンダリングを開始していなかった;サーバーがそれだけの時間かけてレスポンスするのだった。

APIサーバーとデータベースの間にボトルネックがあった。もっと深く掘り下げる時だ。

プリロードの仕組み

私たちはGo + gqlgen + GORMをCloud Runで実行し、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事前ロードで64ms追加されるだけです。それはノイズです。しかし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: progressフィールドにはまだタスクデータが必要です

各バックログアイテムの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 }をまだリクエストし、EnhanceFieldsヘルパーはhasTasksが既にtrueである場合にはノーオペレーションです。詳細ページの挙動は全く変わらず。

異なる方法で行うべきこと

このデバッグから得られるいくつかの要点:

  • TTFBは最初の診断です。 TTFBが項目数に比例して増加する場合、問題はサーバー側です。APIを除外するまでReactのレンダリングをプロファイリングする時間を無駄にしないでください。

  • FragmentsはUXの便利な機能であり、データ契約ではありません。 Fragmentを異なるフィールドセットをレンダリングする複数のビュー間で共有する瞬間、そのビュー間のパフォーマンスプロファイルの間接的な結合を作成します。SubTaskFieldsList を検討してください。SubTaskFieldsDetail は一つの共有 Fragment の代わりに使用されます。

  • 「サーバーはあなたが要求するものだけをロードする」は、正しく要求すれば機能します。 GraphQL の正確なデータ取得の約束は、クエリ作成者 が正確であることに依存しています。サーバーは忠実に指示されたことを行っています——問題は、あなたが伝えているものにあります。

  • 線形の劣化は小さなデータセットに隠れています。 8ms/アイテムは8件のとき非表示です。100件で壁になります。シードデータのみでテストすると、この種類の問題に気づかないでしょう。本番規模のデータでプロファイルするとカウントされます。


本記事は、タスク管理SaaSのLasimban(Lasimban)における実際のパフォーマンス修正に基づいています。これはスクラムチーム向けに構築されたサービスです。無料で利用できます — クレジットカードは不要です。

Lasimbanを無料で試してみる→lasimban.team

Lasimban - スクラムに焦点を当てたタスク管理ツール

スクラム開発をより直感的で楽しくする。Lasimbanは、スクラムに焦点を当てたタスク管理ツールで、あなたのチームに正しい方向性を示す。

favicon lasimban.team


GraphQLの設定で似たFragment共有の罠に遭遇しましたか?他のチームがリストと詳細の事前ロードの分割をどのように処理しているかについて、聞いてみたいです。