Ömer
Özbay

Full-Stack Engineer

LOADING
©2026
Why Plain HTML and Tailwind Is Sometimes the Best Choice — An Honest Analysis
ARCHITECTUREHTMLTailwind CSS

Why Plain HTML and Tailwind Is Sometimes the Best Choice — An Honest Analysis

calendar_todayJUL 13, 2026
schedule7 MIN READ
boltADVANCED LEVEL

Why Plain HTML and Tailwind Is Sometimes the Best Choice — An Honest Analysis

There is a growing counter-movement among senior engineers: stepping away from React, Next.js, Nuxt, and Gatsby for content-heavy websites, and returning to the fundamentals — plain HTML, CSS, and minimal JavaScript.

As a Senior Full Stack Architect who has built production systems in both paradigms, I want to give you an honest, data-driven analysis of when this decision is correct, when it is a mistake, and what the real trade-offs look like.

This is not an "frameworks are bad" argument. This is an architectural decision framework.


The Context: What Triggered This Decision

When I first architected a personal blog-style content site, my instinct was to reach for Next.js. I knew it well, I trusted it for production, and it offered excellent SEO tools. But as I wrote the specification, I kept asking myself one question:

What problem is this framework actually solving for me right now?

For a blog with static markdown content, no real-time data, no complex state management, and high SEO requirements, the honest answer was: the framework was solving problems I did not have.


The Real Cost of a JavaScript Framework for a Static Site

When you deploy a Next.js blog site with default settings, here is what you ship to every single visitor:

| Asset | Approximate Size | |:---|:---| | React runtime | 42kb gzipped | | Next.js client runtime | 95kb gzipped | | React DOM reconciler | 35kb gzipped | | Your actual page content | 5–15kb | | Total overhead | 172–187kb before content |

For a blog post page that is just text and code blocks, you are shipping nearly 15 times the JavaScript needed to read the actual content.

With plain HTML and purged Tailwind:

| Asset | Approximate Size | |:---|:---| | Your CSS (purged Tailwind) | 8–15kb gzipped | | Alpine.js (for dark mode toggle) | 7kb gzipped | | Your actual page content | 5–15kb | | Total overhead | 20–37kb |

That is roughly a 5 to 8 times reduction in payload, which translates directly to faster LCP and lower bandwidth costs.


When Plain HTML and Tailwind Wins

1. The Content Changes Less Than Once Per Week

If your site primarily serves static content — articles, portfolios, documentation — the re-rendering lifecycle, hydration, and JavaScript bundle of a framework adds zero value to the user experience.

A static HTML file served from a CDN has a TTFB of under 20ms globally. A Next.js page even with ISR typically takes 30–100ms more because of runtime overhead.

2. You Have No Interactive State Dependencies Across Components

Dark mode toggle? One script tag with 8 lines of Alpine.js. Mobile navigation? Another 10 lines. Copy-to-clipboard on code blocks? 5 lines.

You do not need React's component tree to coordinate a dark mode toggle across three elements.

<!-- Dark mode in plain HTML — Alpine.js, 8 lines -->
<html x-data="{ dark: false }" :class="{ 'dark': dark }">
  <body>
    <button @click="dark = !dark" aria-label="Toggle dark mode">
      <span x-show="!dark">Dark</span>
      <span x-show="dark">Light</span>
    </button>
  </body>
</html>

3. You Control the Full Build Pipeline

With a custom static site generator — even just a Node.js script that converts markdown to HTML — you have full control over which fonts load and how, which scripts run and when, and exactly what HTML is shipped.

// A complete static site generator in approximately 80 lines of Node.js
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
import { marked } from 'marked';

async function buildPost(filename: string): Promise<void> {
  const raw = await fs.readFile(path.join('./content/posts', filename), 'utf-8');
  const { data: frontmatter, content } = matter(raw);
  const html = await marked(content);

  const page = `<!DOCTYPE html>
<html lang="${frontmatter.locale || 'en'}">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${frontmatter.title}</title>
  <meta name="description" content="${frontmatter.excerpt}">
  <link rel="stylesheet" href="/styles.css">
  <script type="application/ld+json">
    ${JSON.stringify({
      "@context": "https://schema.org",
      "@type": "BlogPosting",
      "headline": frontmatter.title,
      "author": { "@type": "Person", "name": frontmatter.author }
    })}
  </script>
</head>
<body class="max-w-3xl mx-auto px-4 py-12">
  <article>${html}</article>
</body>
</html>`;

  const slug = filename.replace('.md', '');
  await fs.mkdir(path.join('./dist/blog', slug), { recursive: true });
  await fs.writeFile(path.join('./dist/blog', slug, 'index.html'), page);
}

When a Framework Still Wins

To be clear: this is not an anti-framework argument. There are clear scenarios where Next.js or a comparable framework is the correct choice:

| Scenario | Plain HTML | Next.js | |:---|:---|:---| | Static blog, no authentication | Recommended | Works | | Authenticated user dashboard | Not recommended | Recommended | | Content updates multiple times per day | Manual rebuild required | Recommended with ISR | | Complex interactive components | Not practical | Recommended | | E-commerce with cart state | Not practical | Recommended | | Developer documentation site | Recommended | Works | | Portfolio with real-time analytics | Extra work required | Recommended |

The key question is always: does my project have interactive state that must be shared across components?

If yes, use a framework. If no, consider plain HTML seriously.


The Hybrid Approach: What I Actually Use

After extensive testing, my architecture for content-first sites became a hybrid:

  • Next.js App Router for the main shell, routing, and SEO metadata API
  • React Server Components for all static content — blog posts, project pages — zero client JavaScript
  • Minimal client components only for true interactivity (navigation toggle, bookmark feature)
  • Tailwind CSS for utility styling
  • No state management library — no Redux, no Zustand

This gives me the deployment benefits of Vercel, the SEO tooling of Next.js Metadata API, and the performance of essentially static HTML — because React Server Components compile to static HTML.

// This server component ships ZERO JavaScript to the browser
// It renders as pure HTML on the server
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);

  return (
    <article className="max-w-3xl mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
    </article>
  );
  // Result: pure HTML, no JavaScript hydration, PageSpeed 100/100
}

The Architectural Decision Framework

When evaluating any new project, ask these 5 questions in order:

1. Will content change more than once per day?
   Yes  — consider ISR or SSR (framework wins)
   No   — static generation works

2. Does any UI element require shared reactive state?
   Yes  — framework component model is valuable
   No   — Alpine.js or vanilla JavaScript is sufficient

3. Do you need authentication and user sessions?
   Yes  — use a framework with an auth library
   No   — static HTML works perfectly

4. Will the team grow beyond 3 engineers?
   Yes  — framework conventions reduce onboarding friction
   No   — plain HTML is easier to maintain

5. Do you need tight integration with a CMS or API?
   Yes  — framework (Next.js API routes, data fetching)
   No   — build-time markdown processing is simpler

Conclusion: Tools Are Not Identities

The worst engineering decision is choosing a tool because it is what you know, or because it is what the industry defaults to. The best engineering decision is choosing the tool that solves your actual problem with the least complexity.

For a personal blog in 2026, plain HTML and Tailwind is not a step backward — it can be a deliberate, high-performance, low-maintenance architecture that outperforms most framework-based blogs on every metric that matters.

For a complex SaaS dashboard, authentication-heavy platform, or real-time application, Next.js or a comparable framework is absolutely the right choice.

Know your problem. Choose the tool. Then execute it well.

Ömer Özbay
Written By

Ömer Özbay

Full-Stack Engineer specialized in bridging high-performance backend architectures with pixel-perfect frontend experiences. Building the future with AI and modern web technologies.

Why Plain HTML and Tailwind Is Sometimes the Best Choice — An Honest Analysis | Ömer Özbay Blog | Ömer Özbay