Building Production Web Apps Without a Framework
Modern browsers provide enough built-in functionality to ship fast, maintainable web applications without React, Vue, or similar frameworks. Here's what that actually looks like in practice.

Why I Stopped Reaching for Frameworks First
For the past two years, I've shipped several production web applications using vanilla JavaScript, Web Components, and modern browser APIs. Not prototypes or side projects—actual client work handling thousands of daily users. The experience has changed how I think about web development complexity.
The decision wasn't ideological. I got tired of fighting build tool configurations, debugging hydration mismatches, and explaining to clients why a simple form needed 400KB of JavaScript. When I finally sat down to evaluate what browsers actually provide in 2026, I realized the gap between framework features and platform capabilities had narrowed significantly.
What Modern Browsers Actually Give You
The platform has grown up. Here's what you get without any dependencies:
Web Components provide real component encapsulation. Custom elements with shadow DOM give you scoped styles and reusable modules without a build step:
class UserCard extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host { display: block; padding: 1rem; border: 1px solid #ddd; }
.name { font-weight: bold; }
</style>
<div class="name">${this.getAttribute('name')}</div>
<div class="email">${this.getAttribute('email')}</div>
`;
}
}
customElements.define('user-card', UserCard);
ES Modules work natively in all major browsers. No bundler required for development, and for production you can use esbuild or Rollup just for minification—a 10-line config instead of webpack.config.js archaeology.
CSS has evolved. Container queries, cascade layers, native nesting, and :has() selector eliminate most CSS-in-JS use cases. Grid and flexbox handle layouts that previously required framework-specific solutions.
Fetch API and async/await replaced the callback soup that made jQuery necessary. Add AbortController for request cancellation and you've got everything most applications need.
Reactivity Without React
The hardest part to replace is reactive state management. Frameworks shine here. But modern JavaScript makes basic reactivity straightforward:
class Store {
#state = {};
#listeners = new Set();
constructor(initialState) {
this.#state = initialState;
}
get(key) {
return this.#state[key];
}
set(key, value) {
this.#state[key] = value;
this.#notify();
}
subscribe(callback) {
this.#listeners.add(callback);
return () => this.#listeners.delete(callback);
}
#notify() {
this.#listeners.forEach(callback => callback(this.#state));
}
}
const userStore = new Store({ name: '', email: '' });
userStore.subscribe(state => {
document.querySelector('#user-name').textContent = state.name;
});
This is 30 lines. It handles 80% of state management needs. For the remaining 20%, you can reach for a library like Preact (3KB) instead of committing to a full framework ecosystem.
Routing and Navigation
Client-side routing without a framework requires the History API:
class Router {
#routes = {};
route(path, handler) {
this.#routes[path] = handler;
}
navigate(path) {
history.pushState(null, '', path);
this.#handleRoute(path);
}
#handleRoute(path) {
const handler = this.#routes[path] || this.#routes['*'];
if (handler) handler();
}
init() {
window.addEventListener('popstate', () => {
this.#handleRoute(location.pathname);
});
this.#handleRoute(location.pathname);
}
}
For complex routing with parameters and nested routes, something like Navigo adds minimal overhead. But many applications have simple enough navigation that this basic implementation works fine.
When Frameworks Still Make Sense
I'm not suggesting frameworks are obsolete. They excel at:
- Large teams with varying skill levels. Frameworks provide guardrails and consistent patterns. Vanilla approaches require more discipline.
- Complex, highly interactive UIs. If you're building something like Figma or Linear, framework-level abstraction pays for itself.
- Ecosystem requirements. If you need a specific library that only works with React, fighting that integration isn't worth it.
But for content-heavy sites, admin panels, marketing pages with interactive elements, and internal tools—the framework often adds more complexity than it removes.
The Build Process I Actually Use
Here's my current setup for production:
- Write ES modules, import from
node_modulesvia import maps for development - Run esbuild for bundling and minification before deploy
- Use CSS nesting and modern features directly
- Let Cloudflare or Netlify handle caching and CDN
No hot module replacement, no complex dev server, no framework CLI. The entire build configuration fits in a package.json script:
{
"scripts": {
"build": "esbuild src/main.js --bundle --minify --outfile=dist/bundle.js"
}
}
Performance Results
The last production app I shipped this way:
- Initial bundle: 45KB gzipped (including all application code)
- Time to Interactive: 1.2s on 3G
- Lighthouse score: 98-100 across all metrics
No code splitting needed. No hydration delay. Just HTML, CSS, and a small JavaScript bundle that runs immediately.
The Real Trade-off
You trade framework conveniences for direct platform knowledge. You'll write more boilerplate. You'll implement features that frameworks give you for free. But you'll also understand exactly what your application does, debug issues faster, and ship dramatically less code.
For many projects, especially those with small teams or solo developers, that trade-off makes sense. The web platform is capable enough now that reaching for a framework should be a deliberate choice, not a default.