Web apps - my mental cheat-sheet
Intro
This article is part of the “Continuous Delivery: HTML to Kubernetes”.
While I can’t wait to get into the nitty gritty details of distributed systems, I found myself in an unpleasant position: I think its best if I start by writing about frontend.
That’s because web apps are the standard nowadays. And it doesn’t matter how many 3000 microservices you have in your ArgoCD-Kubernetes clusters deployed in multiple clouds; those are all irrelevant if users can’t or won’t use your product.
Why in a browser
To say delivering software in the browser has grown in popularity would not serve it justice. Heck, it is nowadays more or less the default for everything. Adobe’s moving frigging Photoshop in the web.
Ok, that’s an extreme example. Nonetheless, web apps have grown in popularity even if we still have the joke about JS' world changing weekly. Despite Javascript’s ecosystem state of the art, we could say.
So the default is now to deliver software over the internet, piggy-banking on the cross-platform-runs-everything-silver-bullet aka browser
.
With great power comes great complexity
But what does that mean, exactly?
- What’s the difference between a “simple site” like Wikipedia and a full-blown web app that can be “installed” on your phone and even send you notifications?
- Or apps that you can interact with while you don’t have an active internet connection?
- How does it differ when it is server-side-rendered or client-side-rendered?
- Is it plain, static html? Or was it generated by a server and just returned?
- Does the browser flicker when you change the page inside the same website?
- Does it behave the same when you access the same URL from inside the web app versus when you hit enter in the browser’s URL bar?
- We can do PWA with SSR… just do it, right? Right?!
A lot of questions, a lot of options. Here’s my mental model - it could serve as a “WebApps - associated complexity & when-to-use cheatsheet”.
The “overall complexity” that you saw in the table is the sum of how complex I think it is to build, maintain, evolve, test and deliver such an app. Not only for the first 3 or 6 months - but in time:
- when requirements change
- when evergreen browsers get updates
- when libraries stop working with the evergreen browsers
- when you need to update said libraries
- when security patches
- when X
- when Y
- when…
The list can go on.
I think there’s no reason (except for CV inflation, maybe) to create a PWA with offline capabilities for an admin interface that needs real-time data anyway. And that’s just one example.
While there are different pros and cons for each of the types of apps, I think it is worth looking into how complex delivering an app is versus what it can do. Versus what it should do.
Please note that my list builds upon itself: the simplest “web app” is a static HTML file, while the most complex is a full-fledged PWA.
WASM is a beast on its own - not detailed here.
Static HTML
The first one on my list is the classic static HTML
. It is how the web started: a simple html
file that is served by a server when a user hits a URL
.
The process is quite simple:
- User enters
site.com
in his/hers URL bar inside a browser. The browser (after someDNS
fun) will make a request to the server. - Server receives the request, handles it, and returns a response: the
html
file. - Browser receives the
html
file, maybe even requests others (css
,js
) that were referenced in the initial file. It reads the whole file(s) and displays the contents. - User sees the information in the browser. Simple, elegant, beautiful.
Behind the scenes, developers working to write & deliver the files to a server looks like this:
The dev process is simple too: just write your html
, css
, js
files, “link” them together and then deploy to your server. No fancy IDEs are needed, and no compiling is necessary.
✅ Pros: easy to develop, no tooling needed, loads fast, compatible with all browsers
⚠️ I’m considering
js
to be light or non-existent. For js-intensive sites, please see my definition of SPA.
❌ Cons: no code re-usability (for things like header, footer, etc), user agnostic (displays the same data for everyone).
❗ Not that cross-browser-compatible if you want the js & css to work the same everywhere :)
Static site generators
For things like code re-usability where the plain static HTML
runs short, the static site generators come in and save the day.
❗The caveat: an extra build step is necessary for the developers before being able to deliver the site.
While request handling looks the same as for the static HTML
:
The dev process requires an extra step:
While this is not the most complex scenario, it is still worth noting that now a simple refresh of the browser would not allow the developer to see the final result. Not without a local server with hot-reload or by running the build manually.
✅ Pros: loads fast, and is scalable in terms of re-usability for static content workloads.
❌ Cons: user agnostic, requires an extra build step (compared with the static html
) before the site can be deployed.
Server-generated HTML
What about sites that need authentication? And to display the logged-in user’s information?
Pure static html
or static site generators
are not going to cut it anymore.
We are now moving to the “real-world” web functionalities, where server
is no longer just a dumb content delivery mechanism.
Server
is now going to refer to a full-fledged machine, that is also capable of processing requests.
From a user perspective, it is exactly the same process as for static websites. However, a more complete description of what’s actually happening is this:
- User enters
site.com
in his/hers URL bar inside a browser. The browser (after someDNS
fun) will make a request to the server. - The
server
, listening for connections, receives a new request. - [Optional] Depending on the application’s logic, it might call other services: database, emails, authentication, etc.
- The server passes the required information to the
templating engine
, which transforms thetemplates
(defined by the devs) +variables
(“passed in” by theserver
) => into anhtml
file. - Browser receives the
html
file, maybe even requests others (css
,js
) that were referenced in the initial file. It reads the whole file(s) and displays the contents. - User sees the information in the browser, not knowing anything of what happened under the hood.
A lot more powerful than the static websites - but also more costly.
It requires way more resources than a simple content-delivery-only server.
It is also more expensive in terms of dev costs: while static html
and even static site generators
’s only requirement is to understand html
, the “knowledge bar” is now higher.
You now need a local dev server
so you can have a feedback loop and test out your changes. Ideally, it is kept in sync and works just as the production one. This adds a ton of complexity.
The build step might seem similar to the one from the static site generators, as you still need to copy a bunch of files in your deployment process; for some server-side languages, though, there’s also an additional compiling step.
Add configuration for the daemon listening for requests - congratz, you now have a complex setup to maintain.
✅ Pros: re-usability (due to the templating engine, similar to static site generators), can return user personalized responses.
❌ Cons: significantly more complex than static responses in terms of dev - deployment - maintenance, requires additional resources for processing requests each time.
Single Page Apps
For the server-side generated html
, for each request, the server needs to do extra work to process it and return the entire html
that will be rendered by the browser.
That means extra work on the server plus a huge response (the entire html
markup) being sent over the (possibly unreliable/slow) network. To top it off, the users see a blank page while the browser loads the new page.
Wouldn’t it be great if we could tackle all of these problems and reduce:
- the time users need to wait for a page to load (not applicable to the first-page load, that might actually be slower)
- the load on the server - by removing the templating and just returning the
dynamic
part of the data - size of the payload that is sent over the network
- the size of a deployment quanta
That’s where SPA come in and solve all those issues mentioned above:
However, that’s not for free. We get extra complexity: in dev time, of course, but, we now have to start considering the implications for the request handling as well.
Let’s look at how a usual SPA gets loaded and is able to process the requests and display data:
- Alice enters
site.com
in her URL bar inside a browser. The browser (after someDNS
fun) will make a request to the server. - The server receives the request, handles it, and returns a response: the
html
file. - In this case, it is safe to consider the initial
html
file returned by the server as an empty shell. It is usually a blank page with an emptydiv
element that will be used by the actual app. - The
html
file references the application code, so the browser will make a new request for theapp.js
file. - The browser receives and starts processing the
js
file. Depending on its size, it might take a while… - Only now do we have a SPA loaded and run by the browser. It might give a clue to Alice that it started to do some work for her (in this case, gather the information needed to display her name).
Client-side hydration
: the app will then make an asynchronous call to theAPI server
(might be the same one that served the static files too, for all we care).- When it gets back the information (“Alice”), the browser can hide the loading indicator and render the page.
- Alice can now interact with the loaded app.
A few notes:
Steps 1 to 6 need to be completed only when Alice first enters the app.
I made a clear distinction between the
dumb content-delivery server
and theAPI server
for clarity - nothing stopping you from setting up a server to serve both static contents and respond to API requests.
❗ Not all SPAs work the same - while this example covers a lot of the work that is being done, it is by no means a complete & accurate example of what happens in a SPA: simply because it depends on how the SPA is implemented. Not to mention the browser cache, the fact that the
app.js
could be split, etc.
Now, for the development & deployment process, things get tricky too:
For the local dev process, the frontend & content delivery server
is most likely handled, along with the build system by the framework & setup of choice.
Just in case I wasn’t clear enough with the extra complexity: there’s already a build step in place for the simple process of having a feedback loop between the files edited by the developer and what the browser displays.
On top of that, more often than not, an additional API server
is required so that the developer can get data for the SPA.
May be still on the local setup, or it may not. The fact remains that it is kinda necessary and another thing to maintain.
Oh, there’s more! Since you are working from the local development server
, additional configuration might be needed only so that they work together. Fun, right?
💡 There are some nice things about it too, though:
If you do separate the two of them (SPA
versus API
), you can deploy them independently - thus, in theory, you should be able to deploy smaller chunks of functionality.
⚠️ There’s a caveat here, too: if you work on a vertical slice (touching both the SPA and the API) you will need to deploy both of them. The good news is that the advantage still applies: first, deploy the API functionality, validate everything is ok, then proceed with the SPA update. This should allow for easier rollbacks, more granular deployments, etc, etc.
To sum it up, for SPAs:
✅ Pros: code re-usability (due to the templating engine, similar to static site generators), can serve user-personalized content, reduces the load on the server
compared to server-generated html
,
small deployment size, good UX after the app loads.
❌ Cons: multiple build systems to maintain, a lot of configuration on both local dev env
and in prod
, content not index-able by the web crawlers (crawlers only see an empty page)
Single Page Apps with Server Side Rendering
SPAs are awesome - as is static html
. That’s what a SPA is, actually, static html
on steroids.
And just as static html
have the static site generators
to solve their shortcomings, welcome to the Single page apps on steroids - Server Side Rendered SPA.
What does that mean? Do we move the whole SPA on the
server
?Not quite - the SSR-SPA still leverages the browser just like the usual SPA, with one “minor” change: at the time of the first request, when for simple SPAs we would have an empty
div
- we now get back from theserver
the wholeHTML
.
The immediate benefits are that web crawlers can now index the pages - and we still keep the same application code as before. On top of that, the user’s browser’s capability of interpreting and running js
is no longer a bottleneck for displaying information - the browser can just display the html
the server-rendered as soon as it receives it.
Sounds awesome, let’s make all our SPA to be server-side-rendered, right? Right?!
Ofc, do as you wish. But please look into the cost as well: for the first paint, the server needs to do some extra work - and handle the generation of the html
that the browser displays right-away.
- Alice enters
site.com
in her URL bar inside a browser. The browser (after someDNS
fun) will make a request to the server. - The server receives the request, handles it, and returns a response: the
html
file. Since it was the first paint request, the server will, under the hood, call the SPA methods and render the full web page that should be displayed for the user. - Server sends back the
html
for the browser to interpret & run, but also adds a reference to theapp.js
- our SPA. - The browser renders the
html
contents - Alice can see her page. For the moment, we can consider the contents of Alice’s browser as purestatic html
. - The browser also starts the request for the
app.js
file. Theserver
will notice that now a static file is being requested, so it will simply return it. - The browser receives and starts processing the
js
file. When done, we can start considering that the switch was made fromstatic html
toSPA
. - Alice can interact with the site as if it was a usual SPA. Let’s say she performs an action by clicking a button.
8 & 9. The app makes an asynchronous request to the server; the server knows that the SPA is fully loaded (information sent via the request headers) and that it can take the short path - returning only the “raw” response, without wrapping it in html
.
- Alice can see (almost instantly, maybe?!) that her operation was successful.
For the build and deployment of the SPA-SSR, things are even darker: we have to say goodbye to the option of choosing our own server-side language. There’s a hard, non-negotiable requirement of “run javascript” on the server side as well. Say “Hi” to node.js, bun, and deno.
We also lose the flexibility of separating the Static Bundle of the SPA from the Dynamic Server API server, because we need to bundle together the server
and the SPA
.
To sum it up:
✅ Pros: SPA on steroids (see SPA
above) in terms of functionality, responsiveness, and speed.
❌ Cons: locks in the server language of choice; bigger, riskier deployments; considerably harder to maintain long-term.
Progressive Web Apps
We’ve seen how strong SPA
& SSR-SPA
are; we’ve also looked at how expensive they are.
What about Progressive Web Apps - PWA? What purpose do they serve?
While they have a lot of power and are quite flexible, I tend to think of PWAs as the “offline web engine”. With the help of a service worker
, we can now intercept the network requests.
Consider how, no matter the type of app we discussed so far, there’s only one common need: to fetch some files from the internet. What if we remove this constraint?
Well, we can now access web apps without the internet.
Wait, without internet?
There’s a catch - you need to have that application
installed
.
Basically, if there’s a PWA you’re interacting with, the first time you load that app, you will be asked (ideally) if you want to install it.
While it looks 99% the same as for the SPA, take a look at the final pieces:
10 & 11. The app notices that it is, in fact, a PWA, not a simple SPA. It asks Alice if she’s ok with installing it.
12 - Alice gives her consent - the service worker
is now registered, and the app is installed.
With an installed app & no internet connection, if Alice tries to use the same app, this is a rough look at what will happen under the hood: the service worker will pretend he is both the API & and Content Delivery Server.
For the deployment & delivery, we have the usual build system where we build
some static files into a bundle
:
But we also have an extra step, an interesting detail: the deployment is no longer a dev-only process. It is noticeable by the user. It can even be allowed & delayed by the user if the app is set to take into account the user’s choice on the matter:
I know most people would disagree with my complexity
score, especially for the SSR vs PWA. Hear me out, though:
-
I am reasoning about PWAs as a non-SSR-SPA with offline capabilities. While I am aware that you can do an
SSR-PWA
, I think it is out of the question how many needs that covers: basically all. But, the complexity score goes to the roof. -
The SSR locks you in and couples the frontend with the backend very tightly. We know how that goes in the long term.
-
While PWA is complex to set up & understand… once you get the hang of it and have a working setup, it tends to just work. A lot of things are handled automagically by the service workers.
-
For the PWA you can opt-in & opt-out of various settings pretty much any time you want. The only risky thing is to mess up the
service worker
’s self-cache/update mechanism. Then you’re kinda screwed. SSR, on the other hand, is an all-or-nothing kind of setup.
✅ Pros: has the same capabilities as a normal SPA + it works when there is no internet connection or an intermittent one, and a lot of extra functionalities that can be configured on a per-need basis.
❌ Cons: complicated to set up initially, requires a more thoughtful approach - your web app is now susceptible to “the fallacies of distributed systems”.
WASM
Delivered as static html
, with a bunch of js
for integrations… and 100.000 other things behind the scenes.
WASM apps deserve a lot more in-depth dive than what I can do in a high-level overview here. Sorry, but I will skip this one for now - I need more knowledge on the matter before I can summarise it as I did for the other types of apps.
Necessary disclaimer
This is my mental model that helps me reason about things. It is in no way a comprehensive guide to all things possible in the web apps world.
However, in my experience, understanding these gets me through my workday quite easily, as I can expand on my knowledge of these basic concepts and then build upon them:
- maybe some parts of an SSR SPA would have client-side hydration - for some reason.
- or maybe we deploy a static blog with comments from a database embedded at build time: the new comments won’t show up until we rebuild & redeploy the website altogether.
The possibilities are endless.
Closing notes
❗For web dev to really make sense, look at the whole lifecycle of an app, from how long it takes to create a new <div>
,
all the way to how users (or web crawlers) will interact with your app.
❗ Understanding all the trade-offs between the type of software that can be run in a browser brings tenfold rewards: you can now do almost anything, on all platforms.
❗I was often able to reduce the challenge into a frontend/backend problem once I was able to understand the requirements: how do users expect to interact with the app and what features must the app support.
❗Understanding trade-offs doesn’t mean only “SPA or PWA with React or Angular - which one is best for my needs”. It means understanding how your app will scale in time: how complex & expensive it will be to develop and, most importantly… deliver it.