-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Avoid costly cloneDeep snapshots when cache results are known to be immutable. #4543
Conversation
Creating the QueryManager lazily has limited value, since almost nothing ApolloClient needs to do can happen without the QueryManager. This change cuts 183 bytes from the minified size of the package (60 bytes after gzip).
Part of the plan I outlined in this comment: #4464 (comment) Passing { assumeImmutableResults: true } to the ApolloClient constructor should probably always be accompanied by passing { freezeResults: true } to the InMemoryCache constructor (see #4514), though of course the use of InMemoryCache is optional, and other cache implementations may not support that option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks awesome @benjamn!
@@ -72,19 +73,16 @@ export default class ApolloClient<TCacheShape> implements DataProxy { | |||
public link: ApolloLink; | |||
public store: DataStore<TCacheShape>; | |||
public cache: ApolloCache<TCacheShape>; | |||
public queryManager: QueryManager<TCacheShape> | undefined; | |||
public readonly queryManager: QueryManager<TCacheShape>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm really happy to see this, but do you think we need to worry about this potentially impacting backcompat? People really shouldn't be setting their own queryManager
, but we've unfortunately allowed it for a while.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danilobuerger Any thoughts on how risky adding readonly
might be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What @hwillson said, they shouldn't be setting it, but they might have. But since its just a typing change, its still settable if someone really wanted to. So I don't see an issue with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After sleeping on it (and doing a code search for initQueryManager
and queryManager
), here's my assessment of the use cases:
- By far the most common use case for calling
client.initQueryManager()
is simply to ensureclient.queryManager
exists so it can be examined before the first query happens (e.g. to save a reference toclient.queryManager.queryStore
). These changes render that effort unnecessary. - Mistakenly calling
initQueryManager
in an attempt to reset client state. This still works as well as it ever did, becauseinitQueryManager
was previously idempotent, and now does nothing. - Hypothetical use cases that I haven't actually found in the wild:
- Setting
client.queryManager = null
and later callingclient.initQueryManager()
to recreate it. This no longer works becauseinitQueryManager
won't recreate theQueryManager
now, so addingreadonly
toclient.queryManager
seems like a helpful signal to reconsider this pattern. - Saving
client.queryManager
somewhere, setting it tonull
temporarily, and then restoring it to the old value later. This still works, as long as you circumvent thereadonly
restriction withclient as any
, which seems like a reasonable "I know what I'm doing" requirement.
- Setting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good assessment, the only other thing I could come up with is mocking it away for some reason in tests. (However, I don't know why one would want to do that)
This PR sounds fantastic @benjamn ! Could we get a snapshot release of this to get more eyes on testing the performance impact? |
apollographql/apollo-client#4543 * Enable strict mode in all scripts
This commit allows the default value of the `assumeImmutableResults` option to be determined by the cache (any implementation of `ApolloCache`), which allows our implementation of `InMemoryCache` to express the safety of assuming `assumeImmutableResults` is `true`, unlocking significant performance savings (fewer defensive deep copies of query results), even if `assumeImmutableResults` is not configured. Related past PRs: - #4543 - #5153 - #9680 - [Apollo Client 2.6 blog post](https://www.apollographql.com/blog/announcement/frontend/whats-new-in-apollo-client-2-6/#rewarding-immutability)
The
cloneDeep
helper fromapollo-utilities
has begun showing up prominently in some performance profiles (#4464), because we were forced to start taking snapshots of previous results to guard against destructive modifications (#4069).While we cannot mandate immutable data handling for all application code that interacts with Apollo Client, we can give developers the tools to eliminate the runtime cost of using
cloneDeep
, which is incurred to defend against destructive modifications of cache results. If we don't have to worry about destructive updates, we can skip callingcloneDeep
. Some cooperation from the application developer is required to achieve this goal, and that's what this PR enables.This new (highly recommended) strategy has two parts:
Thanks to Support InMemoryCache({ freezeResults: true }) to help enforce immutability. #4514, when you create an instance of
InMemoryCache
, you can instruct it to callObject.freeze
(in development only) against all the result objects that it creates, which effectively prevents any destructive mutations of those objects. If you have been relying on mutability of cache results, you may need to update some of your code, but thefreezeResults
option should help you find the problematic spots, because modifications of frozen objects throw an exception (in strict mode). Since the freezing happens only in development, this new functionality has zero runtime cost in production.Once you know that your application code is no longer modifying cache results destructively, you can unlock significant performance improvements by passing
assumeImmutableResults: true
to theApolloClient
constructor. Once you have agreed not to modify cache results, the client can avoid usingcloneDeep
to take snapshots of previous results, and identical result (sub)trees will often be===
to each other, so your UI rendering code can immediately detect when no changes have happened.What do we mean by destructive modifications? For example, the following code destructively modifies a result object read from the cache:
While this works, Apollo Client has to do a lot of extra work to compensate for the possibility that nested result data might change at any time.
To rewrite this code in a non-destructive style, you should create a new
data.todos
list that includes the old todos plusmyNewTodo
:Both styles will work, but only the second style is compatible with the
{ assumeImmutableResults: true }
option. When you're usingnew InMemoryCache({ freezeResults })
, thedata.todos.push(myNewTodo)
expression will throw an exception in development (in strict mode), because thedata.todos
object will be frozen.