Web Routing: What Is the Best Way to Get From A to B?
In this blog
What is routing?
Before we talk about the different approaches to routing, we need to have an agreed-upon definition. For this article, I'll be focusing on routing using the URL only. Since we are dealing with websites, we are going to ignore the protocol (http, https, etc.) and the domain or IP address (wwt.com, 1.1.1.1
) as these fall outside the control of a website and its server. We will also ignore the port (:80
for http, :443
for https) as the browser can assume which port in most circumstances.
This leaves us with just the portion of the URL that describe the location of a resource on the server, the query string, and the hash string (e.g. /my-resource?query=string#hash-string
). However, because the hash string is never sent from the browser to the server, we will ignore it in this article.
Multi-page routing
Multi-page routing is the most common form of routing on the web and originally the only option available when writing websites.
Here's an example: A user navigates to our website and visits the page /home/page
. On that page is a link to /new/page
which the user clicks on. The browser then talks to the server and requests the resource at this new location. When it receives the page, it throws away the home page and renders the new one.
Pros:
- Supported without JavaScript.
- Allows the client to be as simple as possible (pure HTML and CSS).
- State is entirely contained in server, reducing the chance of desynchronization between the server and client.
- Generally faster than JavaScript-based solutions.
Cons:
- Each navigation requires a full page reload, leading to potential white flashes and duplicated information over the network.
- Server is now fully responsible for rendering pages, meaning heavier workloads for the server.
- Browser caching can more easily get in the way of delivering the most up-to-date information to the user.
A note on accessibility:
- Another potential pro for multi-page sites and apps is that creating an accessible experience is easier when JavaScript is not involved. Using browser-native elements and attributes goes a long way to ensuring visual, audible, and keyboard accessibility and using JavaScript to customize any functionality can interfere with or interrupt behavior that would normally accommodate everyone. It is possible to create accessible custom functionality, but it does take more work and rigorous testing to ensure an acceptable experience for all.
Single page routing
Single page routing started becoming popular as JavaScript became more powerful in the browser and was used with frameworks like JQuery, Backbone, Knockout, Ember, and AngularJS. It is still commonly used with more modern frameworks like React, Angular, Vue, and Svelte.
The idea behind single-page routing is to keep the web page the user is on alive rather than going back to the server for an entirely new page every time the user clicks on a link. Using the same example as above, when the user clicks on a link to navigate to /new/page
, the website uses JavaScript to cancel the browser's default behavior of fetching the entire page and instead makes the changes to the current page to reflect what the new page looks like.
Pros:
- API can be created independent from display logic, allowing for reuse between multiple clients such as web and mobile.
- Website is a collection of static files that can be cached, resulting in only data being transferred over the network.
- Routing behavior can be delegated entirely to the client side, such as switching between a viewing and editing interface with no need for the network.
Cons:
- Relies on JavaScript being enabled.
- Scripts can easily become bloated and heavy, especially for mobile devices.
- Must explicitly handle offline and error states in the browser to avoid user confusion.
Generalized single page routing
There is not an established name for this typing of routing, but it can be considered a type of single page routing. Examples of this type of routing are Turbo, Livewire, or HTMX.
This approach starts with the argument that the speed of single-page routing is great: The browser avoids tearing down the whole world and building from scratch and you can target small portions of the page for updates to save on data. But there is also a big problem: it is too complicated. Each website has its own approach so there is not much consistency, your client code has to have knowledge of the server's setup, and you are usually having to translate data into HTML in your JavaScript.
Instead, why not use a small amount of generic JavaScript that sacrifices the unique-to-each-app efficiencies for the ability to never have to update your JavaScript when your website changes?
The simplest form of this is to have some JavaScript that cancels the browser's default behavior when a link is clicked and fetch the page in JavaScript where it can then be merged with or substituted for the current page. This keeps the browser from tearing down the original page and then starting to build a new page by having JavaScript that naively gets the HTML from the server and then swaps it in. The client side JavaScript code now does not need to know anything about the "data" it is receiving because there is no mapping or translating to be done: it is just the new page.
You can go further by introducing a little more complexity in exchange for some efficiency while still avoiding your JavaScript being aware of business logic or the structure of the server. By modifying your links and the responses to help narrow the scope of what HTML is requested and replaced, you can reduce the amount of data that is transferred for each update. For example, you could include an attribute suggesting a partial page and your response could include where to insert the new HTML specifically rather than replacing the whole page. This allows the user to still refresh the page and still see the same result while still allowing you to render and send less data when possible.
While not exclusive to this version of single-page routing, another common efficiency included is to communicate over a Web Socket. Without getting too much into the details, this saves time and data by not creating a new request every time an update is needed and instead uses the socket to stream requests and responses.
Pros:
- No business logic in the browser.
- Less prone to needing JavaScript changes.
- Still provides a more instantaneous feel than multi-page app.
- Can usually be used immediately on existing multi-page apps without any server changes needed.
- Usually has built-in fallback to multi-page routing if JavaScript is disabled.
Cons:
- Can cause more complexity if JavaScript is needed for other functionality such as animations or interactivity.
- Still need to handle offline and error states in the browser sometimes.
- For HTML fragments, requires a different pattern of thinking or organizing your code.
- Naive replacement of HTML can cause issues for accessibility and user input, such as wiping out the content of a text field or loss of focus due to being a truly new element. Addressing this requires higher complexity.
Shared routing
Once again there is not an established name for this type of routing. While technically this can be considered a form of single-page routing, it sits somewhere between a generalized approach and an app-specific approach. There is a lack of real world examples because it is such a difficult problem to solve without some form of compromise. The closest examples I can find are Meteor (which is too specialized), Remix , Qwik (which are both very new) and Marko (which has its own syntax). React itself is aiming to provide an approach through Server Components which Next is starting to use.
The goal of this approach is to share as much state as possible between the server and the client so that updates can be as efficient as possible while still aiming to minimize the amount of JavaScript needed on the client. React accomplishes this by allowing you to write a single code base for both the server and the client. In theory, this could be done in any language, but it helps substantially that JavaScript is used in this case as there is much thinner transpilation layer and there is support for source mapping in the browser rendering the differences between client and server effectively invisible.
Pros:
- Single code base for both the server and the client.
- Smaller payloads than multi-page and most generalized single-page routing solutions.
- Better HTML fragment replacement.
- If already developing in React, should be a relatively cheap and easy upgrade.
- Fallback to multi-page routing if JavaScript is disabled.
Cons:
- Mental model complexity and complexity in general.
- Lock in to using the particular technology, such as JavaScript or more specifically React.
- Coupling your backend and frontend together means your choice of technology is all or nothing.
Conclusion
Any of the above approaches to routing can be made to work for your use case, but there are always trade-offs with whichever one you choose.
- Traditional multi-page routing is well-established and requires no JavaScript, but can run into caching issues and heavier loads for your server.
- Single page routing allows you to treat your HTML, CSS, and JavaScript as static resources and send only data across the network, but requires JavaScript to work and can often become too heavy of a solution for mobile users.
- Generalized single page routing allows for a drop-in upgrade for existing multi-page apps and decoupling of business logic from JavaScript, but naive replacement can cause accessibility issues.
- Shared routing allows you to write a single code base for full single page routing and multi page routing, but locks you into a particular technology and has a complex mental model.
I doubt that any of these approaches will ever dominate or disappear from the market soon, but shared routing does seem to be a major goal for the future for frameworks like React. If performance, accessibility, and developer experience continue to improve, it may become the new default approach to creating web applications in the no-so-distant future.