Zero-Blocking GETs
This is part 7 of a 12-part series called Distilling the Web to Zero. The web is far from done improving - particularly as it pertains to building rich web applications that are, to users, developers, and businesses alike, more desirable than their native-app counterparts. This series is prefaced by defining the biggest challenges on the road towards that goal and the 12 essays that follow explore potential solutions using concrete examples.
Zero-blocking GETs
"A good plan, violently executed now, is better than a perfect plan next week."
– General George Patton
The power of reversible decisions
Amazon is famous for its management style which celebrates "decision velocity." It explains that waiting until you have all the information before you make a decision often leads to analysis paralysis. Most decisions should probably be made with roughly 70% of the information you wish you had.
This system only works, however, if decisions are reversible. Otherwise you might be locked into a poor decision forever like a bad game of Tetris where you dropped a piece, missed by one square, and suffered from it for the rest of the game.
web2 adopts a very irreversible ethos to UI. When a GET
comes in (whether for HTML or for JSON), the response stream must sit there and wait until a mountain of data can be pulled in and assembled into a response that’s finally flushed to the browser. Such a strategy ends up being only as fast as your slowest horse.
Such constraints have lengthy ripple effects on the final form of any app’s UI.
Your environment shapes you
HTTP’s GET
method has influenced the literal shape of webapps – specifically navigation.
For native apps, Apple recommends using a stack-based scheme for navigating hierarchical content. This model is well suited for smaller screens and creates a feeling of simplicity and approachability since each screen can focus its full real estate to only one task at a time without distractions. This model is simple enough such that each row’s text can become the next screen’s title while the back button text can become the previous screen’s title. It’s familiar, predictable, the user knows what to expect every time which subconsciously teaches users they never need to hesitate about navigating deeper. The smooth animations keep them connected to their content at all times and most importantly, there’s no waiting, reloading, starting over, or losing your position between each level.
Meanwhile, on the web, a different pattern has emerged. Users have subconsciously learned to hesitate before clicking links. It’s unpredictable what might happen. Will the page simply shift around or will they be disconnected from their task and forced to wait for a new page to load? Will the back button return them to where they left off or will they need to scroll around to find it again? Will this reset other micro-tasks like form elements or paginated content? This lack of predictability has steered the UI used for webapps down a very different path where the web’s best option is to “just scroll more.” This is shallow navigation instead of hierarchical navigation.
The problem is that this path has led to a vicious cycle. As webpages become heavier and heavier, GET
s become slower and slower which exacerbates the problem with link-hesitation in the first place. Some try mitigating this problem by loading partial content asynchronously but that only serves to treat the symptoms, not the cause. Not to mention this core issue is only magnified when operating from The Edge.
The truth is that webapps suffer from shallow navigation. It causes users to perceive webapps as somehow always slower, clunkier, and more difficult to use than a native app. The only reason shallow navigation tests better with users is because users are loath to ever navigate away from their current page. Fixing the root cause so that webapps can perform on the same level as native apps requires fixing the way browsers receive their data – GET
s must become instant. Much like the “decision velocity” example above, there’s only one way to accomplish this, HTTP must become a reversible protocol.
Unblocking the web
If GET
s could feel instant, we’d start to see a shift in webapps’ default design. Instead of the shallow-nav, overloaded pages that are typical in web2 apps, we’d start to see the simpler, more approachable deeply-nested hierarchical navigation common to native apps.
The problem though, is that it’s a fool's errand to merely chase after faster GETs. That’s nothing new, we’ve been doing that for decades. We’re talking about responding in microseconds or even nanoseconds as if it were static content. There’s only one path that can lead us there.
Make GETs nullipotent
Nullipotent describes an action which has no side effects. From Latin etymology, nullus means “not any,” and potent means “having power.” Nullipotent describes an action that yields the same result as performing it zero times. To draw a comparison with a more familiar term, idempotent describes an action where performing it multiple times yields the same result as performing it once. To put it simply, in web4, a GET
to The Edge must put zero load on the origin server... every time.
web1 and web2 follow the pattern where GET
s are not supposed to change data. (That’s what POST
s, PUT
s and DELETE
s are for.) web4 takes that a step further and stipulates that GET
s aren’t supposed to fetch any data either. If it’s not already in RAM, then it’s not included in the response. This only makes sense if you can have a large amount of cache. But not just any cache.
Make cache active, not inert
What is a CDN if not a globally distributed temporary object store that uses HTTP for its API? Also, what is UI if not a set of rules for reacting to changes in state? Combining these two concepts creates powerful advantages. Most of the time cache is forced to be inert, doing nothing, sitting in some centralized service waiting to be fetched or queried. Being able to treat The Edge as a giant cache that can perform specialized actions when changed would be quite empowering.
This certainly does not replace your database however, nor would you be required to float the entire thing in RAM on The Edge. If your GET
is working with stale cache it’ll still render it and send a response. If your GET
is working with missing data, it’ll render what it can and send it regardless.
Simply put, GET
s don’t make any decisions and they NEVER block waiting for data. That job’s been delegated to the WebSocket.
Upgrade async/await to async/amend
GET
s only have one opportunity to respond to a request. That’s why some GET
s can be found executing hundreds of queries to the database or various caches. That archaic one-request/one-response model forces a mountain of asynchronous work to be awaited before it can finally be rendered and sent to the browser.
Moving away from an async/await model onto a async/amend model means you can always simply respond immediately, using whatever data you’ve got to work with. And after the user has something to see, a separate WebSocket connection can correct whatever is stale or missing.
In some ways this resembles a local-first architecture but it differs in a few key areas. Local-first is primarily about being able to work offline and synchronizing co-located data between clients. Async/amend is far simpler. It’s about shooting first and asking questions later. Its data is still technically classified as a cache and there’s no attempt to synchronize its state back to the database.
Benefits
If GET
s were free, webapps could finally be competitive with native apps. Until then, the dynamic web will suffer.
Artifacts
- xUI source: github.com/xui/xui
Member discussion