Home / Articles

How Frontend Engineering Evolved From Styling to Scaling Systems

Frontend engineering grew from HTML and CSS into React components, caching, monorepo tooling and observability. What changed at each step and why it did.

Publisher: REACTAPP.TOP10 min read2240 words

This article is published in English.

Sources: I Thought Frontend Was Colors and Fonts. Then I Shipped to a Million Users.

The tools have evolved, but our thinking has evolved even further.

Ask someone what frontend development means, and you'll usually hear the same answer: HTML, CSS, JavaScript. Colors, fonts, images, layout.

That's a fair starting point. It's just not where the story ends.

What began as assembling web pages has grown into engineering full systems that need to remain fast, correct, and traceable while thousands of people interact with them simultaneously.

The tools evolved along this path. But what shifted even more was the set of questions we learned to ask ourselves.

Here's the complete journey.

1. It started with HTML, CSS, and JavaScript

The early web kept things simple.

HTML handled structure. CSS handled presentation. JavaScript layered a bit of interactivity on top.

The mission fit in a single sentence:

Build a page that looks good and functions properly.

Most pages were static. Servers delivered complete HTML, browsers rendered it, and the browser itself carried almost no logic.

Over time, pages took on more responsibility. Forms began validating input as users typed. Content refreshed without a full page reload.

JavaScript kept absorbing more duties, and it never handed any back.

  1. From pages to applications

Websites turned into applications. That meant dynamic interfaces, calls to APIs, forms spanning multiple steps, authentication flows, and data sitting in the browser between one interaction and the next.

This shift exposed a problem the old techniques weren't built to solve.

When you manipulate the DOM directly, the visible screen becomes your only source of truth. A single piece of data might exist in a variable, inside a DOM node, and in a response from the server. Nothing guarantees these three stay aligned.

Update one and forget the rest, and you end up with a bug that surfaces only under a specific sequence of clicks.

What was needed was a way to declare what the interface should look like and delegate the job of figuring out the actual DOM operations to something else.

3. React: thinking in components

React's real breakthrough wasn't its syntax. It was the mental model behind it.

UI = f(state)

You describe the screen for a given state, and the framework figures out the smallest set of DOM updates needed to get there.

Instead of writing element.appendChild(...), you write what should be true.

The basic unit of work moved away from the full page and settled on the component instead. Think of a single button, a card showing a product, a popup dialog, or one step in a checkout flow. Each of these owns its own markup, its own styling, and its own behavior.

Here's what that approach delivered:

Reusability. Build a component once, drop it into a dozen places.

Local reasoning. You can understand one component without reading through the entire application.

Testability. Feed it props, check the output, no browser needed.

Composability. Small components combine to form larger ones.

The central question shifted from "How do I build this page?" to "What pieces make up this application?"

4. Next.js: from UI to architecture

Purely client-side React came with a cost.

The browser first downloads an empty HTML shell. Then it pulls down a sizable JavaScript bundle. Then it executes that bundle. Then it fetches the actual data. Only after all that does the user see any content.

That's fine on a powerful laptop. On a mid-range phone with a weak signal, it means staring at a blank screen for several seconds. And search crawlers might end up indexing nothing useful at all.

Frameworks such as Next.js gave developers control over where and when HTML actually gets generated.

SSG. Rendering happens at build time, and a static file gets served from a CDN. It's the fastest option, though content stays frozen until the next build.

ISR. Content stays static but regenerates in the background after a defined time window. You get static-level speed with content that stays reasonably fresh.

SSR. Rendering happens on the server for every request. Content is always current, but the server does fresh work each time.

CSR. Rendering happens entirely in the browser. This suits screens locked behind a login, where indexing doesn't matter.

The key insight: you choose per route, not for the whole app at once.

A marketing page benefits from SSG. A product page benefits from ISR. A dashboard benefits from CSR.

This is also where hydration comes into play. The server ships HTML, and then the client re-runs the component tree to wire up event handlers. Visually the page looks ready before it's actually interactive.

React Server Components and streaming largely exist to close that gap. HTML gets sent as soon as it's available, and less JavaScript ships for parts of the page that were never meant to be interactive.

At this stage, frontend work stops being just "the UI layer." Decisions about rendering strategy, routing, and caching become architectural decisions, and frontend engineers are the ones making them.

5. Then came scale

Building an application is one challenge. Building one that serves millions of users is an entirely different challenge.

The questions you ask start to change:

What should happen when a downstream API slows down instead of failing cleanly?

How do six separate teams ship code to the same codebase without stepping on each other?

How do you roll a change out to just 1% of users and pull it back within a minute if something goes wrong?

How does the app behave on a three-year-old Android phone somewhere you've never tested it?

Which of yesterday's deployments is responsible for today's spike in errors?

These are systems-level questions. They just happen to surface inside the browser.

6. Speed isn't optional polish, it's a baseline expectation

An elegant interface that takes six seconds to render might as well be broken. Users register delay before they can even put a name to what's wrong.

Today's performance discipline relies on measurement rather than intuition. Core Web Vitals give teams a shared language for talking about speed.

LCP (Largest Contentful Paint) marks the moment the primary content becomes visible. Aim to stay under 2.5 seconds.

