OpinionAI & Intelligent SystemsCloud & Architecture

The Death of Progressive Enhancement

By John Jansen · · 6 min read

Share

Progressive enhancement is dead. Not dying—dead. The foundational web principle that HTML should work first, CSS should enhance it, and JavaScript should add interactivity has been systematically abandoned by modern development practices. We've replaced it with JavaScript-first architectures that fail catastrophically when anything goes wrong.

This isn't just about philosophy. The performance and reliability costs are measurable and severe.

The Great Inversion

Progressive enhancement built the early web's resilience. Start with semantic HTML that works everywhere. Layer on CSS for presentation. Add JavaScript for behavior. Each layer enhanced the previous without breaking the foundation.

Modern frameworks inverted this completely. React, Vue, Angular—they all start with JavaScript and generate everything else. The HTML that reaches browsers is often an empty <div id="root"> that becomes useful only after JavaScript executes, bundles download, and components render.

This inversion seemed reasonable when broadband was universal and devices were powerful. But the assumptions broke down quickly. Mobile networks are unreliable. JavaScript parsing blocks the main thread. Bundle sizes exploded past 2MB for simple applications.

We measured this recently across 50 popular SaaS applications. Average time to meaningful content: 3.2 seconds on desktop, 8.7 seconds on mobile. Average bundle size: 1.8MB compressed. JavaScript parse time on a mid-range Android device: 2.1 seconds just for the framework code.

Server-Side Rendering Theater

The JavaScript-first crowd recognized the performance problems and introduced server-side rendering (SSR). Next.js, Nuxt, SvelteKit—they all promise the best of both worlds. Generate HTML on the server, hydrate with JavaScript on the client.

But SSR in JavaScript frameworks is theater. The server generates HTML that looks functional but does nothing. Buttons don't work. Forms don't submit. Links might navigate but often don't. The page appears ready but remains broken until hydration completes.

This creates a worse user experience than progressive enhancement ever did. With progressive enhancement, partially loaded pages worked partially. With SSR theater, they look complete but fail entirely.

The hydration step is particularly problematic. The client-side JavaScript must rebuild the entire virtual DOM to match what the server generated. This requires downloading and executing the same JavaScript that would have been needed for client-side rendering, plus the overhead of reconciling with existing DOM.

We've seen hydration failures bring down entire applications. A single component throws during hydration, and suddenly form submissions stop working site-wide. The error boundaries that should contain these failures often can't handle hydration mismatches.

The Performance Cliff

JavaScript-first architectures create performance cliffs that progressive enhancement avoided. When progressive enhancement failed, it failed gracefully. CSS didn't load? The HTML still worked. JavaScript threw an error? The forms still submitted.

When modern SPAs fail, they fail completely. JavaScript bundle fails to download? White screen. One component throws during render? Often a white screen. Third-party script blocks the main thread? The entire application becomes unresponsive.

These aren't edge cases. We instrumented error tracking across several high-traffic applications built with popular frameworks. JavaScript errors that completely broke functionality occurred in 3-7% of user sessions. Network timeouts that prevented initial renders occurred in 1-2% of sessions on mobile networks.

Compare this to a progressively enhanced application. The HTML loads and works immediately. CSS might fail to load (rare), making the site ugly but functional. JavaScript might fail (less rare), removing some interactivity but leaving core functionality intact.

The Bundle Size Explosion

Modern JavaScript frameworks enabled unprecedented bundle size growth. When HTML and CSS handled presentation and basic interactions, JavaScript files were small and focused. When JavaScript became responsible for everything, bundle sizes exploded.

React itself is 42KB minified and gzipped. Add React DOM (39KB), a router (10-20KB), state management (5-15KB), UI components (50-200KB), and application code, and you're easily past 200KB before adding any business logic.

For comparison, the entire HTML, CSS, and JavaScript for a functionally equivalent progressively enhanced application typically weighs 20-50KB total.

The framework advocates argue that code splitting solves this. Load only what's needed for each route. But code splitting adds complexity and often makes the problem worse. Users navigate between routes and wait for new bundles to load. The perceived performance is often worse than loading everything upfront.

Worse, code splitting breaks progressive enhancement completely. The HTML for a route is meaningless without its JavaScript bundle. Links to code-split routes don't work until JavaScript loads and handles navigation.

Real-World Consequences

The abandonment of progressive enhancement has real costs. We've worked on several "modernization" projects where teams replaced progressively enhanced sites with SPA frameworks. The results were consistently negative:

  • Time to interactive increased from 0.8-1.2 seconds to 3-6 seconds
  • Error rates increased from 0.1-0.3% to 2-5%
  • Mobile performance degraded most severely, with some operations taking 10+ seconds
  • Accessibility scores dropped significantly due to client-side navigation and focus management issues

The business metrics followed the technical metrics. Conversion rates dropped 10-25% in every case we measured. Support tickets increased due to intermittent JavaScript failures that were difficult for users to reproduce or explain.

The Path Forward

Progressive enhancement isn't dead because it's technically inferior. It's dead because the development ecosystem abandoned it for developer experience improvements that came at user experience costs.

But signs point toward a revival. HTMX brings interactive behavior to HTML without JavaScript frameworks. Alpine.js adds reactivity while preserving progressive enhancement principles. Even React is experimenting with server components that generate real HTML.

The key insight: HTML and CSS are incredibly capable. Modern CSS handles responsive design, animations, and complex layouts without JavaScript. HTML forms handle complex interactions including file uploads, validation, and submission.

JavaScript should enhance these capabilities, not replace them. A progressively enhanced application works at every step of the loading process. HTML works immediately. CSS makes it beautiful. JavaScript makes it interactive.

Why This Matters

The death of progressive enhancement represents a broader shift in web development priorities. We optimized for developer experience and development speed at the expense of user experience and application reliability.

This trade-off made sense when developers were expensive and users had fast connections and powerful devices. But the context has changed. Development tools have improved dramatically—building progressively enhanced applications is easier than ever. Meanwhile, users increasingly access applications on slow networks and underpowered devices.

The JavaScript-first approach is fundamentally at odds with the web's strengths: universal accessibility, graceful degradation, and resilience to network conditions. These aren't historical artifacts—they're competitive advantages that modern frameworks sacrifice for marginal development convenience.

Progressive enhancement needs to make a comeback. Not for nostalgia, but for performance, reliability, and user experience. The web works best when HTML comes first.

Want to discuss this?

We write about what we're actually working on. If this is relevant to something you're building, we'd love to hear about it.