0

I'm learning about optimization and I've found that either I've misunderstood something or every article about Vue and the examples are basically wrong.

It's about bundle size and tree shaking.

When I use composable like this—and this pattern is mentioned in every tutorial and probably even on the official Vue.js website—it will be included in its entirety in the resulting bundle, and that's wrong!

Option 1

const counter = ref(0);
export function useCounter() {

  const increment = () => counter.value++;
  const decrement = () => counter.value--;
  return { counter, increment, decrement };
}

// Usage:
import { useCounter } from './counter'
const { increment } = useCounter() // ❌ The bundle contains the ENTIRE useCounter function.

Option 2 - this way, after tree shaking, the bundle only contains what I actually use.

const counter = ref(0);
export const increment = () => counter.value++;
export const decrement = () => counter.value--;

// Usage:
import { increment } from './counter' // ✅ The bundle contains ONLY increment

But option two is not encapsulated, and generally the concept does not always fit.

Otherwise, for example, this is the Vue icon, and here it has composables similarly https://michaelnthiessen.com/inline-composables (listed here as inline, but it can be an external file and imported).

So what is the correct approach? I am confused because I am using it incorrectly.

6
  • It's unclear why there's idea that something needs to be tree-shaken. A composable is inseparable. If you need it to be separated then there should be multiple composables. "Option 2" is a different thing because it's global state. Consider explaining your case if you have one Commented Oct 8 at 8:20
  • Hi thx for answer. Hi, thanks for your reply. I've edited the first example so that the difference isn't so obvious—now both files are "global." The only thing I'm wondering is whether the first method is valid, because I might have a composable with 10 functions and use all of them in project A, but only half of them in project B, and the bundle will contain the entire file (tree shaking won't remove the 5 unused ones), but is that actually necessary? Commented Oct 8 at 8:29
  • part 2: Isn't it unnecessary optimization of the bundle size compared to destroying the composable as a WHOLE? Or is it not, and should I remove unused code on a project where I don't use all the functions from the composable? Commented Oct 8 at 8:30
  • That there are unused functions may have very small impact on minified bundle size, you can check it. But a composable with 10 functions seems to be bloated and asks for refactoring Commented Oct 8 at 9:27
  • 1
    Yes, for an indivisible thing like this one it would be that some functions aren't used but exist in a bundle - but so it would be for the methods of Calculator class, for instance Commented Oct 8 at 10:30

1 Answer 1

3

Tree shaking may depend on a specific tool that provides it, but the general principle is that functions canbe expected to be removed if they are unused:

export function useCounter() {
  const increment = ...;
  const decrement = ...; // Dead code removal
  return { counter, increment };
}

export function useFoo() {...} // Tree-shaken if not used

In example 1, decrement is considered to be used because it's a part of the object that useCounter returns, and so it cannot be tree-shaken. Tree shaking mechanism can't remove object properties because this would lead to unpredictable consequences.

That some parts of useCounter need to be excluded from a bundle means that it needs to be split:

export function useCounterIncrement(counter = ref(0)) {
  const increment = () => counter.value++;
  return { counter, increment };
}

export function useCounterDecrement(counter = ref(0)) {
  const decrement = () => counter.value--;
  return { counter, decrement };
}

And joined back together when necessary:

export function useCounter(counter = ref(0)) {
  return {
    ...useCounterIncrement(counter),
    ...useCounterDecrement(counter),
  };
}

Optional state argument is a convenient pattern that makes it possible. It allows to provide existing state to a composable, whether global or local.

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for the explanation and illustrative example. My example was deliberately simple; it could be much more complex. I understand your example, but at least for me, it leads to very complex code that will be more difficult to maintain and, over time, to understand. But I'm glad I learned something new.
You're welcome. If you have specific code, consider asking here or at codereview.stackexchange.com . Most times composables are just factory functions, "use" determines that they use vue api and should be called in component setup, so general programming concerns apply to them. This always depends on a case, but in general maintainability beats optimization in terms of priority
Hi, Thx again! :) I will remember that.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.