I Gave Up Vue/React and Chose to Develop My Own Framework
我放弃了 Vue/React,选择自研框架
1. Framework Selection Background Analysis
1.1 Current State of the Frontend Ecosystem
The current frontend development landscape is characterized by a "framework-is-king" paradigm. Vue and React, as the two major mainstream frameworks, dominate the vast majority of the market share, forming enormous ecosystems around them: state management, routing, UI component libraries, build tools, testing frameworks... with dozens of choices at each layer.
Behind this prosperity lies the cognitive burden on developers who need to continuously learn and maintain. According to npm statistics, over 10,000 new packages are published weekly in the frontend domain, and the update frequency of frameworks and their surrounding ecosystems is even more dazzling.
1.2 Framework Dependency Dilemmas in Commercial Projects
In enterprise-level development, framework selection often implies long-term technological lock-in:
- High upgrade costs: Migrating from Vue 2 to Vue 3, or upgrading from React 17 to React 18+, requires significant refactoring work.
- Ecosystem lock-in: Once a framework is chosen, accompanying solutions for state management, routing, UI libraries, etc., tend to be selected from within that framework's ecosystem.
- Limited customization capabilities: When business requirements exceed the capabilities provided by the framework, it often necessitates extensive hacks or waiting for official support.
1.3 Trigger for the Decision
The direct reason that prompted me to decide on a self-developed framework was a technical challenge in a real project: we needed to implement a complex visualization editor requiring extremely high rendering performance and flexible custom rendering capabilities. After trying Vue, React, and Solid, I found that no single framework could simultaneously meet the following requirements:
- Smooth rendering under high-frequency state updates
- Replaceable rendering backends (DOM/Canvas/WebGL)
- Fine-grained control over the rendering process
2. Elaboration on Pain Points with Mainstream Frameworks
2.1 Limitations of Vue
Vue 3 is known for its elegant reactive system and progressive design, but it still presents some pain points in practical use:
Black-box built-in components: Components like Teleport and Transition, while powerful, have implementation details that are completely opaque to developers. When there's a need to customize animation logic or extend teleport functionality, it can only be achieved through hacks.
2.2 Challenges of React
React is renowned for its flexible JSX syntax and vast ecosystem, but it also has its issues:
Complexity of Hooks rules: Managing dependency arrays for useEffect, useMemo, and useCallback is an eternal pain point, often leading to bugs that are difficult to debug.
// React Hooks dependency management trap
useEffect(() => {
fetchData(id).then(setData)
}, [id]) // Missing dependencies can lead to closure issues
Virtual DOM performance overhead: Although React 18 introduced concurrent rendering and the Fiber architecture, the diffing overhead of the virtual DOM remains significant in high-frequency update scenarios.
2.3 Shortcomings of Signal-Based Frameworks
Signal-driven frameworks like Solid and Svelte perform excellently in terms of performance, but their compile-time optimization strategies introduce new problems:
Opaque compiled output: The compiled code differs significantly from the source code, making debugging difficult. Limited runtime capabilities: As much of the logic is completed at compile time, runtime dynamism and flexibility are restricted.
3. Technical Architecture Design of the Self-Developed Framework
3.1 Core Design Philosophy
Based on the analysis of mainstream frameworks, I identified three core design philosophies for the self-developed framework Vitarx:
| Design Principle | Specific Meaning | Advantages |
|---|---|---|
| Signal-level precise updates | Only update associated DOM nodes when data changes | No diffing overhead, excellent performance in high-frequency update scenarios |
| Runtime view tree | Retains complete runtime structure, supports dynamic operations | Flexible rendering orchestration capabilities |
| Everything is a component | All built-in components are built with public APIs | No black boxes, customizable, extensible |
3.2 Layered Architecture
Vitarx adopts a layered architecture design, with clear responsibilities and unidirectional dependencies for each layer:
┌─────────────────────────────────────────────┐
│ vitarx │ ← Aggregation package, unified export
├────────────┬─────────────┬──────────────────┤
│runtime-dom │ runtime-ssr │ │ ← Platform adaptation layer
├────────────┴─────────────┼──────────────────┤
│ runtime-core │ utils │ ← Runtime core & utilities
├──────────────────────────┼──────────────────┤
│ responsive │ │ ← Reactive system
└──────────────────────────┴──────────────────┘
3.3 Reactive System Design
The reactive system is the core of Vitarx. Unlike Vue's implementation, Vitarx uses doubly linked list dependency management, achieving more efficient dependency tracking and triggering:
/**
* DepLink — Doubly linked list node between a signal and an effect
* Each node exists simultaneously in two linked lists:
* - Signal list: which effects depend on this signal
* - Effect list: which signals this effect depends on
*/
class DepLink {
sigPrev?: DepLink // Predecessor in the Signal list
sigNext?: DepLink // Successor in the Signal list
ePrev?: DepLink // Predecessor in the Effect list
eNext?: DepLink // Successor in the Effect list
constructor(
public signal: Signal,
public effect: EffectRunner
) {}
}
Track (Dependency Collection): When an effect function executes and accesses reactive data, a linked list node is created or reused and attached to both lists.
Trigger (Update Trigger): When data changes, traverse the Signal list to notify all dependents – no diffing, no re-rendering needed.
3.4 View System Design
Vitarx's view system is based on a View abstraction layer, which converts JSX into lightweight view objects, rather than a virtual DOM:
interface View {
init(ctx: ViewContext): void // Initialization
mount(container: HostContainer): void // Mount to container
dispose(): void // Dispose
}
This design allows the renderer to be completely replaceable – simply by implementing the ViewRenderer interface:
interface ViewRenderer {
createElement(tag: string, parent: HostContainer): HostElement
createText(text: string): HostText
createComment(text: string): HostComment
append(child: HostNode, parent: HostContainer): void
remove(node: HostNode): void
setAttribute(el: HostElement, key: string, next: unknown, prev: unknown): void
// ... other methods
}
4. Key Challenges and Solutions During Development
4.1 Challenge 1: Memory Management of the Reactive System
Problem: If dependencies between signals and effects are not cleaned up promptly, it can lead to memory leaks.
Solution: Employ a version number incremental marking mechanism. Each time an effect re-executes, all old dependencies are first marked as expired, then dependencies are re-collected and marks are updated, and finally, expired dependency nodes are cleaned up.
export function trackSignal(signal: Signal): void {
const activeEffect = currentActiveEffect
if (!activeEffect) return
let link = activeEffect[DEP_INDEX_MAP]?.get(signal)
if (!link) {
link = createDepLink(activeEffect, signal)
activeEffect[DEP_INDEX_MAP]?.set(signal, link)
}
link[DEP_VERSION] = activeEffect[DEP_VERSION] // Mark as "still accessed this time"
}
4.2 Challenge 2: Efficient View Tree Updates
Problem: How to ensure the efficiency of update operations while maintaining a complete view tree structure at runtime?
Solution: Introduce ViewSwitchTransaction, encapsulating view switching operations as interruptible and rollbackable transactions. This allows components like Transition to insert animation logic during view transitions.
onViewSwitch((tx) => {
const { prev, next } = tx
tx.stopPropagation()
// First execute the leave animation
runTransition(prev.node, 'leave', props, () => {
tx.commit() // Commit the switch
// Then execute the enter animation
runTransition(next.node, 'enter', props)
})
})
4.3 Challenge 3: Customizing JSX Runtime
Problem: How to enable Vitarx to use its own JSX compilation pipeline without relying on React's jsx-runtime?
Solution: Achieve custom JSX compilation using the @vitarx/plugin-vite plugin in conjunction with Vite. The core workflow of this plugin is:
- Preserve JSX without transpilation: Configure
jsx: 'preserve'for esbuild/oxc, preventing build tools from processing JSX. - Custom transformation pipeline: Implement the JSX →
createViewtransformation logic within thetransformhook. - Compile-time macro support: Built-in handling for directives like
v-if,v-else,v-model,v-show. - Macro component support: Built-in compilation optimization for components like
Switch,IfBlock. - HMR injection: Automatically inject hot module replacement related code in development mode.
Usage simply involves importing the plugin in vite.config.ts:
// vite.config.ts
import vitarx from '@vitarx/plugin-vite'
export default defineConfig({
plugins: [vitarx()]
})
Essential difference from React jsx-runtime: React's jsx-runtime executes createElement calls at runtime; whereas Vitarx compiles JSX into createView calls at build time via a Vite plugin, eliminating any additional JSX transformation overhead at runtime.
4.4 Challenge 4: Server-Side Rendering Implementation
Problem: How to implement server-side rendering and client-side hydration?
Solution: Implement renderToString and renderToStream as two core APIs, while also implementing incremental hydration on the client-side – only updating the changed parts.
import { renderToString, renderToStream } from '@vitarx/runtime-ssr'
// String rendering
const html = await renderToString(App)
// Stream rendering
const stream = await renderToStream(App)
5. Performance Comparison Test Data
5.1 Test Environment
- Test Tool: JS Framework Benchmark
- Test Environment: Chrome 120, macOS 14.0
- Test Project: 1000-row data table, including operations like creation, update, selection, swap, and deletion.
5.2 Test Results
Execution Time Comparison (ms)
| Test Scenario | Vue 3.6 | React 19 | Vitarx 4.0 |
|---|---|---|---|
| Create 1000 rows | 81.9 | 82.4 | 112.2 |
| Update 1000 rows | 88.2 | 84.8 | 122.4 |
| Partial update (every 10 rows) | 19.1 | 11.2 | 9.2 |
| Highlight row | 9.8 | 5.8 | 4.5 |
| Swap two rows | 10.7 | 67.6 | 10.4 |
| Delete a row | 20.9 | 18.1 | 17.2 |
Memory Allocation Comparison (MB)
| Test Scenario | Vue 3.6 | React 19 | Vitarx 4.0 |
|---|---|---|---|
| Initial memory | 0.86 | 1.18 | 0.83 |
| Runtime memory | 3.77 | 4.42 | 5.46 |
| After create/clear | 1.23 | 1.97 | 1.16 |
Transfer Size Comparison
| Item | Vue 3.6 | React 19 | Vitarx 4.0 |
|---|---|---|---|
| Uncompressed | 63.7 KB | 180.3 KB | 57.8 KB |
| Compressed | 22.9 KB | 51.4 KB | 17.0 KB |
6. Future Framework Iteration Plan
6.1 Short-term Goals (2026 - 2027)
| Goal | Specific Content | Priority |
|---|---|---|
| Ecosystem completion | Launch official UI component library & design system | High |
| VitaStack | Full-stack development framework | High |
| Toolchain | Develop browser debugging plugin | High |
| Documentation upgrade | Improve official documentation and tutorials | Medium |
| State management | Develop official state management library | Medium |
6.2 Mid-term Goals
| Goal | Specific Content | Priority |
|---|---|---|
| Cross-platform support | Launch mobile and desktop support | Medium |
| Performance optimization | Further optimize initial load and rendering performance | High |
6.3 Long-term Goals
| Goal | Specific Content | Priority |
|---|---|---|
| AI Integration | Explore AI-assisted development capabilities | Low |
| WebAssembly | WASM optimization for critical paths | Low |
| Community building | Establish an active developer community | Medium |
7. Summary and Reflection
7.1 Value of a Self-Developed Framework
A self-developed framework has brought me more than just a usable tool; more importantly, it has provided:
- Deep understanding: An unprecedented understanding of the underlying principles of frontend frameworks.
- Technical control: No longer bound by framework API changes, able to independently control the technical roadmap.
- Innovation capability: Accumulated experience in building complex systems from scratch.
7.2 Scenarios Suitable for Self-Developed Frameworks
A self-developed framework is not suitable for all projects. The following scenarios are more appropriate for considering self-development:
- Projects with extreme performance requirements.
- Projects that require deep customization of framework behavior.
- Teams that seek architectural transparency.
- Teams with sufficient technical capability and time investment.
7.3 Advice for Other Developers
If you are also considering developing your own framework, my advice is:
- Clarify your goals: Be clear about what problems you want to solve; don't build a wheel just for the sake of building one.
- Start simple: Implement a minimum viable version first, then gradually improve it.
- Stay open-minded: Refer to the designs of excellent frameworks, but have your own critical thinking.
- Iterate continuously: A framework is a living entity that requires constant optimization and improvement.
Final thoughts: Vitarx is the culmination of two years of my technical exploration. It might not be the most perfect framework, but it is a "visible and tangible" one. If you are also interested in the underlying principles of frontend frameworks, feel free to join our discussion group to explore together.
- 🌟 GitHub — A Star is the biggest support
- 📖 Official Documentation — Complete usage guide and API documentation
- 💬 QQ Discussion Group — Let's discuss together