# Why You Might Want to Avoid Vue Script Setup in Vue 3 Projects

> Discover potential drawbacks of Vue script setup in Vue 3. Learn how it can limit organization, break tooling, and complicate debugging in large projects.

- Repository: [Vue/core](https://github.com/vuejs/core)
- Tags: deep-dive
- Published: 2026-02-16

---

**Vue script setup introduces strict compiler-time constraints that can limit code organization, break legacy tooling, and complicate debugging in large or mixed-type codebases.**

While vue script setup offers a concise syntax for writing Vue 3 components, it is fundamentally compiler sugar that enforces a rigid contract. The Vue core team explicitly designed the compiler-sfc package to reject certain patterns that are valid in standard `<script>` blocks. Understanding these limitations—enforced in files like [`packages/compiler-sfc/src/compileScript.ts`](https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/compileScript.ts)—helps you decide whether the convenience outweighs the architectural constraints for your specific project.

## The Compiler Contract and Architectural Constraints

Vue script setup is not interpreted at runtime; it is transformed into a standard `setup()` function during compilation. This transformation requires strict static analysis, which imposes several non-negotiable rules.

### No External Source Files (src Attribute)

Unlike standard `<script>` blocks, you cannot use a `src` attribute to externalize a vue script setup block into a separate file. The parser explicitly validates this in [`packages/compiler-sfc/src/parse.ts`](https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/parse.ts) at lines 237-246, throwing an error if a `src` attribute is detected.

This limitation prevents you from sharing common setup logic across components via external files or from code-splitting large setup functions. You must either duplicate code or move shared logic into composables imported within the block.

### No ES Module Exports

Vue script setup forbids any `export` statements. The compiler checks for this in [`packages/compiler-sfc/src/compileScript.ts`](https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/compileScript.ts) at lines 682-686, erroring if it encounters an export declaration.

This means you cannot export helper functions, constants, or type interfaces directly from the setup block. If you need to expose utilities to other components or test files, you must place them in a standard `<script>` block or an external module, then import them where needed.

### Macro Variable Reference Restrictions

Compiler macros like `defineProps`, `defineEmits`, `defineOptions`, and `defineModel` cannot reference variables declared within the same `<script setup>` block. The compiler enforces this at [`packages/compiler-sfc/src/compileScript.ts`](https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/compileScript.ts) lines 282-288.

For example, this pattern fails:

```vue
<script setup>
const step = 2
defineProps({ step }) // Compile error: cannot reference locally declared variables
</script>

```

You must hoist such constants to module scope in a regular `<script>` block or import them from external files.

## Reactivity and Props Handling Limitations

The compiler's handling of destructured props introduces specific constraints that differ from standard Options API or `setup()` function behavior.

### Destructured Props and Watch/ToRef

When using vue script setup with destructured props, you cannot pass the raw destructured variable directly to `watch` or `toRef`. The compiler checks for this anti-pattern in [`packages/compiler-sfc/src/script/definePropsDestructure.ts`](https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/script/definePropsDestructure.ts) at lines 15-24.

Incorrect usage:

```vue
<script setup>
const props = defineProps(['msg'])
const { msg } = props
watch(msg, () => console.log(msg)) // Error: cannot watch raw destructured prop
</script>

```

Correct approach using a getter:

```vue
<script setup>
const props = defineProps(['msg'])
watch(() => props.msg, (newVal) => console.log(newVal))
</script>

```

## Developer Experience and Tooling Challenges

Beyond compiler constraints, vue script setup introduces friction in debugging, TypeScript handling, and ecosystem compatibility.

### TypeScript Handling Quirks

The compiler moves type-only imports and `declare` statements out of the `<script setup>` scope during transformation, as seen in [`packages/compiler-sfc/src/compileScript.ts`](https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/compileScript.ts) lines 91-100. This hoisting behavior can confuse IDE tooling, causing IntelliSense to behave differently than in standard TypeScript files or suggesting imports that appear unused because they were moved to the module scope.

### Debugging and Source Maps

Because vue script setup completely rewrites the source block into a `setup()` function, source maps must bridge the gap between the original syntax and the generated code. The transformation logic in [`packages/compiler-sfc/src/compileScript.ts`](https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/compileScript.ts) lines 1034-1043 handles this mapping, but the complexity means breakpoints may land in generated code rather than the original `<script setup>` lines, especially in custom build pipelines or when using older versions of Vue Devtools.

### Ecosystem Compatibility

Some older Vue plugins, ESLint configurations, or IDE extensions were designed before `<script setup>` became stable. While the Vue core compiler (`packages/compiler-sfc`) assumes modern APIs, legacy tooling may fail to recognize compiler macros like `defineProps` as valid globals, leading to false-positive linting errors or broken autocomplete in certain editors.

## Summary

- **No external sourcing**: Vue script setup blocks cannot use the `src` attribute, preventing code-splitting of setup logic into separate files.
- **Export restrictions**: You cannot use `export` statements inside `<script setup>`, forcing all shared utilities into regular `<script>` blocks or external modules.
- **Macro scope limitations**: Compiler macros cannot reference locally declared variables, requiring careful hoisting of constants used in `defineProps` or `defineEmits`.
- **Props destructuring edge cases**: `watch` and `toRef` require getter functions when working with destructured props, not raw variables.
- **Tooling friction**: Source map complexity, TypeScript hoisting behavior, and legacy ecosystem compatibility can degrade the debugging experience.

## Frequently Asked Questions

### Can I use both `<script>` and `<script setup>` in the same component?

Yes, you can use both blocks simultaneously. The compiler merges them, with the standard `<script>` block handling module-level exports and the `<script setup>` block providing the component logic. However, be aware that import hoisting and variable scope rules differ between the two blocks, which can lead to subtle ordering issues if you reference variables across boundaries.

### Why can't I export values from `<script setup>`?

The `<script setup>` block is designed to be transformed into a single `setup()` function. Since `setup()` is a function scope, not a module scope, exporting values from within it would violate ES module semantics. The compiler explicitly forbids this in [`packages/compiler-sfc/src/compileScript.ts`](https://github.com/vuejs/core/blob/main/packages/compiler-sfc/src/compileScript.ts) to prevent runtime errors and ensure static analyzability.

### How do I share common setup logic if I can't use the `src` attribute?

Since `<script setup>` cannot reference external files via `src`, you should extract shared logic into **Composables**—functions that use Vue's reactivity APIs—and import them into your `<script setup>` block. This pattern is actually preferred over sharing raw setup code because it provides better tree-shaking, type inference, and testability while working within the compiler's constraints.

### Is `<script setup>` less performant than the traditional Options API?

No, `<script setup>` does not introduce runtime performance penalties. It is purely compile-time sugar that transforms into the same `setup()` function used by the Composition API. The "drawbacks" are architectural and ergonomic—such as stricter coding rules and tooling limitations—rather than execution speed. In fact, the improved tree-shaking enabled by `<script setup>` can sometimes result in smaller bundle sizes compared to the Options API.