Siddhant DevalAuthor
Senior Full-Stack Engineer·Oct 13, 2026·14 min read
Frontend Performance: Bundle Cost, APQ & Query Discipline
GraphQL's expressive query model is also its biggest frontend performance liability — the decision to have a local replica at all starts with bundle cost, and every subsequent optimization from APQ to Persisted Operations is about protecting the replica's query surface on the network.
Technical Series
GraphQL Frontend Engineering
Part 7 of 8
Frontend Performance: Bundle Cost, APQ & Query Discipline
The first GraphQL performance decision happens before the first query is written. It is the
npm install @apollo/client command. That command adds 47KB min+gzip to the client bundle — before any queries, before any components, before any cache configuration. The question that should have been asked before running it: do the normalized cache, the fragment colocation, and the optimistic UI together justify 34KB more than TanStack Query?For many teams, the answer is no. For teams where it is yes, the second question is: are you getting full value from those 47KB? Most are not. They have Apollo Client installed, one
useQuery per component, no field policies, no APQ, and no @cacheControl headers — 47KB of infrastructure generating the same load pattern as plain fetch.A GraphQL client is not a data fetcher — it is a local replica of your server's data graph. Performance optimization starts with the decision to have a local replica at all, and every subsequent optimization (APQ,
@cacheControl, Persisted Operations) protects that replica's query surface on the network.1. Bundle Cost Is the First Performance Decision
1.1 The Real Bundle Comparison
| Client | Size (min+gzip) | Cache Model | Best For |
|---|---|---|---|
@apollo/client | ~47KB | Normalized, entity-level | Graph APIs, optimistic UI, fragment colocation |
@tanstack/react-query | ~13KB | Response-level (per query key) | REST APIs, simple GraphQL, flat data |
urql | ~14KB | Normalized (optional) | Lightweight GraphQL, SSR-first setups |
swr | ~4KB | Response-level | Simple data fetching, no GraphQL features |
The 34KB difference between Apollo Client and TanStack Query is the cost of the replica model. Justify it with at least two of these:
- You use
useFragmentor fragment colocation for component-level data ownership - You use optimistic UI with
optimisticResponseandcache.modify - You consume a federated supergraph where entity-level cache consistency matters
- You need
@deferincremental delivery (requires Apollo Client 3.8+)
typescript
1.2 Import Path Optimization
If Apollo Client is the correct choice, tree-shaking eliminates unused exports — but only if you import from the correct paths:
typescript
Pro Tip & Optimization
Run
npx bundlephobia @apollo/client to see the exact bundle cost before installing. Run npx @next/bundle-analyzer or npx vite-bundle-visualizer after installing to confirm tree-shaking is effective.2. Automatic Persisted Queries (APQ)
GraphQL queries are large strings — a production operation with fragments can easily be 2–5KB. APQ replaces those strings with a SHA-256 hash on repeat requests:
2.1 The APQ Flow
The
GET conversion is the critical benefit. CDNs (Cloudflare, Fastly, Varnish) cache GET requests by URL. A POST request is never cached by CDN — the body isn't part of the cache key. Converting to GET makes the query cacheable.2.2 createPersistedQueryLink Setup
typescript

Expand
3. Making APQ Meaningful: @cacheControl + CDN Headers
APQ alone converts queries to
GET requests. But a GET request without a Cache-Control header is cached for max-age=0 — which means the CDN caches it but immediately considers it stale. Every request still hits the origin.The
@cacheControl directive on schema fields sets the TTL:graphql
Apollo Router propagates the most restrictive
maxAge from the operation's field set to the HTTP Cache-Control: max-age=N response header. The CDN reads this header and caches accordingly.The complete setup:
Crucial Requirement
@cacheControl only applies to public, non-personalized data. User-specific data (profile, cart, orders) must use @cacheControl(maxAge: 0) or scope: PRIVATE. Caching personalized data publicly is a data privacy bug.4. Persisted Operations: APQ Without the Cold-Start Penalty
APQ has one limitation: the first request for any operation pays a two-round-trip cold-start penalty while the server registers the hash. In high-traffic environments, many users may hit this cold start simultaneously after a deployment.
Persisted Operations eliminates the cold start by registering all operations at build time:
bash
typescript
The Apollo Router, with the manifest loaded, rejects any operation whose ID is not in the manifest:
json
This rejection is both a performance win (no expensive query parsing for unknown operations) and a security win (DoS attacks via complex queries are impossible — the router rejects anything not in the manifest before parsing).

Expand
5. Query Discipline
5.1 watchFragment — Fine-Grained Reactive Reads
Apollo Client 3.10+ introduced
watchFragment, a reactive cache subscription that's lighter than useQuery for read-only display components:typescript
watchFragment re-renders only when the specified fragment's fields change in the cache. For high-frequency updates (live counters, presence indicators), this avoids the useQuery re-render overhead for fields not in the fragment.5.2 @skip and @include — Conditional Fields Without Extra Requests
typescript
Architectural Note
@skip and @include are evaluated at the client side before the query is sent — the server only receives the fields that were not skipped or excluded. This reduces response size and resolver execution cost, not just client-side rendering.5.3 BatchHttpLink Revisited — The HTTP/2 Warning
The BatchHttpLink performance advice was introduced in Part 3, but it's worth restating in the context of performance optimization:
typescript
HTTP/2 multiplexes all requests over a single TCP connection concurrently. A
batchInterval: 20ms delay in BatchHttpLink slows down the first request in the batch by 20ms with no connection benefit. On HTTP/2, individual HttpLink requests always outperform BatchHttpLink.Summary
| Concept | Rule |
|---|---|
| Bundle cost | Bundle cost is the first performance decision in any GraphQL client architecture — choosing Apollo over TanStack Query is a 34KB investment that must be justified by the replica model's benefits. |
| APQ and caching | APQ alone does not create meaningful CDN caching — @cacheControl + Cache-Control headers set the cache TTL; without them, APQ produces CDN route hits with max-age=0. |
| Persisted Operations | Persisted Operations is APQ with the cold-start penalty eliminated: the operation manifest is compiled at build time, and the router rejects any query not in it — both a performance and security gain. |
watchFragment | watchFragment (Apollo Client 3.10+) is the correct API for components that need to reactively read a single cache slice — avoids the full useQuery re-render cycle for read-only display components. |
BatchHttpLink | Disable BatchHttpLink in any HTTP/2 environment — multiplexing handles concurrent requests natively; batching adds an artificial delay that degrades time-to-first-byte. |
What's Next
In Part 8, we look at the federated supergraph from the client's perspective — the
@key entity fields that entity stitching requires, the partial data patterns when a subgraph fails, how to read a query plan in Apollo Studio, and how @defer works across subgraph boundaries.Research & Synthesis Note
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.
#GraphQL#Performance#APQ#Persisted Queries#Apollo Client#CDN#Bundle Size
Technical Series
GraphQL Frontend Engineering
Part 7 of 8