The web is incredible: you can publish something on a Tuesday night and it works on laptops, phones, TVs, and a five‑year‑old Android that refuses to die.
But the day-to-day reality of building for the web often feels… heavier than it needs to be. You can end up needing a build step, a router, a meta-framework, a design system, an auth provider, an edge runtime, a database proxy, an ORM, a logging platform, and three different “ways” to fetch data—just to ship a landing page with a contact form.
This isn’t a “back in my day we wrote HTML uphill” rant. It’s an attempt to explain where the complexity comes from, when it’s justified, and how to keep your project from turning into an expensive hobby.
The web isn’t over-engineered. Our goals are.
Historically, you could build a site with a few files and call it done.
Now we want:
- perfect performance scores
- instant navigation
- server rendering + streaming
- personalization
- analytics, experiments, and tracking
- type safety
- consistent UI across teams
- SEO and social previews
- “scale” (often before we have users)
Each goal is reasonable. The problem is we stack them all on top of each other, by default, on day one.
Complexity is rarely added because people love pain. It’s added because it buys something: speed, safety, and features. The danger is when we pay the cost without collecting the value.
Where the complexity actually comes from
1) Tooling that tries to be invisible
Modern tooling is magical: it makes JSX work, bundles modules, optimizes assets, code-splits, polyfills, and deploys. But when it breaks, it breaks like magic—cryptically.
When your mental model is “I write code and it runs,” a “loader”, “plugin”, “transform”, and “edge runtime mismatch” feels like the universe gaslighting you.
The fix isn’t abandoning tooling. It’s keeping the toolchain as small as possible until the project actually needs more.
2) Solving problems you don’t have yet
The modern web’s biggest foot-gun is building the architecture you think a future version of your product will need.
If your app has:
- one developer
- one database
- one deployment
- one audience
then introducing distributed systems patterns early doesn’t make you “senior.” It makes you slow.
3) The “framework ladder” effect
Once you climb into a meta-framework, you inherit a philosophy: routing conventions, rendering modes, data fetching patterns, caching rules, and deployment assumptions.
That can be great—if you commit.
But many teams end up half-committed:
- some pages SSR, some CSR
- some components server, some client
- multiple caching layers
- multiple sources of truth
Now you’ve got complexity plus ambiguity.
4) Accidental complexity in state
Most web complexity isn’t in the UI. It’s in state:
- auth state
- user preferences
- forms
- offline or optimistic updates
- caching
- synchronization
You can have the prettiest components in the world and still ship a frustrating product if state flows are unclear.
A simple rule: earn complexity
The best teams I’ve worked with don’t avoid complexity—they budget it.
Before adding a dependency or a new architectural pattern, they ask:
- What concrete problem does this solve today?
- What’s the operational cost (learning, debugging, hosting, upgrades)?
- What’s the escape hatch if it’s wrong?
If you can’t answer those, postpone.
What “simpler web engineering” looks like
Here’s the approach I try to follow.
Prefer boring primitives first
- HTML for structure
- CSS for layout
- a minimal amount of JavaScript for interaction
You can still use Next.js/React/etc., but treat them like power tools, not a religion.
Pick one rendering story
Mixing every rendering mode is where apps become spooky.
Choose the simplest thing that meets the product needs:
- If it’s mostly content: static-first.
- If it’s user-specific dashboards: SSR/streaming where it helps.
- If it’s mostly interactive and private: client-rendered can be fine.
The goal is not “max tech.” The goal is “clear invariants.”
Build the smallest data layer you can
If you’re early:
- one DB
- straightforward queries
- minimal caching
You can add an ORM if it increases correctness and speed of development, but don’t let the data model become a second programming language.
Optimize only what you can measure
Performance matters. But “performance” is not vibes.
Measure:
- slowest pages
- biggest bundles
- most common interactions
Then fix what hurts.
The punchline
The web isn’t doomed. The web is just… mature.
Mature ecosystems accumulate patterns. Patterns accumulate abstractions. Abstractions accumulate defaults. And defaults eventually become cargo cult.
Your job isn’t to use every modern feature. Your job is to ship something real, keep it understandable, and make the next change cheaper than the last.
If you do that, you’re not under-engineering.
You’re engineering.