Modern Frontend Complexity: essential or accidental?
18 points by BinaryIgor
18 points by BinaryIgor
Some of us are building websites with a little interactivity. Others are building web apps with desktop-like functionality and expectations. Different tools for different problems.
Many people do not understand this. Hence the appearance of stuff like simple blogs running as a SPA.
True, but I would still argue that if interactivity comes largely from exchanging data with the server - as it is for most web apps - then there is a simpler approach to React, Vue or Svelte :)
Once you exchange data with a server, you have a distributed system. Distributed systems are not simple. You can create a DSL or framework that tries to paper over the complexity, but I think those all inherently have tremendous tradeoffs and ignore a lot of the reality of distributed systems.
I don't think that is an apt analogy - in discussed here context, state is always on the server. Whatever clients receives from it, is the truth - ignoring temporary mismatches like forms and selects; but these are manageable, rather straightforward to sync
I don't follow.
state is always on the server
Whatever clients receives from it
straightforward to sync
You are describing a distributed system.
With a SPA, I agree that you have a distributed system. The client stores all sorts of state and it can be a mess to keep everything in sync.
With a more traditional setup, the client is merely a view into the state on the remote system. Mutation happen remotely and then the client refreshes the view. You can definitely call that a distributed system still but I think there's an important distinction between the two.
A todo list that you can edit from multiple logged-in sessions for a single user is a distributed system; consider what happens if I update a task on session A and then go to update it on session B without explicitly refreshing. First write wins, last write wins, merging edits...
Actually, the architects of the World Wide Web solved this problem a long time ago. We can return an ETag header in the response when we serve the task. Then in session B, trying to update the task without refreshing would fail with an HTTP 412 Precondition Mismatch error. In the frontend we can handle this error and ask the user to refresh the task before updating it.
You can frame it in this way, but true (and hard to manage) distributed systems usually have:
Ephemeral - user can always refresh the page, that's the worst case - clients with one server seems like a much easier case to manage.
Web clients have persistent state though. The state that lives in the browser while the htmx get request is going on is persistent. More importantly, it's mutable state - I can click one button that triggers a server-state-updating request, then click another button that issues a different server-state-updating request before the first request is finished. Communication between the client and the browser is asynchronous, this creates a distributed system.
You can already run into these sorts of issues with very basic HTMX-based setups when juggling different kinds of state that need to be processed in different ways, or when multiple inputs can affect the same output element. In my experience, the solutions there are not necessarily simpler or clearer when expressed via HTMX than when expressed via a framework like Vue or React. That's not to say that HTMX isn't still similar overall in these situations, but I think it's important to understand how complex distributed UI development can be.
In principle I agree there there's a lot of accidental complexity in modern frontend development, but I don't think your article does a good job of identifying where that accidental complexity is. It feels more like you are listing out the set of tools that you feel most comfortable with - which, to be clear, is a perfectly fine set of tools, I don't think there's anything wrong with them, but in the fundamental sense of managing state in the browser, I don't think they're significantly simpler at all.
To put it another way: Webpack or TypeScript don't add that much complexity to frontend development. Add TypeScript to your application, you need to deal with a minor build step. Remove it, you need to deal with a dynamic type system. People (including myself) have some opinions about which approach they prefer, but they're just different choices about what kind of complexity you find easier to deal with. The essential problem of web development has less to do with that, and more to do with how you build distributed stateful systems, particularly ones which require instant feedback (i.e. when you click a button, something must happen immediately to indicate that the button was pressed, even if the state eventually gets processed on the server).
You can run into state mismatch issues even with SPA frameworks. One famous example is the common pattern where a UI says 'no results found' while the results are still loading. Of course, it shouldn't happen, and good helper libraries like React Query prevent it. But most SPA frameworks by themselves don't.
Htmx gives you a pretty simple toolset and expects you to do some, if not all, of the work of handling distributed state. Eg, it gives you support for a 'loading indicator', which you can use to show the user that a request is in flight. It doesn't handle it for you automatically. On the flip side, it also doesn't show any 'no results found' screen while the request is in flight.
Absolutely, these sorts of problems aren't necessarily easy to solve. But a lot of the complexity in modern frameworks comes from trying to make exactly these sorts of cases trivial to solve. In React, you've got things like Suspense and the RSC system; in Svelte, the compiler handles async/await logic directly; SolidJS has just can completely rewritten in large part to better handle async state updates.
That's not to say the HTMX approach is wrong, it just suggests that there is some real essential complexity here that HTMX chooses not to solve for you, but that needs to be handled at some level in every application. I also don't think it's clear at all what the best way of hiding this complexity is - React introduces a lot of complexity while trying to solve distributed state updates, and I don't think it's clear that all of that complexity is inherent to the problem.
I learned HTML, CSS and Javascript in the mid 90's, so I recall the "simplicity" of what the article is describing (although it didn't feel simple at the time!). However, the crux of the rest of this reads like the following to me:
Today, we have lots of complexity, like TSX, JSX, bundling, post CSS processing, polyfills and transpilers. Wouldn't it be so much better if instead we just had something simple like HTMX, web components, Mustache, Tailwind and some simple scripts to glue them together?
This might as well say "Aren't you tired of blork, glamf and blorg? Simply switch to gamf, florb and klorb!"
I don't pretend to have to deal with the problems of supporting old browsers, handling mobile and tablet sized screens, and gracefully handling the jittery internet connections we find in the real world. But I do find that static HTML loads real fast, is handled well enough by all browsers and keeping state in URLs and query parameters serves both users and developers well. I hope to see more of this for sites, at least ones that aren't Google Docs or Netflix.
Yeah Tailwind especially seems pretty out of place when proposed as a "simple" alternative to today's complexity... For one it already seems to be used everywhere, and it also is the source of sooo much complexity, much more than postcss or whatever.
Sure, in theory, you can just grab the tailwind CSS file and import it. But that's not how people use it these days. People have editor plug-ins to automatically alphabetically sort their class name strings to keep them manageable. People have build steps to scan their whole application, find which tailwind CSS classes are used, and generate a bespoke tailwind CSS file for the project which doesn't include all the unused classes.
From a complexity perspective, I'd much rather use SCSS or CSS in JS.
Or, you know, just CSS.
I don't know, CSSs first problem is the first letter. That unfortunately fails to scale, fails to make "widgets" portable, and makes understanding basically impossible - makes a local property into a global one.
Sure, variables have solved many of the issues, but too little too late.
What tailwind solves: it resets styling to a default, and it gives back strictly local reasoning (if this sets it to blue, it will be blue). In addition, it removes the hard problem of naming stuff everywhere (.button-container-holder and the like), and inline styles make sense in that you don't have to move back and forth between HTML and CSS, which divide haven't made sense for many many decades - there is no way to write CSS without knowing the accompanying DOM structure, and DOM is not doing layouting in and of itself. So why have them separated?
You may say that "but then I have to repeat this and that" at every step - that's why we have component, you have web components, SSR frameworks have solved it eons ago (fragments or whatever), or client side libraries can also property abstract it away.
People have build steps to scan their whole application, find which tailwind CSS classes are used, and generate a bespoke tailwind CSS file for the project
And? This is an absolutely trivial search, that is used to minimize the CSS that will be a static resource. But you are free to include the whole thing, this is an optional step. Any kind of "alternative CSS" you mention does way more than that, they are complete languages.
I find the cascading to be a helpful, albeit very tricky, part of CSS. If it was only Tailwind, I'd probably be @apply it everywhere or use something like DaisyUI, in which case it's not really local reasoning again.
Sure, variables have solved many of the issues, but too little too late.
Not sure what to think of it, I personally like that CSS keeps improving.
there is no way to write CSS without knowing the accompanying DOM structure
I don't understand this point fully, I'd expect small (semantic) elements to stay mostly the same so I'd just write up some rules for that. I'd expect a <div class="stack">...</div> also to behave like something that stacks elements together, so I'd write up rulesets for that.
that's why we have component,
Yes, so components in Astro, Svelte, Vue, things with CSS Modules solve it at that level as well, its scoped there to the component. I don't feel the urge for Tailwind as much there either. If I, for some reason, really want absolute specificity I create an atomic class for the exception or inline it with the style attribute.
If CSS was stuck in the year 2018, I'd probably agree with your take, but luckily it isn't. I don't mind that people prefer Tailwind because cascading isn't the abstraction for them, but cascading has probably worked fine for a silent majority(I'm guessing :P).
The line below, pulled from one of the examples on the page, did feel like there was still quite a bit of accidental complexity still left on the table.
<div data-drop-down-options class="rounded border-2 whitespace-nowrap absolute mt-2 right-0 top-6 bg-white border rounded hidden z-99">
As ever when criticizing Tailwind, the thing you think you're comparing to is
<div data-drop-down-options class="options">
The thing you're actually comparing to is
<style>
.options {
border-radius: 6px;
border: 2px black solid;
whitespace: no wrap;
position: absolute;
margin-top: 8px;
background-color: white;
display: none;
top: 24px;
right: 0px;
z-index: 99;
}
</style>
It is not clear at all to me that the written out version is less complex, particularly since you have to connect .options from the HTML to the CSS by reading different files. In this case, there was no cascade, but adding cascade only makes it harder to understand the code by just reading a little.
In the context of the article(less tooling), I'd say it's fair to critize usage of Tailwind no?
I'm not trying to start a Tailwind vs CSS thing here, but if the article wants to promote thinking if you /really/ need a tool for web development, then a comment trying to question Tailwind would fall right into place imo.
E.g. if you aren't using CSS cascade a lot, maybe some custom variables and a few components could do the job, or a small script, or the style attribute can be a better fit than Tailwind...or not!
I think if your goal is to avoid build steps, Tailwind is an incongruous choice, yes.
Or an extra dependency! If you're making a small internal dashboard why pull in a processor when some custom vars and atomic classes can be enough?
Or if something like CSS modules is enough for your use case. Or if the CSS language server has been better than you than Tailwind LS (ahem).
I like the locality of Tailwind, but it is a solution of having your CSS colocated with your markup, not the solution, IMO. I appreciate atomic classes more after using Tailwind, but I wouldn't default to it.
That's fair - I am still torn on the Tailwind usage; for now, I've just concluded that complexity-to-benefits ratio is worth it
What I don't get about tailwind is that it just seems like inline styling with a detour to me.
The example you are using isn't honest because you are using a single HTML element. If you had shown multiple similar elements you'd still have one css rule and one class on each element. You'd still end up with this forest of tailwind classes on each element when using tailwind.
I can expand on HTMX and Web Components, in that those 2 build on top of how the web already works. Whereas TSX/post-processing stuff/other JS toolchains build their own little ecosystem of things.
The benefit here is that things I learn from HTML and CSS I can readily use in HTMX & Web Components across several languages. But my knowledge of Vite and its fantastic ecosystem of plugins is stuck in JSON configs.
So while I get where you're coming from, and I agree even with that static sites usually is sufficient for certain websites, I think OP is questioning if you immediately have to go from a template to fullstack JS when you have server side state.
Being able to reason about the flow of your program/computation from database to HTML(edit: request-to-response is a better phrase) template is a thing of tradeoffs imo, one that blork and florb might not fix, but it can help!
100%; I would also argue that most web apps are server-driven anyways - even if built with a client-heavy approach.
If most interactions require exchanging JSON-formatted data only to then transform it to HTML through JavaScript - I would recommend giving simpler approaches (HTMX) a try :)
I think responsive design is probably an unsolvable constraint (i.e. it's nearly impossible to express UIs that cater to a full spectrum of vastly different UI affordances), this is likely the contradiction the root of why modern front end design never seems to sit right with anyone. The solution would be custom designs for each device category, but that's more expensive than anyone wants to pay for, so we get this crap instead.
ORM has similar qualities, as SQL is profoundly alien to OOP and the two will never be reconcilable, a state of affairs that has famously been likened to the Vietnam war of computer science.
I strongly disagree with this. I'm not going to say responsive design is easy, but it's rarely anywhere close to the most difficult problem I need to solve for a given project. It certainly creates challenges on the design side, but in terms of implementation, the browser typically provides everything you need to correctly wire up different affordances for different interfaces, and it's mostly just a case of doing that grunt work and testing enough combinations.
The fundamental flaw with trying to categorise devices and serve different applications to different devices is that a lot of devices fall into multiple categories. For example, a lot of tablets are touchscreen but include an integrated hardware keyboard. Many people will turn choose to plug mice into those tablets. In the other direction, many laptops have touchscreens, and many people use drawing tablets that behave like touchscreens. One device can even have several screen-sizes, for example if you're putting your browser side-by-side with another application, or if you plug an external monitor into your phone.
There's a good reason why that approach was abandoned - it just doesn't work.
I had a similar revelation a while ago. I got my start working on large SPA projects (mostly in Vue), and honestly thought that was just how things were done. That pattern poisoned my approach to many projects.
It wasn't until quite some time later (when I started working more with static content) that I came to realize just how much of the "app feel" of SPAs could be achieved with server-side functionality and a small amount of JS. Obviously, standards had to come a long way for this to be a reality; without view transitions, we wouldn't be able to easily replicate the SPA experience. But those standards have moved on, and we now have excellent helper libraries for this.
I sat down once to work out how one could implement a music player (one of my old projects) in a more server-driven way. With HTMX, it's largely trivial. I will say that the DX for writing/returning templates on the server side does feel a bit clunkier, but on the whole I find the pattern of doing nearly everything on the server much more pleasant.
I disagree about using Tailwind. I personally think it complects projects by providing defaults outside of the written code. CSS is perfectly fine, and it's trivial to load into web component shadow roots when needed.
The web has had to catch up a lot to what libraries such as React and Vue were created to resolve, and probably wouldn't be there without those libraries showing the need, but for many use cases I think we're there now.
The issue is moreso that the complex tooling is being used for everything, even when it's not necessary. Single page apps aren't inherently bad but when they get misapplied they can make can make something that should be relatively easy much harder
I recently started a similar project in Rust (with Axum + Askama), though I want to use JS strictly for progressive enhancement, and I also don't use Tailwind. I haven't found a use of HTMX (currently only a sprinkle of vanilla JS).
Good idea to parse the generated HTML in test! My tests are more ad hoc and string-based than yours. I also considered snapshot/approval tests, but decided against them since I am still in the prototyping stage and expect the HTML to change a lot.
Author directly touches on why: "How they work is inherently different from what browsers were designed to do.".
Modern browsers are essentially a runtime for lots of actual applications and the main reason why desktop apps are not very prominent anymore. I agree that in many cases people reach out for them when they don't really need, but in many cases the complexity is indeed essential.
My understanding is that SPAs mainly emerged because front-end developers wanted to handle routing themselves instead of the web server. And if some framework is able to handle routing, why not add authentication, templating, and whatnot?
My understanding is that SPAs mainly emerged
I don't think so. They emerged because people sometimes need an application over a web page. There are many types of components that only depends on frontend state, or that doesn't make sense to do a roundtrip to the backend, or we would like to work offline as well. These already benefit from an SPA doing smart tracking of changes (basically react's idea of view=f(state), that managed to find its way into desktop frameworks as well)
Having been there when it emerged, I think it was a combination of:
flicker of white background when moving from one HTML to another. Browsers (other than Opera) weren't able to cache rendered page and delay progressive rendering of the next page well enough to avoid it. It made websites look ...whatever opposite of slick and cool was.
Web performance was a major problem. Browsers, JS engines, HTTP/1, networks, mobile phones were all slow. Web perf solutions were in infancy. Bloated pages made reload of HTML look even slower, creating justification that you just have to have SPA for performance (BTW, right now it's the opposite - browsers matured and optimized loading of HTML so much you can't beat them with JS).
Flash was a thing. Devs who liked Flash didn't want to reload the page, because that restarted their flash (interrupting their background music!). Those who hated Flash wanted to prove that they can do instant transitions without Flash.
Early SPA development was painful and tedious due to DOM being global mutable state, which combined with async events meant you could too easily mutate it into jumbled buggy nonsense. React dramatically improved UI state management, making devs forget for a while that UIs could be done any other way (later rediscovering HTML templates as "SSR")
The article mostly ignores TypeScript. Vanilla JavaScript for web components and event handlers is fine at small scale, but frontends rarely stay that small, and once the code grows, types help.
Adding tsc here does not really weaken the argument. The target is JSX, React runtime overhead, and heavy bundler setup. Running tsc before a simple bundling script is a small complication. As @Johz says in the comments, choosing TypeScript over plain JavaScript mostly changes where the complexity lands.
frontends rarely stay that small
Actually, I would argue that frontends usually stay that small. Most apps are tied to the scale of the business, and most businesses don't really scale. For the few that do, scaling the app to handle the complexity is a fortunate problem to have.
In fact, Facebook themselves had this problem when they grew. Back before the days of React, they were using an htmx-like system called Primer. Eventually they moved when their frontend needs outgrew it. But again, that's the rare case and imho not the one we should optimize for.