| |

Vue.js 1 🟢 Single Page Application Architecture and Progressive Enhancement

Vue.js is a progressive JavaScript framework for building user interfaces. The word “progressive” is not marketing language; it describes an architectural property that few other mainstream frameworks have in quite the same way. Vue can be added to an existing HTML page as a single script tag without a build step, or it can power a full single-page application with routing, state management, and server-side rendering. The same framework covers the entire spectrum from “sprinkle a little interactivity onto a server-rendered page” to “build a complex application that owns the entire frontend.”

The two ends of that spectrum are the subject of this chapter. A single-page application uses Vue to control the entire page, fetch data through APIs, and handle navigation without reloads. Progressive enhancement uses Vue to add interactivity to markup that the server already rendered, with a baseline experience that works even if JavaScript fails to load. These are not competing philosophies; they are two positions on a continuum that Vue was explicitly designed to span.

Understanding both is understanding what makes Vue different from frameworks that force you to commit to an architecture before you write a line of code. This chapter covers the SPA model, the progressive enhancement model, the tooling that supports each, and the criteria for choosing between them.

Key point: A single-page application uses Vue to control the entire page, fetch data through APIs, and handle navigation without reloading. Progressive enhancement uses Vue to add interactivity to server-rendered HTML, with the baseline experience working without JavaScript. Vue supports both because its core is a view layer that can be adopted incrementally.


Why the SPA versus progressive enhancement distinction matters

The architecture-first problem. Most frontend frameworks assume a specific architecture. React expects a client-side application. Angular expects a similar model. These frameworks can be used for progressive enhancement, but it is not their natural mode. Vue was designed from the beginning to support both, which means the architectural choice can be deferred, or made differently for different parts of the same application.

The performance problem. A pure client-side SPA has a measurable cost. The browser receives an empty HTML shell, waits for the JavaScript bundle to download and execute, and only then renders content. This delays the Largest Contentful Paint metric and the perceived load time. Progressive enhancement addresses this by sending rendered HTML first and layering interactivity on top.

The SEO problem. Search engines can execute JavaScript, but it is slower and less reliable than parsing static HTML. An application that renders everything client-side may be indexed poorly. Server-side rendering and static generation solve this, and Vue provides APIs for both.

The accessibility problem. A baseline of HTML that works without JavaScript is more accessible. Users with older browsers, users with JavaScript disabled, and assistive technologies all benefit from a page that renders its content before the JavaScript loads.

The incremental adoption problem. A team with a large server-rendered application cannot rewrite it as an SPA overnight. Progressive enhancement allows Vue to be adopted one component at a time, with each adoption delivering value immediately.


a. The SPA architecture

A single-page application is an architecture where Vue controls the entire page, handles data fetching, and manages navigation without reloading. The browser loads the application once, and subsequent navigation updates the page through client-side routing.

The architecture has five components:

ComponentRole
Vue coreRendering and reactivity
Vue RouterClient-side navigation
State managementShared application state
API layerData fetching from the backend
Build toolingBundling, optimization, development server

The backend exposes API endpoints that return JSON. The frontend fetches the data and renders it. The server does not render HTML for application routes; it returns the same index.html for every route, and the router handles the rest.

import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import routes from './routes';

const router = createRouter({
  history: createWebHistory(),
  routes,
});

createApp(App).use(router).mount('#app');

The createWebHistory function uses the browser’s History API, which allows the URL to change without a full page reload. The router matches the URL to a component and renders it in the <router-view>.

The benefits of the SPA architecture are rich interactivity, deep session state, and fast navigation after the initial load. The costs are the initial load time, the SEO challenges, and the complexity of managing client-side state.


b. Progressive enhancement

Progressive enhancement is a design philosophy that provides a baseline of content and functionality to all users while delivering an enhanced experience to users of modern browsers. The baseline is server-rendered HTML that works without JavaScript. The enhancement is the interactivity that Vue adds when it loads.

The pattern proceeds in four stages:

  1. The server renders the HTML, including any Vue directives and bindings embedded in the markup.
  2. Users without JavaScript see the rendered content and can use basic HTML interactions.
  3. Users with modern browsers download the Vue runtime, and the embedded directives become active.
  4. The page becomes reactive, and the interactivity that was dormant is now available.

A form that works without JavaScript and is enhanced with Vue:

<form action="/api/subscribe" method="POST" @submit.prevent="handleSubmit">
  <label for="email">Email Address:</label>
  <input type="email" id="email" name="email" v-model="email" required>
  <button type="submit">Subscribe</button>
</form>

Without JavaScript, the form submits to /api/subscribe via a standard POST request. With JavaScript and Vue, the @submit.prevent modifier stops the default submission, and the handleSubmit method sends the data via AJAX.

