Why You Might Want to Avoid Vue Script Setup in Vue 3 Projects
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—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 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 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 lines 282-288.
For example, this pattern fails:
<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 at lines 15-24.
Incorrect usage:
<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:
<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 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 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
srcattribute, preventing code-splitting of setup logic into separate files. - Export restrictions: You cannot use
exportstatements 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
definePropsordefineEmits. - Props destructuring edge cases:
watchandtoRefrequire 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →