The Evolution of Frontend System Design
The Two-Hour Dropdown
Juntao Qiu opens with a familiar estimation failure: a seemingly tiny frontend task expands from hours into days. He introduces a reusable user selector for assigning work in products such as Jira or Trello, then sketches its initial React implementation.
From Happy Path to Pagination
Remote data immediately adds loading and error states to the component. When rendering thousands of users freezes the interface, Qiu introduces pagination and incremental loading, showing how a performance fix can alter API contracts and require versioning or feature flags.
Search and Asynchronous Failure Modes
Adding search creates another complete data-fetching workflow with loading, errors, empty results, and backend query support. Qiu explains debouncing, race conditions, stale responses, request cancellation, and the growing burden of coordinating local state.
Accessibility and Internationalization
Accessibility testing exposes broken keyboard navigation, weak focus visibility, inadequate contrast, and missing ARIA behavior. Localization then introduces text expansion, right-to-left layouts, and locale-aware sorting, revealing that rendering is only the visible tip of a production component.
A Local Interaction Becomes a System Change
Qiu zooms out from the selector to the assignment workflow surrounding it. One choice can update a card, board filters, activity feeds, notifications, and assistive technology, while also raising questions about optimistic updates, rollback, and concurrent edits.
Delivering a Large Browser Application
Using a Jira board as an example, Qiu examines the volume of JavaScript behind a modern application page. He describes dividing work among server-side rendering, client-side rendering, lazy loading, and code splitting so the browser loads only what the experience needs.
Coordinating Teams and Shared Experience
A single coherent interface may be assembled by many teams working on separate regions and features. Qiu shows how inconsistent APIs, styling patterns, duplicated fetching, and disconnected state updates create application-wide complexity.
Three Scales of Frontend Complexity
Qiu distinguishes component correctness, interactions between components, and application-level concerns. At the largest scale, teams must account for feature toggles, observability, security, accessibility, and instability across interconnected parts.
Frontend as a Distributed System
Qiu argues that frontend complexity reflects the web platform’s expanding capabilities rather than needless over-engineering. He closes by urging developers to think in systems, design for failure and asynchronous behavior, and build a mental model that can absorb new techniques without requiring encyclopedic knowledge.
Before we get into anything technical, I guess, I want to start with something simple, something we have all experienced. Please, can you raise your hand if you have ever estimated a front-end task, like a simple task, at two hours? It somehow turned into two days or even a week.
Cool. Yeah, me too. It happens to me all the time. And it's not because we are fundamentally bad at estimating. But why does that happen over and over again for a simple front-end task that always turns out to be so complicated? To answer this question, I would like to take you onto a journey small, and we'll stop from something small, even trivial, and watch it grow into something more involved.
So imagine that you have a product team reached out to ask you to implement something simple. They say, you know, we need a dropdown to select a user who can assign a task. And the design is very simple. You know, it has, there's a panel or bugs is collapsed by default. And when you click it, it expands and you can pick user from the dropdown list and it will assign the task to someone else. And the component can be used in different scenarios.
A typical one is, for example, if you have used Jira or Trello, there's card in the board application and you can click the avatar and that will show up this component. You can select the user from it and it will assign the task to the user. Another typical scenario is when you have some tasks and need a manager to approve and when you click that approval list, it will show all the manager names.
So we can quickly put together some code like this. I'm using React here just for demonstration purpose, but obviously you can use any, for any language or library you are using is pretty much like a similar structure. So firstly, we will need to fetch the data from the remote server, and we need to store it into local store, and then if everything goes well, we can render the final list.
And after a few hours, maybe, or even shorter than that, you have a working version. And because the data is from the server side, we obviously need to think about if the data is in loading status, we need to show something like indicate that loading status and also the if something went wrong, either it's 402 or 402 or gateway timeout or anything, we need to correspondingly handle this case.
That means we will need to add a few more states into our user selector component. It's still manageable. Firstly, before we send out the request, we will set the loading as true so we will see the spinner to show up and as we waiting and either the result is resolved or rejected by the server side, we will set the loading as false so we don't have to show the spinner.
And if anything went wrong, we show a error message, otherwise we just go to happy path. That's still like simple at this stage. And that one day like after you put that into the code base and someone testing it. And you got the escalation that said, I cannot use this component at all because it's just a freezes.
And you check the code and know, obviously, it is true. And because we are fetching 2,000 items at once and trying to render them in the list, and that means we will need to break the request down into small chunks. Yeah, obviously fetching 2,000 or more items and running them is not a very good idea.
So we need to introduce pagination. And for enable this simple enhancement, we'll need a few more states here. So we need a page number to indicate P2W which fetching. And we need to show or hide the has more indicator at the bottom of the list.
And we want to probably use some intersection observer or detecting the scroll. And when we reach to the bottom, we do the refresh. And it's not a big change, but here it's kind of different because we have changed the contract between the front end and the back end. That means initially we only have the list list and we render. And now we have metadata for the page information, like how many items in the page and are there any items are left. And also the data is wrapped into a data field.
That means we will need to either introduce some API versioning with the backend, we will have different version of API in the URL maybe, or feature toggling to make sure we are not breaking any existing functionalities. For example, if the mobile device is still relying on the old one, that might break that code.
And it's, not the final three yet. And the next day, the same customer reached out and said, okay, I can see the pop-up or the dropdown now, I can select a user, and otherwise scroll it will load more. That's great, but as I said, it has 2,000 customers, and when I scroll, it's very hard to find the right name.
So we need to support the search. That's a fair request, and we definitely need to support that. But search is kind of difficult itself. By saying that, I mean for Eable Search, search itself is a data fetching. That means we need to handle the loading status and the error handling. And also, if the name doesn't match anything, we need to fall back to an empty state.
That means we need the design for it, like an illustration or a, you know, try a different query kind of message. And also the, that will be some backend changes as well. We need to introduce these parameters and make the backend either support the, the cursor based or offset based pagination and also We definitely don't want to type, sorry, send too many requests to the backend to overload the server.
For example, if I type Alex, I just want to send one request as ALEX at the user post instead of sending ALEX for queries because the query is used quite frequently. So it's very easy to overload the server side. And also, this is the simple problem. We can fix that quite easily with the debounce.
And search is very easy to run into risk conditions. If you don't know that, it's like if you send multiple requests, the response could come back in different orders. Than the same request. So it will easily get overridden by the stale data. So we need to handle this case.
That means for any in-flight request, we need to cancel it first before we send out any new query. And it's not, you know, unsolvable. We can do that. We have patterns to fix the problem. And now we have these fully working searchable, paginated, debounced, error handled, correct, the user selector.
And the code is turned into something more longer and the more complex, at least we have to maintain six to seven states. And in the use effect block, if you don't know that, if you're not using React, it's like where you fetch the data, but we needed to maintain the, you know, the, which page we are fetching, do we have more items to show?
And when we want to clean up, when we want to unmount the component, we need to cancel any eflight requests, something like that. And we are, at this stage, we're pretty ready to, you know, release to production and give it a try. But we got the accessibility auditing team, they reached out and said, you know what? The component is not really useful for keyboard users.
Like when you really use a keyboard to navigate, it's just not working. We want to support that. That's definitely something we need to do. To support to make that a fully production ready component. Obviously, I'm using this screenshot on purpose.
You cannot see easily the the flexing ring and that's a problem we need to fix. And to make that accessible, there will be a lot more to consider. Like keyboard, obviously, you need to handle all the events for the key press and the ARIA attributes Color contrast, for example, this one is not really correct.
And for proper focusing management, we want to make sure when you tap the tab, it's focusing switch from the user selector to the dropdown, and you can select the items from the list. And after a few enhancements, we solved the problem, and we're just adding a few more states to make that happen.
And then the localization team reached out is that the component is, when I switch to a different language, the UI has CSS has cut off the style is just ugly, it's not really usable. That's common, we need to fix that.
But it's a typical problem we found very late before the release. So again, the translation itself is another aspect of the normal happy path. We need to handle a lot of test wrappers. Some texts are longer, like a German has 30% longer than English normally. And some languages are reading from the other direction.
And we need to handle locale-aware sorting. When you do a search. And you can see the pattern here, like the code again is growing even longer. So when we initially think about the user selector or when product team reached out, when they talk about the user selector component, we are mainly talking about this one.
But that's a like a very small part of the whole story. For a proper production ready component, we need to think about the loading standards, error handling, empty states, pagination, accessibility, and so on. And for each category, it has its own problem to resolve.
For example, in search, you need to implement the debouncing the about controller to managing this request to cancel, you know, the any in-flight request. Similarly, for accessibility, you need to set up the mechanism to test them to prevent us or developers to make mistakes again.
And it's very, you know, for every component, a single component that has layers. When we talk about the component, the very first assumption is that we are only considered the rendering part. But behind the scenes, or the happy parts, is only the tip of the iceberg. And for any product ready component, it has a lot more to consider other than the render itself.
For example, the thing I've listed here, the styling, obviously, the network request, or even local state management, and all these cross-functional requirements. And this is just like for a simple one component, it has hiding a lot. If you just look at the surface, it's simple, but if you unpack it, it has a lot more.
And let's zoom out to look at the original requirement again. So the product team wants us to build a user selector to assign a task to a user. And the workflow is simple. You click the avatar, you get this expanded user list, you select the user from list, and it's assigned.
It's a very simple one. Step interaction. But behind the scenes, it has more that happening on the UI because it's cascading the changes to the different components. For example, I'm just listing a simple flow here. Like user selector sends the patch request to the backend and the card need to update the avatar to the new one, to the new assignee and the board whole application need to refresh somehow or just, you know, even you just randomly partial because we have some filter defined. And the activity feed might need to update itself to log the who has done, who has made some change, what change.
And for the assignee in that top bar, there may be a notification badge they're showing a red dot or something to indicate that you are assigned to a new task. So you see, it's not a local change anymore. If we put that in the context, it's a system-wide change. And now, there will be much more deeper question raised up.
Like, do we update the UI first or do we wait for the sort of response? That both are correct, but it depends on the user experience. For example, the task to user scenario, we probably will update the avatar first and then we send the request to the backend.
Then it will raise another question, like if the network failed, how do we handle that? How do we rewind back to the initial state. That means we will store the initial state like that. And also, what if multiple users are editing the same user or the same card? How do we resolve the conflict?
And when we make that change, how do we announce the change correctly and properly to the screen readers? We need some mechanism to and announce these to the screen readers. And this is like a, again, it's a very simple interaction. It's a one step. You click, expand, select, and assign.
And think of that, if we put this, like zoom out again, this is a very typical feature in Jira. This is a Jira board view page. It looks like a a lot of things going on here. And just imagine, do you know how many JavaScript bundles we load for rendering a single page?
It's roughly 112 JavaScript bundles. It's not about like Atlantic is doing something crazy. It's pretty like Jonah said, it's a standard or a trend. If you open up a Amazon page, you will be surprised how many jobs without loading. And for Jura front end, it has like 30 or more gigabytes, sorry, megabytes data to JavaScript gzipped in total.
And to render this particular page, it has Obviously, we don't need all the JavaScript for running this single page. That means we need to categorize the JavaScript by the usage. For example, some of them need to be rendered in a server-side. On the screenshot on the left, you can see the navigation bar and toolbar and all the tabs for this view is already rendered.
On the server side. And for the main part of the board is client-side rendering. And we definitely can lazy loading some of the components for later. And there are some features we don't need at all. So we will not delay, we just don't load until user shows the real intention. Like if you use a click a middle button, we start to download some more JavaScript chunks.
And that means for this page, we need to think a few more aspects, like server-side rendering, code splitting, for making the code splitting happen, we need to, in a bundler step, we need to set up correctly and so on. And for like a, Let's go back to this JIRA board view again. So if we look at this page for users, for end users, it's just like one coherent page.
But underneath, there are many things happening. There are many teams working on different parts of the application, and there are challenges to coordinate those teams to working together smoothly to make this happen. For example, probably there will be some team A working on the navigation, team B working on the what you call the top bar, team C working on the like us, we are just working on the user selector and more.
And that means when every team is using different API or different styling patterns, it very easily could cause the the mismatch or inconsistency. For example, if we go back to the view here, when one user, we're showing the avatar in different places and just imagine that one team fetched the avatar data and these users are locally while the other team using the same data, they will obviously overfetch at least. And then when someone changes the data, how do you update the avatar on the other component which you don't know exists at all? So that means in the application level, we need to think a lot more globally. There's some global complexity here.
Data and API inconsistency is one of the problem. And the UI or UX fragmentation is another, for example, how do you show the loading status correctly? Not just for the sake of showing the loading status, but for making it a consistent pattern in the user.
Because one team might be using this kind of loading, the other maybe, or they are using the loading in incorrect position, but for the end user perspective, they don't care about this, like you're using what pattern, but it should be a coherent experience. And at this stage, I guess you have, like we are dealing with different types of complexity.
For the single component, it's mainly about making it correct and we probably will handle a few local states. And if you zoom out a bit, you will see the interactions or the coherent of the components. And then if you zoom out again to the application level, there are a few more considerations.
Like if you want to make the application really production ready, then definitely need to consider the feature toggling, observability or monitoring, security, accessibility, surely these kind of cross-functional requirements. And there are different parts of the application that interacting with each other can cause any unstable issues.
So let's go back to the original question, so why do simple front-end tasks keep becoming complicated? And it's not because we are over-engineering, it's because front-end today has evolved into something far more capable and far more demanding.
It's just like some problem we don't say or we can say, okay, we're just building a simple page, It should be simple. But now as the web browser or some of the techniques are evolving as an ecosystem, the things that is not possible is now possible today. So there will be new things come up.
We need to adapt the new techniques to make the user experience differently. And what used to be just the UI work, is now front end architecture. We're building a rich, data heavy, and even interactive distributed system running inside the browser.
So it's a totally different thing than it was 10 years ago. So before we jump up, let me summarize a few key points here. So first, the complexity in front end is inevitable. You don't fight it by pretending the problem doesn't exist.
Instead, you should design for complexity in mind. And secondly, in just thinking in systems, front end today is a system, essentially. It's a distributed system running, just happens to run in the browser. There are a lot of, you know, it used to be back in the developers consideration now has partially moved to the front end already. And third, design for failure, not just for the happy path. As we just resolved, the happy path is only the very tip of the iceberg.
It has a lot more underwater. Essentially, the front end, most front end is like asynchronous, you need to coordinate all these different resources, you know, components. And you need to handle the, you know, fallbacks, the timeout or retry correctly inside the browser.
And finally, if you can, you need to build a personal mental model. That means you don't have to know everything like we have shown. There are a lot of things to consider when you implement a simple feature in modern web application. But the point here is that you don't have to understand or know everything, but you do need a map that can help you to navigate or expand when you learn something new, you need to fit that into your mental model or the framework you have.
That can easily help you to navigate efficiently in the new domain. And if you want to go deeper into practical front-end design techniques, I share a lot more on my YouTube channel, in my newsletter and on my website. And if this talk helped you to see front-end complexity in a different way, feel free to subscribe or follow in the reach out.
Thank you.
Technologies & Tools
- React
- useEffect
- screen readers
Standards & Specs
- Intersection Observer
- ARIA
- AbortController
Concepts & Methods
- pagination
- API versioning
- feature toggling
- cursor-based pagination
- offset-based pagination
- debouncing
- race conditions
- keyboard navigation
- color contrast
- focus management
- localization
- locale-aware sorting
- server-side rendering
- client-side rendering
- lazy loading
- code splitting
- observability
- distributed systems
- systems thinking
- mental models
Organisations & Products
- Jira
- Trello
Frontend starts with a single “happy-path” mockup, but real apps must handle many
dimensions—internationalization, accessibility, varying devices, performance
constraints, and data complexity.
In this talk, we map the evolutionary path of frontend system design: from static
pages, to components, shared state, caching, normalization, performance
optimization, and beyond. Learn to locate your project on this journey and
discover the next essential system concept to embrace.