For this pattern, Vue provides petite-vue, a lightweight distribution optimized specifically for progressive enhancement. It is approximately 7KB minified and gzipped, uses the same template syntax and reactivity model as standard Vue, and is designed to be sprinkled onto existing HTML.

<script type="module">
  import { createApp } from 'https://unpkg.com/petite-vue?module';

  createApp({
    addToCart(productId) {
      // call platform-specific add to cart API
    }
  }).mount('#product');
</script>

<span @click="addToCart('123')">Add to cart</span>

The petite-vue distribution does not use a virtual DOM and reuses the existing markup, which means it can activate interactivity without re-rendering the page.


c. The spectrum between SPA and progressive enhancement

Vue’s progressive nature means the SPA and progressive enhancement approaches are not binary. They are endpoints on a spectrum, and a single application can move between them.

ApproachVue controlsDataNavigationBuild step
Progressive enhancementSpecific elementsEmbedded in HTMLFull page reloadsOptional
HybridPart of the pageAPI and embeddedClient-side for some routesRecommended
SPAEntire pageAPIClient-side routingRequired

The hybrid approach uses a server-rendered shell for the initial load and mounts a Vue application for the interactive parts. This is the pattern used by applications that need both SEO and rich interactivity.

Vue also supports building custom elements (Web Components) that can be embedded in any HTML page, regardless of how it is rendered. This allows Vue components to be used in legacy applications, static HTML, or applications built with other frameworks.


d. The SPA tooling ecosystem

Building a modern SPA with Vue involves a toolchain that covers routing, state, and build optimization.

Vue Router provides client-side navigation. It maps URLs to components and supports nested routes, route parameters, and navigation guards.

State management is handled by Pinia, the official recommendation. Pinia provides a store that holds shared state and exposes it to components throughout the application.

Vite is the build tool. It provides a development server with hot module replacement and a production build that bundles and optimizes the application.

Nuxt is a higher-level framework built on Vue that provides SSR, SSG, file-based routing, and a full-stack development model. It is the recommended approach for applications that need server rendering or static generation.

TypeScript is supported throughout the ecosystem. The create-vue scaffolding tool can generate a project with TypeScript, and Vue’s Composition API is fully typed.


e. Choosing the right architecture

The choice between SPA and progressive enhancement depends on the application’s requirements.

RequirementRecommendation
Rich interactivity, deep sessionsSPA
SEO-critical contentSSR or SSG
Existing server-rendered HTMLProgressive enhancement
Mixed content and interactivityHybrid or SSG with partial hydration
Simple interactivity on static pagesCDN Vue or petite-vue
Full-stack JavaScriptNuxt

A marketing website with a contact form is a candidate for progressive enhancement. A dashboard with complex state is a candidate for SPA. A content site with some interactive elements is a candidate for static generation with partial hydration, where the static content is pre-rendered and the interactive components are hydrated on the client.

The key insight is that Vue does not force the choice. The same framework, the same template syntax, and the same reactivity model apply across the entire spectrum. The architecture is a decision about how the application is delivered, not a decision about which framework to use.


Complete Example Session

<!-- ============================================
PART 1: PROGRESSIVE ENHANCEMENT — NO JAVASCRIPT
============================================ -->
<form action="/api/subscribe" method="POST">
  <label for="email">Email Address:</label>
  <input type="email" id="email" name="email" required>
  <button type="submit">Subscribe</button>
</form>
<!-- Works without JavaScript: submits via POST -->
<!-- ============================================
PART 2: PROGRESSIVE ENHANCEMENT — WITH VUE
============================================ -->
<form action="/api/subscribe" method="POST" @submit.prevent="handleSubmit">
  <label for="email">Email Address:</label>
  <input type="email" id="email" name="email" v-model="email" required>
  <button type="submit">Subscribe</button>
</form>
<!-- With Vue: AJAX submission, no page reload -->
<!-- ============================================
PART 3: MOUNTING PETITE-VUE
============================================ -->
<script type="module">
  import { createApp } from 'https://unpkg.com/petite-vue?module';

  createApp({
    email: '',
    handleSubmit() {
      fetch('/api/subscribe', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: this.email }),
      });
    },
  }).mount('#subscribe-form');
</script>
// ============================================
// PART 4: SPA ENTRY POINT
// ============================================
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import routes from './routes';

const router = createRouter({
  history: createWebHistory(),
  routes,
});

createApp(App).use(router).mount('#app');
// ============================================
// PART 5: ROUTE DEFINITIONS
// ============================================
// routes.js
export default [
  { path: '/', component: () => import('./pages/Home.vue') },
  { path: '/about', component: () => import('./pages/About.vue') },
  { path: '/tasks', component: () => import('./pages/Tasks.vue') },
];
// ============================================
// PART 6: PINIA STORE
// ============================================
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useCartStore = defineStore('cart', () => {
  const items = ref([]);
  const count = computed(() => items.value.length);

  function add(item) {
    items.value.push(item);
  }

  return { items, count, add };
});
<!-- ============================================
PART 7: SPA COMPONENT
============================================ -->
<script setup>
import { useCartStore } from './stores/cart';
const cart = useCartStore();
</script>

