How to save users from their own network connection

20 December 2023

The dilemma

Consider you have an app. It could be a web app, a native mobile app, or a desktop app. If you have a web app, we are going to assume the app is already client-side rendered and does not need to load from the server every time it is opened, as it is cached in a service worker. I talk about this kind of web app and its tradeoffs more in this article.

Setting that aside, whenever you have some data to store or some computation to do you have a critical decision:

Server-side or client-side

This decision is not just for web apps, but also for mobile apps and desktop apps, as they also might need to use cloud services. In this article, I justify a particular rule of thumb to use for making these decisions, the exceptions to this rule, and some general development tips which fit well with this rule.

Do things on the client-side if you can, and on the server-side if you must

The network connection

When you fetch data over the network, you are passing over a gigantic void of unpredictability, which you cannot control. Your users’ devices are no longer tethered to a relatively stable wired connection. Many users will be using WiFi or cellular connections, and sometimes these connections can grow slow, congested or drop out completely. Users no longer have the patience to wait for slow network connections; we all know the three-second statistic.

The solution is to do everything that can be done on the client-side on the client. Here are some examples:

UsageClient/Server
Settings for a single deviceClient, only needed on that device
Social Media postServer, needs to be stored centrally so many users can access it
QR code scanClient, most devices are powerful enough to run QR code scanning code without needing cloud support
Access controlServer, the client side is not a trusted environment and therefore cannot perform access control in a trustworthy way
Running an LLM

When a network connection is faster than doing the computation on the client

Consider the last example in the table. With our rule, we would run it on the client, as technically it can be run on the client, but on most devices it would be far faster to run it on a server and wait for the dreaded network connection. If, for a significant proportion of your users, it is faster to do a computation on the server than on the client, then its best to do it on the server if possible. Another approach if this proportion is not your entire user population, is to start the operation on the client and server in parallel, and when one finishes abort the other.

How to make the network more bearable

Sometimes you have to fetch across the network, like accessing a piece of shared data or something that needs access control. When this happens, we can still make some optimisations to make it feel more user-friendly. Here are some things you can do to improve your user experience for fetching server-side data:

  • Optimistic updates
    • After making a change to server side state, update it on the client side assuming it succeeded and if it fails then revert it.
  • Persistent caching
    • Cache server-side data between page loads, so data is shown immediately (state data is better than no data)
  • Periodic sync
    • When offline, queue up changes to be made to the server-side and sync this in the background when back online
  • Validation
    • Validate data to avoid undefined behaviour when invalid data is given, either because of an actual bug or a proxy (like on school or corporate networks)
  • Graceful error handling
    • Keep showing stale data even when an error occurs so the user can work with the stale data until the issues stop
  • Exponential backoff on retries
    • If an error is temporary it might resolve itself after some period of time, so keep trying a certain number of times. Use exponential backoff to avoid putting unnecessary load on the server.

This is not easy to implement yourself. You may want to use a data-fetching library, sometimes called an async state manager. This library will be responsible for syncing server-side state with a client-side “store”, which your app can deal with, so you don’t need to worry about the network as much. I personally use TanStack Query for web apps, and I’ve heard good things about SWR. I don’t currently have any recommendations for native. Also for things like periodic sync and persistence you may need to do some extra configuration. For validation of network requests on the Web, I use zod.