INP (Interaction to Next Paint) captures how quickly the interface reacts once someone taps or clicks. Aim for under 200 milliseconds.

CLS (Cumulative Layout Shift) measures how much elements bounce around while the page settles. Aim for a score below 0.1.

Each optimization technique lines up with one of these metrics.

Splitting code and loading it lazily. Send only the JavaScript the current route actually needs, not the entire application bundle. This improves both LCP and INP.

Tuning images. Use modern formats, size them correctly, and always declare width and height up front so nothing jumps. This helps LCP and CLS.

Handling fonts deliberately. Preload them, apply font-display: swap, and trim the character set to what's actually used. This addresses CLS.

Distributing through a CDN. Serving bytes from a server geographically close to the user benefits nearly every metric at once.

Keeping heavy work off the main thread. The browser has a single thread that handles both rendering and your scripts. A task that runs for 300 milliseconds freezes every click for that same stretch. This is where INP gains come from.

The underlying principle, stated simply:

Deliver the best experience using the fewest resources possible.

One caution: tune based on what you've actually measured, not on assumptions. Numbers gathered from a developer's laptop in a lab setting frequently diverge from what real users experience in the field, and the field data is the one that reflects reality.

7. Caching: avoid recalculating what you've already solved

As traffic scales up, generating the same response over and over becomes wasteful. Caching exists so you're not paying that cost twice for identical work.

Every request travels through multiple layers, and each one represents an opportunity to short-circuit the work.

Browser → CDN → Application / ISR → Redis → Database

The nearer to the user a response gets served from, the cheaper and quicker it is.

The nearer to the user something is cached, though, the harder it becomes to walk back if it's wrong.

That tension defines the whole practice.

What belongs in a cache? Data that's stable, shared across users, and costly to generate. Never anything user-specific unless the cache key accounts for the individual user.

How long should it live? Short expiration windows are safer but deliver less benefit. Long windows are fast but carry more risk.

How do you clear it out? Options include time-based expiration (TTL), event-driven purges (triggered on publish), or filenames hashed by content so invalidation is never even necessary.

What happens when a lookup fails? If a widely-used key expires and a flood of requests all reach the backend simultaneously, you get a stampede. Guard against this with a lock, or by serving the stale version while a single request handles the refresh.

The pattern worth committing to memory is stale-while-revalidate. Return the cached version instantly, refresh it quietly in the background, and the following visitor gets the updated content. Users experience speed, and you get correctness with a brief lag.

Pay attention to what a mistake here actually costs. A broken cache doesn't just make things sluggish—it surfaces the wrong price, the wrong name, or someone else's data entirely.

Caching looks like a performance technique but functions as a correctness decision.

8. Structuring for growth

Once multiple teams work inside one codebase, how it's organized becomes just as important as how fast it runs.

Boundaries between modules. Decide what's exposed publicly, what stays internal, and what dependencies are permitted.

Contracts for APIs. Typed and versioned, so that a backend change breaks the build pipeline rather than the live site.

Where state lives. Server state, URL state, and local client state are fundamentally distinct categories. The majority of state-related bugs come from treating one kind as another.

Isolating failures. Error boundaries ensure that one broken widget doesn't bring down the entire page.

Rolling out changes. Feature flags, canary deployments, and the ability to revert instantly.

The mindset shifts from simply getting a feature working to making sure that feature doesn't make every future change harder.

9. Shared code across a monorepo

Once multiple applications rely on the same header, the same design tokens, and the same authentication helpers, copying and pasting code stops being sustainable.

A monorepo allows teams to share UI components, design systems, utility functions, configuration, and type definitions—one canonical source of truth, with changes landing atomically across packages.

Sharing does come with a cost, though. Every shared package becomes a dependency, and every dependency is effectively a queue: one team's update can hold up another team's release.

The actual objective is reuse without tight coupling.

A useful heuristic: share elements that rarely change and mean the same thing no matter where they're used—things like design tokens, a Button component, or type definitions.

Avoid sharing anything that bakes in one team's specific business logic. Duplicating code is often cheaper than committing to the wrong shared abstraction.

Micro-frontends take this idea further, letting teams ship independently at the expense of added runtime complexity and duplicated dependencies. That trade-off can make sense for a large organization with firm boundaries between teams. Outside of that context, it rarely pays off.

10. Watching the system in production

At scale, phrases like "it works on my machine" or "it worked fine in staging" stop meaning much.

What you actually need is visibility into how the application behaves in production, on real devices, in real time.

Error tracking paired with uploaded source maps, so a stack trace points to your actual source code instead of minified noise.

Real User Monitoring to capture Core Web Vitals from genuine users, broken down by device, country, and connection quality.

Structured logging tied to a request ID that you can trace from the browser all the way through every backend service it touches.

Alerts tied to symptoms. Notify people when error rates or latency cross a threshold, not simply when a server's CPU usage spikes.

Tracking releases. Tag every error with the specific deployment that introduced it, so answering "what changed?" takes seconds rather than hours.

The questions this setup should let you answer:

Where exactly are users running into errors? Which API call is dragging down the experience? Which route is slow, and for which segment of users? Did the most recent deploy make anything worse than before?

Frontend code used to be the hardest part of the stack to observe. That no longer has to be true.