Using Misdirection to Make a Local-First App Load Fast
It's cheating, but I think it's mostly OK...
I'm somewhat of a proud parent of my notepad calculator app NumPad, it's been a passion project of mine for years now, and I'm often surprised and impressed by the calculations it can take in its stride. This has downsides. Like many proud parents, my pride has caused me to turn a blind eye to the fact NumPad has got quite bloated as it's matured.
Unfortunately, as a local-first app, quite a lot of this heft is unavoidable. NumPad uses Automerge to persist data and sync it to the server, and React-pdf to export documents to PDF—both clock in at over 1MB—but I think they're both important. The good news is that, after a user's first visit, everything gets loaded from a service worker with no perceptible delay, but first impressions are important, so I wanted to do something about that dismal initial loading time.
The answer I settled on kills two birds with one stone. At the time I set out on my mission to slim NumPad down, the app had no homepage. Users were just dumped into the calculator app with a big editable document that showcased all NumPad's features, and expected to work out what they were there for. Personally I quite like this approach, but some people seemed to hate it, and I eventually talked myself around to building a homepage when I realised it could really drastically cut loading times.
But first a quick diversion: this trick only works if you're willing to have your app and your homepage on the same subdomain. It's a commonly-held belief now that this is a lost technology, like Greek Fire or Damascus Steel. But I promise, if you have enough resolve, it's a thing you can do. Your app doesn't have to live at app.yourdomain.com
, you don't have to show signed-in users a generic homepage so that they have to click "Sign in" before you show them the page they actually want. You don't have to make a mockery of my URL bar's autocomplete. There's a better way.
OK, rant over (for now). The trick I ended up using is to load only the homepage when a user first visits numpad.io, and then use dynamic imports to load the rest of the app in the background. By the time a user has finished poking around on the homepage, and clicks through to the app, it's very likely to have already loaded all the JavaScript it needs, and saved it to a service worker for the next page load. This means—as long as the homepage loads fast—a user never has to experience painful loading times.
Downsides
The elephant in the room here is that we're downloading a pretty heavy app in the background when the user hasn't actually asked for it. If the user does click through to the web app, we're absolved of our sins, because their download is no bigger but their load time appeared much faster. But if the user doesn't, ever click through, then I've stolen (relatively) precious megabytes from them, which may or may not be a problem depending on their data situation.
I don't feel great about that to be honest. I find myself wanting to say "but have you seen [popular website]? They load up megabytes of crap without asking their users, and most of that crap is adware that makes the user experience worse!", and obviously that's true. But it's an incredibly low bar to clear and I don't think anyone should feel too proud about clearing it.
I'm keeping this solution for now. For users on the happy path it really is better. I think the only way of making it better for other users is probably to actually find some way of slimming the app down, or possibly doing some overly-clever code splitting to allow users to use the calculator app without loading Automerge. I suspect the latter option would be a complexity nightmare though.
Hot Rodding
The homepage is rendered with JavaScript, so the user still has to wait on some JavaScript (including React) to load before it can render anything. I was quite tempted to inline and hide all the homepage's HTML and CSS to really speed up load times. To avoid a unsolicited flash of homepage for signed-in users I'd need to hide the body with CSS and use some inline JavaScript to check the page route and whether the user is signed in (user credentials are stored locally) before unhiding the page. To be honest I had to stop myself from doing this, because I'd already spent too long working on the homepage, but I think it'll be worth doing at some point.