
Can your database do this? Ep. 1: Magic caching
Let's talk about Jank. In this case, application jank. So, say you are developing a web app as I am here. This one is using Next.js and it represents kind of like how long you have to wait at the Department of Motor Vehicles. "You'll wait." So we've got this clock up here and then we've got these users who each are in a different position in a queue. They're each waiting, they have some wait time they've been given. Now, this application is backed by a Convex deployment. Convex is a reactive database and backend as a service that stores your data and sends it to all your apps in a way that's pretty cool. So because it is reactive we can come in here, I think right now Greta is right around 6:40, is the pickup time if we make Greta wait two more minutes we should see that minute hand jump forward just a little bit. And it does. So reactive database at work. One more change let's take Karen and Karen is now–tada–"Kari." Good, everything working. Everything hooked up, database-backed system, nice iteration times, everything's great, right? Not quite. As you might see here as we switch between these different users in this users table there is "jank." It re-draws the whole thing and because we're using Next router here this stuff is all going into the browser history so if we hit back and forward you can see kind of infuriatingly we are redrawing the page when we hit back and it feels like that just shouldn't happen, right? We just loaded that value so why aren't we showing again right away? So we want to fix the jank. So what are we going to do about that? Well I'm guessing that you might be thinking some kind of cache. And yeah the answer is: it is some kind of cache, but this one might be a little different than you're used to. This one has a little trick up its sleeve that's pretty interesting and that reveals something kind of specific about Convex that many users find useful. Let's dive in and take a look at how we achieve it. So what I'm going to do is I'm going to make exactly two changes to my app. The first thing I'm going to do is I'm going to go into my layout where I've got my providers in Next.js and I'm going to add a ConvexQueryCacheProvider. In doing that I'm making sure there's a context in my React app that has a cache in it so that all of my components have access to this cache when they are issuing Convex queries. Then, inside of the page itself I've got these useQuery calls. This is the time from the DMV, this is the list of the users, and then if we're on a page with a user I load that user using the URL parameters. So, I've got these useQueries, these are being automatically updated because of the subscriptions. But instead of using the standard one from "convex/react" I'm just going to replace these with the useQuery from the corresponding caching library that also had that provider. And that's it. Two changes, and everything automatically deploys. When I come over here take a look at my app after my initial load, there we go, we're just reusing the values, the cache is working. And if I go back... yep everything's good. I can hit the back button and uh everything reloads instantly so the jank is gone. We did it. Good job! So you might be saying to yourself "yeah, I know, I've seen this before. Lots of the data loading request libraries have a cache in them. This isn't that big of a deal." And you're right. But this one does have something a little different about it that we'll dive into now. Although it seems like this may be just a standard cache it's actually not caching values. It is caching subscriptions. So... right now I'm looking at Greta's time and if I go and change it... right now I think it's a seven minute wait time. Yeah. We change that, let's move her back to five minutes, let's be nice. So the reactivity is still working of course. But what you might be surprised to see is that... Greta is at 6:40. If we go over to Sujay's page and we update Greta's time in the background (we can move it 2 minutes forward again in the database) in the app the cached value will have updated. We have cached these subscriptions to data and so we're continuing to get these consistent bunches of data when transactions happen that are pushed back out to our app. Even for things that we're not currently rendering. That way when we use this cache everything is still consistent. That's pretty cool, that is kind of the "just works" sort of experience you would expect where you don't want to trade off performance for correctness, you don't want to deal with invalidating caches yourself and things like that. Just to really bring home how pervasive this is let's do one more change to this app. So, let's set up a cron job because in this app this DMV time is in this other table here I haven't shown you yet. And it's just been sitting here static, so nothing is happening. Everyone's waiting forever, the DMV time is is still. And time does move forward. Let's see if we can fix this. So inside of our app, everything in a Convex project is just regular ol' TypeScript including cron job specifications. I'm going to add a cron job to this "crons" object that run that I'm going to call... let's see... let's call it "update dmv time" and then I'm going to make it happen every 3 seconds, and it's going to call a convex mutation (which is how we change data in Convex reactive databases) called updateTime. So update the time every 3 seconds. Convex automatically deploys in the background, so we should see this secondhand start ticking... there it is, yep. 3 seconds: tick. 3 seconds: tick. We can even see that on the dashboard here, which is also subscribed to the value it goes green and these guys jump and the cron job is updating the time in the background. our query that is returning the arrival time for all of our users is based on this time and then adding the interval to it. Once again all queries in Convex are also TypeScript. So that looks a little like this. We get the user object we get the time object and we add them together and return that. So all right we got this going now watch what happens. You'll notice the DMV time is ticking forward and so are the cache values, right? Those subscriptions are continuing to pull forward new values in consistent transactional windows as the background data changes. So we haven't merely cached a stale value that we can use to render, and then we have to go poll and maybe we invalidate. We've actually just been getting streams of updates pushed to us in the background and I can use the back and forward buttons and rapidly switch–it's kind of trippy, right? I'll go forward and backward and you'll see all of their pickup times change a bit, but the second hand is the same for all of them. It keeps ticking along with the DMV time. In summary, we used a technique here to with this caching library to leverage the fact that these subscriptions are really powerful in Convex, to cache the subscriptions instead of just the values. And because of that we were able to have an app that loads very quickly but it doesn't have a lot of the consistency issues and manual cache invalidation issues you have with traditional databases and backends. This is one of the many really powerful techniques that Convex opens up, new architectures it enables. If you haven't tried it yet, go to convex.dev and give it a shot. And thanks for watching.
Check out the ConvexQueryCacheProvider in the convex-helpers npm package.
npm i convex-helpers
1<ConvexClientProvider>
2 <ConvexQueryCacheProvider>
3 {children}
4 </ConvexQueryCacheProvider>
5</ConvexClientProvider>
6Convex is the backend platform with everything you need to build your full-stack AI project. Cloud functions, a database, file storage, scheduling, workflow, vector search, and realtime updates fit together seamlessly.