Apollo Client Caching
in Depth
Ben Newman
GraphQL Summit
8 November 2018
Why is it so important to have a GraphQL client?
GraphQL has plenty of value if all you do is send raw HTTP requests to a server
Using
curl
:
curl \
-X POST \
-H "Content-Type: application/json" \
--data '{ "query": "{ posts { title } }" }' \
https://1jzxrj179.lp.gql.zone/graphql
Using
curl
:
curl \
-X POST \
-H "Content-Type: application/json" \
--data '{ "query": "{ posts { title } }" }' \
https://1jzxrj179.lp.gql.zone/graphql
{
"data": {
"posts": [{
"title": "Introduction to GraphQL"
}, {
"title": "Welcome to Apollo"
}, {
"title": "Advanced GraphQL"
}]
}
}
Using
fetch
:
await fetch('https://1jzxrj179.lp.gql.zone/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: '{ posts { title } }' }),
}).then(res => res.json())
Try this in your browser console!
Better than REST:
-
One round trip instead of many
-
No overfetching or underfetching
-
One endpoint, endlessly flexible
-
Predictable result structure
-
Enforcement of schema types
-
Schema introspection
-
Developer tools like GraphiQL
Worse than REST?
GraphQL HTTP responses cannot be usefully cached by the browser
-
Exact queries and exact responses, perhaps
-
But nothing more useful than that
Because HTTP caching is mostly useless, GraphQL clients must reimplement caching themselves
And that becomes a huge advantage!
-
Queries that are subsets of cached queries benefit from the cache
-
Queries that are supersets of cached queries can be simplified
-
Queries that are logically equivalent to other queries…
Only the client knows…
-
about @client resolvers
-
about optimistic mutation results
-
about local state written manually to the cache
-
about the contents of localStorage or IndexedDB
Generic HTTP caching will never solve these problems!
The more domain-specific your caching system is, the more efficient it can be
GraphQL gives us so many powerful tools to tame complexity…
Resolvers combine existing data sources while hiding the sins of past data models
Schemas elevate raw data by enforcing application-level object types and relationships
But resolvers can also sow chaos
But resolvers can also s̵̹͘o̵̓w̶̗͌ c̴̜̐h̸͎͜͝ä̷̮̻̳̟o̶̭̼̩̠̓̎́s̶͓͔̹̪͝
Chaos in point: FakerQL
Schemas impose useful constraints, but they do not guarantee your graph is stable or coherent
The perils of mocking
const typeDefs = gql`
type Query {
hello: String,
document: Document
}
type Document {
id: String!
currentVersion: Version!
}
type Version {
id: String!
downloadUrl: String!
editors: [User]
}
type User {
id: String!
name: String!
}
`;
const resolvers = {
Query: {
hello: () => 'Hello world!',
document: () => {
return {
id: 'foo'
}
}
},
Document: {
currentVersion: async () => ({
id: uuid()
})
},
Version: {
downloadUrl: () => 'http://example.com',
editors: () => [{
id: '1',
name: 'Foo Bar'
}, {
id: '2',
name: 'Bar Baz'
}]
}
};
The perils of mocking
const documentQuery = gql`
query {
document {
id
currentVersion {
id
downloadUrl
}
}
}
`;
const editorsQuery = gql`
query {
document {
id
currentVersion {
id
editors {
name
}
}
}
}
`;
const resolvers = {
Query: {
hello: () => 'Hello world!',
document: () => {
return {
id: 'foo'
}
}
},
Document: {
currentVersion: async () => ({
id: uuid()
})
},
Version: {
downloadUrl: () => 'http://example.com',
editors: () => [{
id: '1',
name: 'Foo Bar'
}, {
id: '2',
name: 'Bar Baz'
}]
}
};
🤔
💭
🍌
If query results can be different any time a resolver is called, how can it be safe to cache query results on the client?
If you knew your GraphQL endpoint behaved like FakerQL, how could you justify caching anything?
If you knew your GraphQL endpoint might behave like FakerQL, how could you justify caching anything?
Crucial insight:
The job of the cache is not to predict what the server would say if you refetched a given query right now
The job of the cache is to ingest previously received query result trees into an internal format that resembles your data graph
Image credit: Dhaivat Pandya
The job of the cache is to ingest previously received query result trees into an internal format that resembles your data graph
So that logically related (structurally similar) queries mutually benefit from the cache
The job of the cache is to ingest previously received query result trees into an internal format that resembles your data graph
And active queries can be updated automatically when new data arrive, based on logical dependencies
The job of the cache is to ingest previously received query result trees into an internal format that resembles your data graph
Further reading: GraphQL Concepts Visualized
Apollo DevTools
Once previous results are decomposed into this normalized graph structure, the cache can respond to any variation of a previous query, as long as the data are available
That flexibility remains valuable even if some of the data are slightly stale
The decision to discard or continue using “stale” data ultimately belongs to the application
Forcing data to remain always up-to-date eventually becomes a performance liability
Solutions to staleness should prioritize data that matter most to the user
Caching based on the structure of data rather than the syntax of queries allows the data fetching load of your application to grow with the amount of data needed, not the number of queries
The GraphQL client cache is the hub of that coordination
Which cache implementation should you use?
Anyone can implement the ApolloCache
interface, and they have!
What aspects of client cache performance really matter?
-
Avoiding network round-trips
-
Avoiding unnecessary re-rendering
-
Efficiently updating watched queries when new data become available
-
Memory footprint
This cache maintains an immutable & normalized graph of the values received from your GraphQL server. It enables the cache to return direct references to the cache, in order to satisfy queries. As a result, reads from the cache require minimal work (and can be optimized to constant time lookups in some cases). The tradeoff is that rather than receiving only the fields selected by a GraphQL query, there may be additional fields.
See yesterday's talk by Ian MacLeod for more details!
That tweet
Bouncing back from a distant last place, the @apollographql
InMemoryCache
is now neck-and-neck with Hermes on @convoyteam 's own benchmark suite, which means the two fastest #GraphQL caching systems are both implementations of theApolloCache
interface:
How?
How?
Ways to make existing software faster:
-
Stop doing unnecessary work
-
Do the same work faster
-
Parallelize the work
-
Prioritize work that matters now
-
Reuse results of previous work
-
Lower your standards?
Caching challenges
-
Staleness
- What's the shelf life of a computation?
-
Sameness
- When might the results of superficially different operations actually be the same?
-
Creepiness
-
Results worm their way into other parts of the system in spooky and indirect ways
-
-
Worthwhile-ness
- Does the caching actually pay for itself?
A cache for a cache?
When readQuery
is called, the cache must extract a tree-shaped query result from the normalized graph
Believe it or not, this is one of the most performance-sensitive parts of the cache!
Especially because
broadcastQueries
re-reads every watched query after any update
A cache for a cache?
What if we reused these result objects, instead of computing them from scratch every time?
Initial reads would not be any faster, but repeated reads could be nearly instantaneous!
Calling broadcastQueries
with lots of query observers would be a lot less expensive
Easy example: data that never go stale
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
Easy example: data that never go stale
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
Easy example: data that never go stale
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
console.log(fib(8)); // 21
Easy example: data that never go stale
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
Easy example: data that never go stale
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
console.log(fib(10)); // 55
Easy example: data that never go stale
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
console.log(fib(10)); // 55
console.log(fib(78)); // ???
Easy example: data that never go stale
import { wrap } from "optimism"
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
console.log(fib(10)); // 55
console.log(fib(78)); // ???
Easy example: data that never go stale
import { wrap } from "optimism"
const fib = wrap(function (n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
});
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
console.log(fib(10)); // 55
console.log(fib(78)); // ???
Easy example: data that never go stale
import { wrap } from "optimism"
const fib = wrap(function (n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
});
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
console.log(fib(10)); // 55
console.log(fib(78)); // 8944394323791464
Less trivial: data that change without warning
function hashDirectory(dirPath) {
const hashesByFile = {};
readDirectory(dirPath).forEach(file => {
const absPath = path.join(dirPath, file);
if (isFile(absPath)) {
hashesByFile[file] = hashFile(absPath);
} else if (isDirectory(absPath)) {
hashesByFile[file] = hashDirectory(absPath);
}
});
return crypto.createHash("sha1")
.update(JSON.stringify(hashesByFile))
.digest("hex");
}
Less trivial: data that change without warning
import { wrap } from "optimism"
function hashDirectory(dirPath) {
const hashesByFile = {};
readDirectory(dirPath).forEach(file => {
const absPath = path.join(dirPath, file);
if (isFile(absPath)) {
hashesByFile[file] = hashFile(absPath);
} else if (isDirectory(absPath)) {
hashesByFile[file] = hashDirectory(absPath);
}
});
return crypto.createHash("sha1")
.update(JSON.stringify(hashesByFile))
.digest("hex");
}
Less trivial: data that change without warning
import { wrap } from "optimism"
const hashDirectory = wrap(function (dirPath) {
const hashesByFile = {};
readDirectory(dirPath).forEach(file => {
const absPath = path.join(dirPath, file);
if (isFile(absPath)) {
hashesByFile[file] = hashFile(absPath);
} else if (isDirectory(absPath)) {
hashesByFile[file] = hashDirectory(absPath);
}
});
return crypto.createHash("sha1")
.update(JSON.stringify(hashesByFile))
.digest("hex");
});
What now??
The
optimism
API
import { wrap, defaultMakeCacheKey } from "optimism"
The
optimism
API
import { wrap, defaultMakeCacheKey } from "optimism"
const cachedFunction = wrap(function originalFunction(a, b) {
// Original function body...
}, {
});
The
optimism
API
import { wrap, defaultMakeCacheKey } from "optimism"
const cachedFunction = wrap(function originalFunction(a, b) {
// Original function body...
}, {
// Maximum number of cached values to retain.
max: Math.pow(2, 16),
});
The
optimism
API
import { wrap, defaultMakeCacheKey } from "optimism"
const cachedFunction = wrap(function originalFunction(a, b) {
// Original function body...
}, {
// Maximum number of cached values to retain.
max: Math.pow(2, 16),
// Optional function that can be used to simplify (or reduce
// dimensionality) of the input arguments.
makeCacheKey(a, b) {
// Return a value that could be used as a key in a Map.
// Returning nothing (undefined) forces the actual function
// to be called, skipping all caching logic.
// The default behavior works even if a or b are objects:
return defaultMakeCacheKey(a, b);
},
});
The
optimism
API
import { wrap, defaultMakeCacheKey } from "optimism"
const cachedFunction = wrap(function originalFunction(a, b) {
// Original function body...
}, {
// Maximum number of cached values to retain.
max: Math.pow(2, 16),
// Optional function that can be used to simplify (or reduce
// dimensionality) of the input arguments.
makeCacheKey(a, b) {
// Return a value that could be used as a key in a Map.
// Returning nothing (undefined) forces the actual function
// to be called, skipping all caching logic.
// The default behavior works even if a or b are objects:
return defaultMakeCacheKey(a, b);
},
// Optional function that can be used to observe changes that
// might affect the validity of the cached value.
subscribe(a, b) {
const watcher = fs.watch(a, () => {
// Invalidates the cached value for these arguments, after
// transforming them with makeCacheKey. Idempotent.
cachedFunction.dirty(a, b);
};
return () => watcher.close();
}
});
Once more, with optimism
function readDirectory(dir) {
return fs.readdirSync(dir);
}
Once more, with optimism
function readDirectory(dir) {
return fs.readdirSync(dir);
}
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
Once more, with optimism
function readDirectory(dir) {
return fs.readdirSync(dir);
}
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
Once more, with optimism
function readDirectory(dir) {
return fs.readdirSync(dir);
}
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
Once more, with optimism
import { wrap } from "optimism"
function readDirectory(dir) {
return fs.readdirSync(dir);
}
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
Once more, with optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
});
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
Once more, with optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
}
});
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
Once more, with optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
const watcher = fs.watch(dir, () => {
readDirectory.dirty(dir);
});
}
});
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
Once more, with optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
const watcher = fs.watch(dir, () => {
readDirectory.dirty(dir);
});
return () => watcher.close();
}
});
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
Once more, with optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
const watcher = fs.watch(dir, () => {
readDirectory.dirty(dir);
});
return () => watcher.close();
}
});
const isDirectory = wrap(path => {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}, {
subscribe(path) { ... }
});
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
Once more, with optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
const watcher = fs.watch(dir, () => {
readDirectory.dirty(dir);
});
return () => watcher.close();
}
});
const isDirectory = wrap(path => {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}, {
subscribe(path) { ... }
});
const isFile = wrap(path => {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}, {
subscribe(path) {
const watcher = fs.watch(path, () => {
isFile.dirty(path);
});
return () => watcher.close();
}
});
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
const watcher = fs.watch(dir, () => {
readDirectory.dirty(dir);
});
return () => watcher.close();
}
});
const isDirectory = wrap(path => {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}, {
subscribe(path) { ... }
});
const isFile = wrap(path => {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}, {
subscribe(path) {
const watcher = fs.watch(path, () => {
isFile.dirty(path);
});
return () => watcher.close();
}
});
const hashFile = wrap(path => {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}, {
subscribe(path) { ... }
});
Once more, with
optimism
What happens when hashFile.dirty(path)
is called?
How does this apply to apollo-cache-inmemory
?
export class DepTrackingCache implements NormalizedCache {
How does this apply to apollo-cache-inmemory
?
import { wrap } from "optimism";
export class DepTrackingCache implements NormalizedCache {
How does this apply to apollo-cache-inmemory
?
import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
How does this apply to apollo-cache-inmemory
?
import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
How does this apply to apollo-cache-inmemory
?
import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
}
How does this apply to apollo-cache-inmemory
?
import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
How does this apply to apollo-cache-inmemory
?
import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
public get(dataId: string): StoreObject {
this.depend(dataId);
return this.data[dataId];
}
How does this apply to apollo-cache-inmemory
?
import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
public set(dataId: string, value: StoreObject) {
const oldValue = this.data[dataId];
if (value !== oldValue) {
this.data[dataId] = value;
this.depend.dirty(dataId);
}
}
How does this apply to apollo-cache-inmemory
?
import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
public delete(dataId: string): void {
if (Object.prototype.hasOwnProperty.call(this.data, dataId)) {
delete this.data[dataId];
this.depend.dirty(dataId);
}
}
How does this apply to
apollo-cache-inmemory
?
import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
Nothing changes in a
DepTrackingCache
without these methods being called
How does this apply to
apollo-cache-inmemory
?
import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
Which makes DepTrackingCache
an extremely convenient bottleneck for dependency tracking
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
private executeSelectionSet({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions): ExecResult {
const {
fragmentMap,
contextValue,
variableValues: variables,
} = execContext;
const object: StoreObject = contextValue.store.get(rootValue.id);
const finalResult: ExecResult = {
result: {},
};
// Recursively populate finalResult.result...
return finalResult;
}
How does this apply to apollo-cache-inmemory
?
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
}
How does this apply to apollo-cache-inmemory
?
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
}
How does this apply to apollo-cache-inmemory
?
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
});
}
How does this apply to apollo-cache-inmemory
?
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
}
});
}
How does this apply to apollo-cache-inmemory
?
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
);
}
});
}
How does this apply to apollo-cache-inmemory
?
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
execContext.contextValue.store,
);
}
});
}
How does this apply to apollo-cache-inmemory
?
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
execContext.contextValue.store,
execContext.query,
);
}
});
}
How does this apply to apollo-cache-inmemory
?
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
execContext.contextValue.store,
execContext.query,
selectionSet,
);
}
});
}
How does this apply to apollo-cache-inmemory
?
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
execContext.contextValue.store,
execContext.query,
selectionSet,
JSON.stringify(execContext.variableValues),
);
}
});
}
How does this apply to apollo-cache-inmemory
?
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
execContext.contextValue.store,
execContext.query,
selectionSet,
JSON.stringify(execContext.variableValues),
rootValue.id,
);
}
});
}
How does this apply to apollo-cache-inmemory
?
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
if (execContext.contextValue.store instanceof DepTrackingCache) {
return defaultMakeCacheKey(
execContext.contextValue.store,
execContext.query,
selectionSet,
JSON.stringify(execContext.variableValues),
rootValue.id,
);
}
}
});
}
How does this apply to apollo-cache-inmemory
?
Other internal optimizations
-
Avoiding side effects in internal APIs, so partial results can be cached
-
Caching
executeSelectionSet
but notexecuteField
-
Identical cache keys for equivalent (sub)queries (
QueryKeyMaker
)
Application-level optimizations 🤝
-
Take advantage of
===
equality-
React.PureComponent
orReact.memo
orshouldComponentUpdate
can help -
If you must modify result objects from the cache, be sure to write them back immediately
-
-
Avoid using an unbounded number of query documents
- Internal optimizations rely on one-time preprocessing of query documents
Future plans 🧙♀️🔮
-
Stability!
- Please keep updating apollo-cache-inmemory and related packages whenever there's a new version
- We're not going to make any more huge changes until the dust has completely settled
-
Broadcasting queries 📡 needs more work, but the 🔥 has been put out 🚒
-
Invalidation 🙅♀️, garbage collection ♻️, deletion ✂️ (3 sides of the same coin)
-
Documentation for optimism
thank’s
Apollo Client Caching in Depth
By Ben Newman
Apollo Client Caching in Depth
- 3,179