M. MAUN STUDIO
Let's Work Together
All posts
Blog3 min read

Building Fast Websites Without a Framework

Modern browsers provide everything you need to build performant, interactive websites. Here's how vanilla JavaScript and web standards can replace most framework boilerplate.

Building Fast Websites Without a Framework

Why Skip the Framework?

Every web project doesn't need React, Vue, or Svelte. For content-heavy sites, admin dashboards, or landing pages, frameworks add bundle size and complexity that slow development and runtime performance. Modern JavaScript and CSS have evolved enough that vanilla approaches often ship faster and maintain better.

I've shipped production sites both ways. Frameworks excel at complex state management and real-time interfaces, but most projects don't need that. A typical marketing site or blog renders static content with light interactivity—exactly what the platform handles natively.

What You Get for Free

Browsers in 2026 support features that used to require libraries:

  • ES modules: import and export work natively, no bundler required for development
  • Template literals: String interpolation and multi-line templates replace most templating engines
  • Fetch API: Clean HTTP requests without axios or similar
  • CSS Grid and Flexbox: Complex layouts without framework component systems
  • Custom elements: Web components for reusable UI without JSX
  • CSS custom properties: Theme variables without preprocessors

The gap between vanilla web and framework features has narrowed significantly.

A Practical Example

Here's a real component pattern I use—a tabbed interface without any framework:

<div class="tabs" data-tabs>
  <nav>
    <button data-tab="overview" class="active">Overview</button>
    <button data-tab="details">Details</button>
    <button data-tab="specs">Specs</button>
  </nav>
  <div data-panel="overview" class="active">
    <p>Overview content here</p>
  </div>
  <div data-panel="details" hidden>
    <p>Details content here</p>
  </div>
  <div data-panel="specs" hidden>
    <p>Specs content here</p>
  </div>
</div>
class Tabs {
  constructor(element) {
    this.root = element;
    this.buttons = element.querySelectorAll('[data-tab]');
    this.panels = element.querySelectorAll('[data-panel]');
    
    this.buttons.forEach(button => {
      button.addEventListener('click', () => this.activate(button.dataset.tab));
    });
  }
  
  activate(tabId) {
    this.buttons.forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tabId));
    this.panels.forEach(panel => {
      const isActive = panel.dataset.panel === tabId;
      panel.toggleAttribute('hidden', !isActive);
      panel.classList.toggle('active', isActive);
    });
  }
}

document.querySelectorAll('[data-tabs]').forEach(el => new Tabs(el));

This weighs under 500 bytes minified. The equivalent React component would pull in the entire runtime—roughly 140KB minified. For users on slow connections, that's the difference between instant interaction and waiting.

State Management

Simple state doesn't need Redux or Zustand. JavaScript objects work fine:

const state = {
  user: null,
  notifications: [],
  
  setUser(userData) {
    this.user = userData;
    this.render();
  },
  
  addNotification(message) {
    this.notifications.push({ id: Date.now(), message });
    this.render();
  },
  
  render() {
    // Update DOM based on current state
    document.getElementById('username').textContent = this.user?.name || 'Guest';
    updateNotificationBadge(this.notifications.length);
  }
};

For reactive updates, Proxy objects can intercept changes and trigger renders automatically, mimicking Vue's reactivity without the framework weight.

When Frameworks Make Sense

This isn't anti-framework. Use them when you need:

  • Real-time collaboration: Google Docs-style interfaces with operational transforms
  • Complex data flows: Deeply nested component hierarchies sharing state
  • Large teams: Framework conventions help coordinate multiple developers
  • Heavy client-side routing: SPAs with dozens of routes and lazy loading

But a blog with a newsletter signup? A restaurant menu site? A portfolio? Those don't need virtual DOM diffing.

Build Process

Vanilla doesn't mean zero tooling. I use:

  • esbuild: Bundle and minify for production, development server for HMR
  • PostCSS: Autoprefixer and CSS nesting when helpful
  • Prettier: Code formatting

No complex webpack configs, no framework CLI. The entire build setup fits in a dozen lines of configuration.

// build.js
import esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/main.js'],
  bundle: true,
  minify: true,
  sourcemap: true,
  outdir: 'dist',
  target: ['es2020'],
});

Performance Results

A recent client project switched from Next.js to vanilla HTML/CSS/JS. Metrics:

  • First Contentful Paint: 0.4s → 0.2s
  • Time to Interactive: 2.1s → 0.5s
  • Total bundle size: 347KB → 23KB
  • Lighthouse score: 78 → 99

Users on 3G connections finally had a usable experience. The code became simpler, fewer abstractions to explain.

The Mental Shift

Frameworks make you think in components and lifecycle hooks. Vanilla makes you think in progressive enhancement:

  1. Build the HTML that works without JavaScript
  2. Add CSS that works without JavaScript
  3. Layer JavaScript for interactivity

This approach naturally creates resilient interfaces. If the JS fails to load, the site still functions.

Conclusion

Frameworks solve real problems for certain projects. But they're not a default choice anymore. The platform has caught up. For many sites, vanilla JavaScript delivers better performance, simpler maintenance, and faster load times.

Next project, try skipping the framework. You might ship something faster in both senses of the word.