DEV Community

Cover image for Speed up your Vue app: The most unexpected and perhaps silliest way πŸ€ͺπŸš€
Pascal Thormeier
Pascal Thormeier

Posted on

Speed up your Vue app: The most unexpected and perhaps silliest way πŸ€ͺπŸš€

I was experimenting with Vue's v-model the other day. I wanted to figure out if what the Vue docs state about v-model was true. Here's what they say:

Remember that:

<input v-model="searchText">

does the same thing as:

<input 
  v-bind:value="searchText" 
  v-on:input="searchText = $event.target.value">

This phrase says that one can rebuild v-model by setting the value prop of a form control (any, really) and setting the prop to the target value in an event listener for the input event. Simple enough, right?

And at first glance, that's very much true. They do the same thing. Updating the prop from somewhere else updates whatever's written in the input field, and updating the input field updates the prop.

Let's look at the generated code, though, to verify that it's really, actually the same.

The example app we'll work with is the most basic example we can build for this:

<template>
  <div>
    <input
      v-model="someProp"
    >
    {{ someProp }}
  </div>
</template>

<script>
export default {
  data() {
    return {
      someProp: 'Foobar',
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

A simple template with an input field and a text node, the script provides the text with some default value.

Next, we execute npm run build and open up the generated dist/js/app.something.js. Ugh, minified. But nothing my IDE can't handle. Doing a quick Ctrl+F for someProp will show us the relevant part of the code:

return n("div", [n("input", {
  directives: [{
    name: "model",
    rawName: "v-model",
    value: e.someProp,
    expression: "someProp"
  }], 
  domProps: { value: e.someProp }, 
  on: {
    input: function (t) {
      t.target.composing || (e.someProp = t.target.value)
    }
  }
}) // ... ])
Enter fullscreen mode Exit fullscreen mode

We see here the component from above but expressed as render functions. The interesting parts are the domProps and the on keys. They tell us how v-model generated the two-way binding.

Now, let's rebuild the above component to use :value and @input instead:

<template>
  <div>
    <input
      :value="someProp"
      @input="e => someProp = e.target.value"
    >
    {{ someProp }}
  </div>
</template>

<script>
export default {
  data() {
    return {
      someProp: 'Foobar',
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

And do the same shenanigans as before to get the generated code:

return n("div", [n("input", {
  domProps: { value: e.someProp },
  on: {
    input: function (t) {
      return e.someProp = t.target.value
    }
  }
}) // ... ])
Enter fullscreen mode Exit fullscreen mode

Notice the difference? The directives key is missing. That's not that surprising, we don't use the v-model directive after all, right? The only difference in functionality is the missing t.target.composing || in the "self-made" version. We can add that, though:

<input
  :value="someProp"
  @input="e => e.target.composing || (someProp = e.target.value)"
>
Enter fullscreen mode Exit fullscreen mode

What we'll get is this:

return n("div", [n("input", {
  domProps: { value: e.someProp }, 
  on: {
    input: function (t) {
      t.target.composing || (e.someProp = t.target.value)
    }
  }
}) // ... ])
Enter fullscreen mode Exit fullscreen mode

And that's the equivalent to v-model. Without the directives key, though.

And that's the point.

Using the @input+:value approach makes the built JS around 85 bytes smaller (estimate, it may vary as it's dependent on the variable's name, the minimum is 71) per usage of v-model.

Now imagine our app has 100 usages of v-model, that'll be roughly 8.5kb less built JS.

The price is readability, though. Who wants to bloat their templates with event listeners and bindings if they could just use v-model, right? Also, the gain is minimal compared to the amount of annoyance this can cause. So, we probably shouldn't do this.

β˜• I also want to shout out to Andras for supporting me via buymeacoffee! I enjoyed the coffee in the sun on my favorite bench - Just the right thing to relax after a long day. Thank you!


I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a ❀️ or a πŸ¦„! I write tech articles in my free time and like to drink coffee every once in a while.

If you want to support my efforts, you can offer me a coffee β˜• or follow me on Twitter 🐦! You can also support me directly via Paypal!

Buy me a coffee button

Oldest comments (4)

Collapse
 
maxarias profile image
Maximiliano Arias

This seems like something the Vue compiler should handle for the user

Collapse
 
thormeier profile image
Pascal Thormeier

Oh, absolutely! That's why I called it "silly" - it's that kind of micro optimization that shouldn't really be necessary, especially if it's syntactic sugar causing the extra 71 bytes.

Collapse
 
mod5ied profile image
Patrick

This is a nice approach to understanding what really happens under the hood Pascal.
Nice one πŸ’ͺ

Collapse
 
areskul profile image
Areskul

Yeah! Good to raise the fact that it slightly decreases redability! Unfortunately, due to the figures,.. it won't stop me from refactoring my code