<template>
  <p>Items in cart: {{ cart.count }}</p>
  <button @click="cart.add({ id: 1, name: 'Widget' })">
    Add to cart
  </button>
</template>
// ============================================
// PART 8: CUSTOM ELEMENT
// ============================================
import { defineCustomElement } from 'vue';
import Counter from './Counter.ce.vue';

customElements.define('my-counter', defineCustomElement(Counter));
<!-- ============================================
PART 9: USING THE CUSTOM ELEMENT
============================================ -->
<my-counter></my-counter>
<script src="/my-counter.js"></script>
<!-- Works in any HTML page -->
# ============================================
PART 10: BUILD FOR PRODUCTION
============================================ #
npm run build
# Vite produces the SPA bundle
# or the SSR/SSG output if Nuxt is used

These ten parts cover the no-JavaScript baseline, the Vue-enhanced version, the petite-vue mount, the SPA entry point, route definitions, a Pinia store, an SPA component, a custom element definition, custom element usage, and the production build.


Quick Reference

Architecture Comparison

AspectSPAProgressive Enhancement
Vue controlsEntire pageSpecific elements
Initial HTMLEmpty shellServer-rendered content
Data sourceAPIEmbedded in HTML or API
NavigationClient-side routingFull page reloads
Build stepRequiredOptional
SEORequires SSR/SSGNative
Best forDashboards, appsContent sites, forms

Vue Distributions

DistributionSizeUse case
Full build~34KBSPAs, complex apps
Runtime-only~23KBSPAs with pre-compiled templates
petite-vue~7KBProgressive enhancement

SPA Tooling

ToolPurpose
Vue RouterClient-side navigation
PiniaState management
ViteBuild tool
NuxtSSR and SSG framework
TypeScriptType safety

Rendering Modes

ModeHTML sourceHydration
CSREmpty shellFull
SSRServer-renderedFull
SSGPre-renderedFull or partial
ProgressiveServer-renderedPartial

Architecture Decision

RequirementApproach
SEO-criticalSSR, SSG, or progressive
Rich interactivitySPA
Existing HTMLProgressive enhancement
MixedHybrid or partial hydration
No build stepCDN Vue or petite-vue

Best Practices

✅ Do This:

<!-- Provide a no-JavaScript baseline -->
<form action="/api/subscribe" method="POST">
<!-- Enhance with Vue -->
<form action="/api/subscribe" method="POST" @submit.prevent="handleSubmit">
// Use petite-vue for progressive enhancement
import { createApp } from 'https://unpkg.com/petite-vue?module';
// Use Vue Router for SPA navigation
const router = createRouter({ history: createWebHistory(), routes });
// Use Pinia for shared state
export const useCartStore = defineStore('cart', () => { });

❌ Don’t Do This:

<!-- Force JavaScript for basic content -->
<div id="app"></div>  <!-- ❌ empty shell for a content page -->
// Use the full Vue build for a single interactive element
import { createApp } from 'vue';  // ❌ use petite-vue instead
// Fetch data in a presentational component
// ❌ keep data fetching in the page or store
// Skip SSR for an SEO-critical SPA
// ❌ use Nuxt or SSR

Common Pitfalls

PitfallWhy It HappensFix
Blank page without JSSPA with no baselineAdd server-rendered HTML
Poor SEOClient-side rendering onlyUse SSR or SSG
Slow initial loadLarge bundleCode splitting, SSR, or petite-vue
Hydration mismatchServer and client render differentlyEnsure deterministic rendering
Over-engineeringSPA for a content pageUse progressive enhancement
Under-engineeringProgressive enhancement for a dashboardUse SPA architecture

Real-World Examples

1. Content Site with SPA Shell

const router = createRouter({
  history: createWebHistory(),
  routes: [{ path: '/', component: Home }],
});

2. Interactive Form

<form action="/api/subscribe" method="POST" @submit.prevent="handleSubmit">

3. Petite-Vue Mount

createApp({ count: 0 }).mount('#counter');

4. Store with Pinia

export const useCartStore = defineStore('cart', () => {
  const items = ref([]);
  return { items };
});

5. Lazy-Loaded Route

{ path: '/tasks', component: () => import('./pages/Tasks.vue') }

6. Custom Element

customElements.define('my-counter', defineCustomElement(Counter));

7. SSR with Nuxt

// nuxt.config.ts
export default defineNuxtConfig({ ssr: true });

8. SSG with Nuxt

