跪拜 Guibai
← All articles
Frontend

How to Secretly Swap a jQuery Codebase for Vue 3 Without Breaking Production

By 奶油mm · · 517 views · 4 likes · 9 comments
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Many organizations sit on jQuery frontends that are too risky to rewrite from scratch. A DOM-preserving, test-compatible migration path to Vue 3 lets teams modernize incrementally without a greenfield project or a production outage.

Summary

A legacy system built on jQuery 1.7.2, a 3.2MB index.html, and 14,000-character single-line JavaScript files was silently migrated to Vue 3, TypeScript, and Vite. The refactor preserved every original DOM id and class so that untouched jQuery plugins continued to function, while Vue took over rendering. An internal npm package, `@company/legacy-bridge`, now lets other teams drop Vue components into old pages with three lines of code.

Key techniques included hijacking `$.fn.ready` to defer execution until after Vue's async mount, isolating destructive jQuery plugins inside Shadow DOM containers, and using memoized composables to drop a 5,000-item list render from 8 seconds of lag to 120ms. CSS Modules and scoped styles prevented the original 3,000-line global stylesheet from polluting new components.

The result passed all existing Selenium tests because the DOM structure, API response shapes, and even known bugs were intentionally preserved. The CTO attributed the speed improvement to a bandwidth upgrade that never happened.

Takeaways
Preserving original DOM ids and classes lets untouched jQuery plugins coexist with Vue-rendered markup.
Hijacking `$.fn.ready` and deferring callbacks until after Vue's `nextTick` solves async mount timing conflicts.
Isolating destructive jQuery plugins inside Shadow DOM prevents them from corrupting Vue's virtual DOM.
Memoizing date-formatting functions with a `Map` cache dropped a 5,000-row list render from 8 seconds to 120ms.
Splitting a 3.2MB monolithic HTML file into Vite chunks with a `legacy-jail` bundle cut first-screen resources to 340KB.
CSS Modules with a dedicated `:global(.legacy-wrapper)` scope prevent old global styles from overriding new component styles.
An internal npm package (`@company/legacy-bridge`) let other developers adopt the migration with three import lines.
Keeping URLs, API response formats, and even known bugs identical meant existing Selenium tests passed without modification.
Conclusions

Stealth refactors succeed when they target the build pipeline and rendering layer without touching the contract the rest of the system depends on.

The hardest part of modernizing a jQuery codebase isn't replacing jQuery—it's managing the load-order assumptions baked into dozens of unowned plugins.

Shadow DOM is an underused escape hatch for incrementally containing legacy DOM manipulation inside a modern framework.

An internal compatibility package turns a solo skunkworks project into team-wide adoption without requiring anyone else to understand the migration mechanics.

The CTO's request for a company-wide tech talk suggests management often tolerates unsanctioned improvement as long as it ships quietly and doesn't break anything.

Concepts & terms
Shadow DOM isolation
Attaching a Shadow DOM root to a host element creates a separate DOM tree that a framework like Vue does not manage, allowing legacy jQuery plugins to mutate DOM without interfering with Vue's virtual DOM reconciliation.
Memoized composable
A Vue composable that wraps a function with a cache (e.g., a `Map`), returning the cached result for repeated inputs instead of recomputing. Useful for expensive per-item operations in long lists.
Manual chunks (Vite/Rollup)
The `manualChunks` option in Rollup's output configuration lets developers explicitly split vendor dependencies (like jQuery and its plugins) into separate bundle files, preventing them from blocking the initial application code download.
jQuery ready hijacking
Overwriting `$.fn.ready` to intercept and queue callbacks, then releasing them after Vue has mounted, solves the race condition where jQuery's `document.ready` fires before Vue renders the target DOM elements.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