A Vue island, in Aegis

The case: a server-rendered page with one Vue component mounted into a <div id="app">, built with Vite, shipping vue.runtime plus the component. The same component in Aegis is one file, no build, and the server markup is the first render.

Where aegis comes from. Every snippet below imports from 'aegis'. That bare name is resolved by the import map in your base template — one block, copied once, pinned to a version with an integrity hash: see getting started. With a bundler it resolves from node_modules; with neither, import https://aegisjs.com/0.7.0/aegis.min.js directly.

Before#

<div id="likes" data-count="12"></div>
<script type="module" src="/dist/likes.js"></script>

likes.js (compiled from a .vue single-file component by Vite):

import { createApp, ref } from 'vue';
import Likes from './Likes.vue';
const el = document.getElementById('likes');
createApp(Likes, { count: Number(el.dataset.count) }).mount(el);

Likes.vue:

<script setup>
import { ref } from 'vue';
const props = defineProps({ count: Number });
const count = ref(props.count);
const pending = ref(false);
async function like() {
    count.value++; pending.value = true;
    try { await fetch(`/api/like`, { method: 'POST' }); }
    catch { count.value--; }
    finally { pending.value = false; }
}
</script>
<template>
    <button :disabled="pending" @click="like">{{ count }} likes</button>
</template>

After#

<div data-aegis="likes" data-count="12"><button>12 likes</button></div>

<script type="module">
import { island, mutation, api } from 'aegis';

island('likes', ({ props, signal, html }) => {
    const count = signal(props.count);
    const like = mutation(() => api.post('/api/like'), {
        optimistic: () => { count.value++; },
        onError: () => { count.value--; },
    });
    return html`<button ?disabled=${like.pending} @click=${like}>${count} likes</button>`;
}, { types: { count: Number } });
</script>

The diff, line by line#

Vue Aegis
createApp(Component, props).mount(el) per island island(name, setup, { types }) once; every [data-aegis="likes"] on the page mounts, including ones inserted later
<div id="likes"></div> is empty until JavaScript runs the server renders the first view inside the host; the island replaces it in place
defineProps + Number(el.dataset.count) by hand { types: { count: Number } } coerces data-*
ref() signal()
{{ count }} in a template compiled at build time ${count} in `html```, parsed once at runtime, CSP-safe
pending + try/catch/finally by hand mutation() with optimistic / onError and a pending signal
Vite, vue.runtime, .vue compiler one file, no build; the pinned URL with a hash

What you give up#

  • Single-file components with scoped CSS: Aegis has `css``` for adopted stylesheets, not the SFC format.
  • Vue's ecosystem: component libraries, Pinia, vue-router, DevTools. Aegis has its own router, cache and an in-page inspector, but not the library shelf.
  • Petite-Vue, if that is what you used: it is smaller than Aegis. Move only if the component needs data, forms or optimistic updates.