// nuxt.config.ts
export default defineNuxtConfig({ ssr: true, nitro: { preset: 'static' } });

9. Partial Hydration

// Only interactive components hydrate; static content stays static

10. CDN Vue

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

Visual

The Architecture Spectrum

┌──────────────────────────────────────────────────────────────┐
│  PROGRESSIVE         HYBRID              SPA                 │
│  ENHANCEMENT                                                 │
│  ─────────────       ─────────           ─────────           │
│  Server HTML         Server shell        Empty shell         │
│  + Vue elements      + Vue app           + Vue app           │
│                                                              │
│  petite-vue          Vue + Router        Vue + Router        │
│  Full reloads        Mixed routing       Client routing      │
│                                                              │
│  ◀─── less JS ──────────────────────── more JS ───▶        │
└──────────────────────────────────────────────────────────────┘

SPA Request Flow

┌──────────────────────────────────────────────────────────────┐
│  Browser requests /tasks                                     │
│       │                                                      │
│       ▼                                                      │
│  Server returns index.html (empty shell)                     │
│       │                                                      │
│       ▼                                                      │
│  Browser downloads and executes the bundle                   │
│       │                                                      │
│       ▼                                                      │
│  Router matches /tasks → Tasks component                     │
│       │                                                      │
│       ▼                                                      │
│  Component fetches data from /api/tasks                      │
│       │                                                      │
│       ▼                                                      │
│  Component renders                                           │
└──────────────────────────────────────────────────────────────┘

Progressive Enhancement Flow

┌──────────────────────────────────────────────────────────────┐
│  Browser requests /subscribe                                 │
│       │                                                      │
│       ▼                                                      │
│  Server returns rendered HTML with form                      │
│       │                                                      │
│       ▼                                                      │
│  Content is visible and usable immediately                   │
│       │                                                      │
│       ▼                                                      │
│  Vue runtime downloads and mounts                            │
│       │                                                      │
│       ▼                                                      │
│  Directives become reactive                                  │
│       │                                                      │
│       ▼                                                      │
│  Form now submits via AJAX                                   │
└──────────────────────────────────────────────────────────────┘

Rendering Modes

┌──────────────────────────────────────────────────────────────┐
│  CSR:                                                        │
│  Server ──▶ empty HTML ──▶ browser renders                 │
│                                                              │
│  SSR:                                                        │
│  Server ──▶ rendered HTML ──▶ browser hydrates             │
│                                                              │
│  SSG:                                                        │
│  Build ──▶ static HTML ──▶ CDN ──▶ browser hydrates       │
│                                                              │
│  Progressive:                                                │
│  Server ──▶ rendered HTML ──▶ Vue enhances specific parts  │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
SPAVue controls the entire page, client-side routing, API data
Progressive enhancementVue enhances server-rendered HTML, baseline works without JS
petite-vueLightweight distribution for progressive enhancement (~7KB)
SSRServer renders HTML, client hydrates
SSGPre-rendered at build time, served as static files
Vue RouterClient-side navigation for SPAs
PiniaState management for SPAs
ViteBuild tool for SPAs
NuxtHigher-level framework for SSR and SSG
Custom elementsVue components as Web Components

Key takeaways:

  • Vue is a progressive framework. It can be adopted incrementally, from a single script tag on an existing page to a full SPA with routing and state management.
  • SPA architecture uses Vue to control the entire page. The backend exposes APIs, and the frontend handles data fetching and navigation without reloads. This provides rich interactivity but has initial load and SEO costs.
  • Progressive enhancement starts with server-rendered HTML. The baseline works without JavaScript, and Vue adds interactivity when it loads. The petite-vue distribution is optimized for this pattern.
  • The choice is a spectrum, not a binary. A single application can use progressive enhancement for some parts and SPA architecture for others. The hybrid approach combines server-rendered shells with client-side interactivity.
  • SSR and SSG solve the SPA load-time and SEO problems. Server-side rendering sends rendered HTML to the client, and static site generation pre-renders pages at build time. Nuxt provides both.
  • The tooling ecosystem supports the full spectrum. Vue Router, Pinia, Vite, and Nuxt cover the SPA, SSR, and SSG use cases. The choice of architecture determines which tools are needed.
  • Custom elements allow Vue in any page. Vue components can be compiled to Web Components and embedded in legacy applications, static HTML, or applications built with other frameworks.

Remember: Vue’s defining characteristic is that it does not force an architectural choice. The same framework that powers a complex single-page application can add a dropdown to a server-rendered page. This is what “progressive” means: the framework grows with the application’s needs, and the adoption can be incremental. The SPA architecture is appropriate when the application needs rich client-side interactivity and deep session state. Progressive enhancement is appropriate when the content must be available to all users and the interactivity is an enhancement. The spectrum between them is where most real applications live.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!