When to use Vue or Petite-Vue?

Petite-Vue is an alternative version of the Vue framework. It shares a lot of the same ideas but is significantly smaller package. When using Vue 3 without a build step you have to load a ~60kb package and ~30kb when using a build step (recommended). Petite-Vue is just ~6kb.

The point of petite-vue is not just about being small. It's about using the optimal implementation for the intended use case (progressive enhancement).

Although being namesake and very similar, you can't really use them in the same way. With Petite-Vue you can't use all the plugins available. You'll have to find the VanillaJS version of whatever slider or drag&drop you want and implement it yourself in your programs.

Use case.

Most Vue (or React/Svelte) tutorials will show you how to create a Single Page Application with its own router and views but in real life you might work with an existing PHP/Python/.NET Multi Page Application. It doesn't really make sense to rewrite all that to a SPA. So you'll progressively enchance your existing views.

This is the use case we'll dive into with a focus to the developer experience.

Examples.

For Vue components I'll use the Options API which is applicable to Vue 2 & 3. It is actually more similar to the Petite-Vue syntax than the Composition API. For these examples we'll use a very minimal html base with only loading the respective Vue packages and mounting into a div with and id "app".

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue Demo</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

To avoid repetition, only fragments of the full html is shown. To open the apps I use the lite-server npm package. If you are trying these examples, use separate base files.

Initialation

Now, we are using cdn sources but you should propably host the files yourself when in production for integrity reasons.

Petite-Vue:

<div id="app" @vue:mounted="init()"></div>
<script src="https://unpkg.com/petite-vue"></script>
<script>
    const { createApp } = window.PetiteVue;

    createApp({
        init() {
            console.log('Petite-Vue app mounted!')
        }
    }).mount('#app')
</script>

Vue:

With Vue it is easier to use the importmap and module syntax.

<div id="app"></div>
<script type="importmap">
    { 
        "imports": {
            "vue": "https://cdnjs.cloudflare.com/ajax/libs/vue/3.2.32/vue.esm-browser.min.js"
        }   
    }
</script>
<script type="module">
    import { createApp } from 'vue';

    createApp({
        methods: {
            init() {
                console.log('Vue 3 mounted!')
            }
        },
        mounted() {
            this.init()
        }
    }).mount('#app')
</script>

We already see some differences in the syntax. Both rely on plain JS objects but the biggest difference is that the Petite-Vue doesn't have any keywords like the Options API has specific keywords on the root level. Also the lifecycle events are called similarly like @click.

Templates

Petite-Vue:

To display a message from a variable in the template we just add the variable to the app object root and surround it with curly braces in the template. Also we have to add v-scope attribute to the #app element to define the scope where the variables are accessible.

<div id="app" @vue:mounted="init()" v-scope>
    <h1>{{ message }}</h1>
</div>
<script>
    const { createApp } = window.PetiteVue;

    createApp({
        message: 'We are using Petite-Vue!',
        init() {
            console.log('Petite-Vue app mounted!')
        }
    }).mount('#app')
</script>

Vue:

The same in Vue is a bit different with the Options API. The message variable has to be inside a data object to be visible in the template. No need for v-scope here but the curly braces templating is identical.

<div id="app">
    <h1>{{ message }}</h1>
</div>
<script type="module">
    import { createApp } from 'vue';

    createApp({
        data: () => ({
            message: 'We are using Vue 3!'
        }),
        methods: {
            init() {
                console.log('Vue 3 mounted!')
            }
        },
        mounted() {
            this.init()
        }
    }).mount('#app')
</script>

Components

Let's create a simple counter component where you click a button to add a count.

Petite-Vue

We use the v-scope attribute again here but now to define the scope of the PetiteCounter component.

<div id="app" @vue:mounted="init()" v-scope>
    <h1>{{ message }}</h1>
    <div v-scope="PetiteCounter({})">
        <p>The count is officially: {{ count }}</p>
        <p>Count multiplied: {{ multiplyCount }}</p>
        <button @click="add">Add to count</button>
    </div>
</div>

The component object is wrapped inside a function. It has the same syntax and idea that the root object has. To have a computed property like the multiplyCount here, just use plain JS getter function.

function PetiteCounter(props) {
    return {
        count: 0,
        get multiplyCount() {
            return this.count * 2;
        },
        add() {
            this.count += 1;
        }
    }
}

Add the component function to the root object like you would any other variable.

createApp({
    PetiteCounter,
    ...
})

Vue

In the html template components are introduced in kebab-case where as the JS objects are declared in CamelCase.

<div id="app">
    <h1>{{ message }}</h1>
    <example-counter></example-counter>
</div>

Vue component has to have its own template inside the component object. Again this will repeat the same Options API rules as before in the root app. The computed properties are defined inside a specific computed object as a function in the same manner as the getter function in a plain JS object. Just without the get keyword.

const ExampleCounter = {
    template: `<div>
        <p>The count is officially: {{ count }}</p>
        <p>Count multiplied: {{ multiplyCount }}</p>
        <button @click="add">Add to count</button>
    </div>`,
    data: () => ({
        count: 0,
    }),
    computed: {
        multiplyCount() {
            return this.count * 2;
        }
    },
    methods: {
        add() {
            this.count += 1;
        }
    }
}

In Vue Options API we introduce components by declaring them in the specific components object.

createApp({
    components: {
        ExampleCounter,
    },
    ...
})

The Petite-Vue really seems more simpler but the Vue version could be used in a project that is powered by the build step.

Conclusion.

It is really tempting just to use regular Vue after all. Often times even prototypes and minimal viable products end up growing to be the actual product at least partially even though it is highly not recommended. You'll wish you could reuse some of the code after all.

And in the case of MPA, it is possible to configure your build systems to have the view codes in the same project but make the distribution targets multiple. So you can convert your Vue views to use the build step and all the tree shaking available.

I also might be quite biased having used regular Vue with and without build step. I'd argue that in most web applications it works just fine. Petite-Vue is still much more powerful than VanillaJS so it could be great in environments where file sizes really have to be small.

Have something to say on this topic? Here's my Twitter: @opiispanen