An honest comparison of two approaches to adding interactivity to existing HTML - helping you choose the right tool for your project
Progressive enhancement has made a comeback. With the web shifting toward faster, more accessible experiences, developers are rediscovering the value of starting with working HTML and layering on JavaScript thoughtfully. But when it comes to implementation, you have choices—and they're more different than you might think.
Today we're comparing Vue's progressive enhancement solutions (Petite Vue and Vue 3's compatibility mode) against Juris's enhance() API. Both promise to add reactivity to existing HTML, but they take fundamentally different approaches. Let's dig into what that means for your project.
The Philosophical Divide
Before we get into code, it's important to understand that these tools come from different philosophies:
Vue's Approach: "Make Vue work progressively"
- Start with Vue's component model
- Adapt it to work with existing HTML
- Maintain Vue's developer experience and ecosystem
Juris's Approach: "Make HTML programmable"
- Start with working HTML
- Enhance it with pure JavaScript functions
- Maintain web standards and simplicity
This philosophical difference affects everything from debugging to team onboarding to long-term maintenance.
Round 1: Getting Started
Vue's Petite Vue
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/petite-vue@0.4.1/dist/petite-vue.iife.js" defer></script>
</head>
<body>
<div v-scope="{ count: 0 }">
<button @click="count++">Count: {{ count }}</button>
</div>
</body>
</html>
Juris Enhancement
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/juris@latest/juris.js"></script>
</head>
<body>
<!-- This works without JavaScript -->
<div class="counter">
<button>Count: <span class="count">0</span></button>
</div>
<script>
const counter = new Juris({ states: { count: 0 } });
counter.enhance('.counter button', {
onclick: () => {
const current = counter.getState('count');
counter.setState('count', current + 1);
}
});
counter.enhance('.count', {
textContent: () => counter.getState('count')
});
</script>
</body>
</html>
What This Reveals:
- Vue requires special template syntax (
v-scope
,@click
,{{ }}
) - Juris enhances standard HTML with JavaScript functions
- Vue's approach is more concise for simple cases
- Juris's HTML works without JavaScript loaded
Round 2: Real-World Form Handling
Let's see how each handles a common scenario: a contact form with validation.
Vue Implementation
<form v-scope="{
email: '',
message: '',
errors: {},
validate() {
this.errors = {};
if (!this.email.includes('@')) {
this.errors.email = 'Please enter a valid email';
}
if (this.message.length < 10) {
this.errors.message = 'Message must be at least 10 characters';
}
return Object.keys(this.errors).length === 0;
},
submit() {
if (this.validate()) {
console.log('Submitting:', { email: this.email, message: this.message });
}
}
}" @submit.prevent="submit()">
<div>
<label>Email:</label>
<input v-model="email" type="email" :class="{ 'error': errors.email }">
<span v-if="errors.email" class="error-text">{{ errors.email }}</span>
</div>
<div>
<label>Message:</label>
<textarea v-model="message" :class="{ 'error': errors.message }"></textarea>
<span v-if="errors.message" class="error-text">{{ errors.message }}</span>
</div>
<button type="submit" :disabled="!email || !message">Send Message</button>
</form>
Juris Implementation
<!-- This form works without JavaScript -->
<form class="contact-form" action="/contact" method="POST">
<div>
<label>Email:</label>
<input name="email" type="email" class="email-input" required>
<span class="email-error" style="display: none;"></span>
</div>
<div>
<label>Message:</label>
<textarea name="message" class="message-input" required></textarea>
<span class="message-error" style="display: none;"></span>
</div>
<button type="submit" class="submit-btn">Send Message</button>
</form>
<script>
const contactForm = new Juris({
states: {
email: '',
message: '',
errors: {}
},
services: {
validator: {
validateEmail: (email) => {
const isValid = email.includes('@');
const error = isValid ? null : 'Please enter a valid email';
contactForm.setState('errors.email', error);
return isValid;
},
validateMessage: (message) => {
const isValid = message.length >= 10;
const error = isValid ? null : 'Message must be at least 10 characters';
contactForm.setState('errors.message', error);
return isValid;
},
isFormValid: () => {
const errors = contactForm.getState('errors');
return !errors.email && !errors.message;
}
},
formHandler: {
submit: (e) => {
e.preventDefault();
const { validator } = contactForm.services;
const email = contactForm.getState('email');
const message = contactForm.getState('message');
if (validator.validateEmail(email) && validator.validateMessage(message)) {
console.log('Submitting:', { email, message });
// Could also allow form to submit naturally for non-JS fallback
}
}
}
}
});
// Email input enhancement
contactForm.enhance('.email-input', ({ validator }) => ({
value: () => contactForm.getState('email'),
oninput: (e) => contactForm.setState('email', e.target.value),
onblur: (e) => validator.validateEmail(contactForm.getState('email')),
className: () => contactForm.getState('errors.email') ? 'error' : ''
}));
// Email error display
contactForm.enhance('.email-error', () => ({
textContent: () => contactForm.getState('errors.email') || '',
style: () => ({
display: contactForm.getState('errors.email') ? 'inline' : 'none',
color: 'red'
})
}));
// Message input enhancement
contactForm.enhance('.message-input', ({ validator }) => ({
value: () => contactForm.getState('message'),
oninput: (e) => contactForm.setState('message', e.target.value),
onblur: (e) => validator.validateMessage(contactForm.getState('message')),
className: () => contactForm.getState('errors.message') ? 'error' : ''
}));
// Message error display
contactForm.enhance('.message-error', () => ({
textContent: () => contactForm.getState('errors.message') || '',
style: () => ({
display: contactForm.getState('errors.message') ? 'inline' : 'none',
color: 'red'
})
}));
// Submit button enhancement
contactForm.enhance('.submit-btn', ({ validator }) => ({
disabled: () => !validator.isFormValid(),
onclick: (e, { formHandler }) => formHandler.submit(e)
}));
// Form enhancement
contactForm.enhance('.contact-form', ({ formHandler }) => ({
onsubmit: (e) => formHandler.submit(e)
}));
</script>
What This Reveals:
- Vue's template approach is more concise
- Juris provides better separation of concerns with services
- Vue mixes logic with markup
- Juris's form works with server-side submission as fallback
- Debugging: Vue requires Vue DevTools, Juris uses standard JavaScript debugging
// Vue debugging (requires Vue DevTools)
console.log('Vue state:', document.querySelector('#app').__vue__.$data);
// Juris debugging (standard JavaScript)
console.log('Form state:', contactForm.stateManager.state);
console.log('Email value:', contactForm.getState('email'));
console.log('Current errors:', contactForm.getState('errors'));
Round 3: Component Composition
Let's compare building a reusable todo list component and see how each handles composition.
Vue's Template-Based Composition
<div v-scope="TodoApp()" id="todo-app">
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add todo">
<button @click="addTodo">Add</button>
<div v-for="todo in filteredTodos" :key="todo.id">
<input type="checkbox" v-model="todo.completed">
<span :class="{ 'completed': todo.completed }">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">×</button>
</div>
<div class="filters">
<button @click="filter = 'all'" :class="{ active: filter === 'all' }">All</button>
<button @click="filter = 'active'" :class="{ active: filter === 'active' }">Active</button>
<button @click="filter = 'completed'" :class="{ active: filter === 'completed' }">Completed</button>
</div>
<p>{{ activeCount }} items left</p>
</div>
<script>
function TodoApp() {
return {
todos: [],
newTodo: '',
filter: 'all',
get filteredTodos() {
if (this.filter === 'active') return this.todos.filter(t => !t.completed);
if (this.filter === 'completed') return this.todos.filter(t => t.completed);
return this.todos;
},
get activeCount() {
return this.todos.filter(t => !t.completed).length;
},
addTodo() {
if (this.newTodo.trim()) {
this.todos.push({
id: Date.now(),
text: this.newTodo,
completed: false
});
this.newTodo = '';
}
},
removeTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
}
};
}
</script>
Juris's Component Composition via enhance()
<!-- Works without JavaScript -->
<div class="todo-app">
<div class="todo-input">
<input class="new-todo" placeholder="Add todo">
<button class="add-btn">Add</button>
</div>
<div class="todo-list">
<!-- Server-rendered todos could go here -->
</div>
<div class="todo-filters">
<button class="filter-btn" data-filter="all">All</button>
<button class="filter-btn" data-filter="active">Active</button>
<button class="filter-btn" data-filter="completed">Completed</button>
</div>
<p class="todo-count">0 items left</p>
</div>
<script>
// Component composition through enhance() API
const createTodoApp = (selector) => {
const todoApp = new Juris({
states: {
todos: [],
newTodo: '',
filter: 'all'
},
services: {
todoManager: {
add: () => {
const newTodo = todoApp.getState('newTodo');
if (newTodo.trim()) {
const todos = todoApp.getState('todos');
todoApp.setState('todos', [
...todos,
{ id: Date.now(), text: newTodo, completed: false }
]);
todoApp.setState('newTodo', '');
}
},
remove: (id) => {
const todos = todoApp.getState('todos');
todoApp.setState('todos', todos.filter(t => t.id !== id));
},
toggle: (id) => {
const todos = todoApp.getState('todos');
todoApp.setState('todos', todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
},
getFiltered: () => {
const todos = todoApp.getState('todos');
const filter = todoApp.getState('filter');
if (filter === 'active') return todos.filter(t => !t.completed);
if (filter === 'completed') return todos.filter(t => t.completed);
return todos;
},
getActiveCount: () => {
const todos = todoApp.getState('todos');
return todos.filter(t => !t.completed).length;
}
}
}
});
// Input component enhancement
todoApp.enhance(`${selector} .new-todo`, ({ todoManager }) => ({
value: () => todoApp.getState('newTodo'),
oninput: (e) => todoApp.setState('newTodo', e.target.value),
onkeyup: (e) => {
if (e.key === 'Enter') todoManager.add();
}
}));
// Add button component
todoApp.enhance(`${selector} .add-btn`, ({ todoManager }) => ({
onclick: () => todoManager.add()
}));
// Composable todo item component
const createTodoItem = (todo, { todoManager }) => ({
div: {
key: todo.id,
className: 'todo-item',
children: [
{
input: {
type: 'checkbox',
checked: todo.completed,
onchange: () => todoManager.toggle(todo.id)
}
},
{
span: {
textContent: todo.text,
className: todo.completed ? 'completed' : ''
}
},
{
button: {
textContent: '×',
onclick: () => todoManager.remove(todo.id)
}
}
]
}
});
// List component that composes todo items
todoApp.enhance(`${selector} .todo-list`, ({ todoManager }) => ({
children: () => {
return todoManager.getFiltered().map(todo =>
createTodoItem(todo, { todoManager })
);
}
}));
// Filter component composition
todoApp.enhance(`${selector} .filter-btn`, () => ({
onclick: (e) => {
const filter = e.target.dataset.filter;
todoApp.setState('filter', filter);
},
className: (props, context, element) => {
const filter = todoApp.getState('filter');
const buttonFilter = element.dataset.filter;
return buttonFilter === filter ? 'filter-btn active' : 'filter-btn';
}
}));
// Count component
todoApp.enhance(`${selector} .todo-count`, ({ todoManager }) => ({
textContent: () => `${todoManager.getActiveCount()} items left`
}));
return todoApp;
};
// Reusable component composition - create multiple instances
const mainTodoApp = createTodoApp('.todo-app');
const sidebarTodoApp = createTodoApp('.sidebar-todos');
// Component composition with shared services
const createSharedTodoSystem = () => {
// Shared state and services
const sharedServices = {
syncService: {
save: (todos) => localStorage.setItem('todos', JSON.stringify(todos)),
load: () => JSON.parse(localStorage.getItem('todos') || '[]')
}
};
// Compose multiple todo components sharing services
const mainApp = createTodoApp('.main-todos');
const mobileApp = createTodoApp('.mobile-todos');
// Add shared sync behavior to both
[mainApp, mobileApp].forEach(app => {
app.services = { ...app.services, ...sharedServices };
// Auto-save on todo changes
app.subscribe('todos', (todos) => {
sharedServices.syncService.save(todos);
});
});
return { mainApp, mobileApp };
};
// Advanced composition - enhance with behaviors
const addDragAndDrop = (todoApp, selector) => {
todoApp.enhance(`${selector} .todo-item`, ({ todoManager }) => ({
draggable: true,
ondragstart: (e) => {
const todoId = e.target.dataset.todoId;
e.dataTransfer.setData('todo-id', todoId);
},
ondragover: (e) => e.preventDefault(),
ondrop: (e) => {
const draggedId = e.dataTransfer.getData('todo-id');
const targetId = e.target.dataset.todoId;
todoManager.reorder(draggedId, targetId);
}
}));
};
// Compose drag and drop behavior
addDragAndDrop(mainTodoApp, '.todo-app');
</script>
Component Composition Advantages in Juris
1. True Composability:
// Mix and match behaviors
const addAnimation = (app, selector) => {
app.enhance(`${selector} .todo-item`, () => ({
style: () => ({ transition: 'all 0.3s ease' })
}));
};
const addKeyboardShortcuts = (app, selector) => {
app.enhance(`${selector}`, () => ({
onkeydown: (e) => {
if (e.ctrlKey && e.key === 'n') {
app.setState('newTodo', '');
document.querySelector(`${selector} .new-todo`).focus();
}
}
}));
};
// Compose multiple behaviors
addAnimation(todoApp, '.todo-app');
addKeyboardShortcuts(todoApp, '.todo-app');
addDragAndDrop(todoApp, '.todo-app');
2. Component Factory Pattern:
// Create reusable component factories
const TodoItemFactory = {
create: (todo, services) => ({
div: {
key: todo.id,
className: `todo-item ${todo.completed ? 'completed' : ''}`,
children: [
TodoItemFactory.createCheckbox(todo, services),
TodoItemFactory.createText(todo, services),
TodoItemFactory.createDeleteButton(todo, services)
]
}
}),
createCheckbox: (todo, { todoManager }) => ({
input: {
type: 'checkbox',
checked: todo.completed,
onchange: () => todoManager.toggle(todo.id)
}
}),
createText: (todo, services) => ({
span: { textContent: todo.text }
}),
createDeleteButton: (todo, { todoManager }) => ({
button: {
textContent: '×',
onclick: () => todoManager.remove(todo.id)
}
})
};
// Use the factory
todoApp.enhance('.todo-list', ({ todoManager }) => ({
children: () => todoManager.getFiltered().map(todo =>
TodoItemFactory.create(todo, { todoManager })
)
}));
What This Reveals:
- Vue provides more compact syntax for templates
- Juris offers superior composition through JavaScript functions
- Vue mixes state and view logic
- Juris separates concerns with composable behaviors
- Juris enables multiple independent instances easily
- Better reusability through pure JavaScript composition
- Component factories and behavior mixing possible with Juris
<div v-scope="TodoApp()" id="todo-app">
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add todo">
<button @click="addTodo">Add</button>
<div v-for="todo in filteredTodos" :key="todo.id">
<input type="checkbox" v-model="todo.completed">
<span :class="{ 'completed': todo.completed }">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">×</button>
</div>
<div class="filters">
<button @click="filter = 'all'" :class="{ active: filter === 'all' }">All</button>
<button @click="filter = 'active'" :class="{ active: filter === 'active' }">Active</button>
<button @click="filter = 'completed'" :class="{ active: filter === 'completed' }">Completed</button>
</div>
<p>{{ activeCount }} items left</p>
</div>
<script>
function TodoApp() {
return {
todos: [],
newTodo: '',
filter: 'all',
get filteredTodos() {
if (this.filter === 'active') return this.todos.filter(t => !t.completed);
if (this.filter === 'completed') return this.todos.filter(t => t.completed);
return this.todos;
},
get activeCount() {
return this.todos.filter(t => !t.completed).length;
},
addTodo() {
if (this.newTodo.trim()) {
this.todos.push({
id: Date.now(),
text: this.newTodo,
completed: false
});
this.newTodo = '';
}
},
removeTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
}
};
}
</script>
Juris's Approach
<!-- Works without JavaScript -->
<div class="todo-app">
<div class="todo-input">
<input class="new-todo" placeholder="Add todo">
<button class="add-btn">Add</button>
</div>
<div class="todo-list">
<!-- Server-rendered todos could go here -->
</div>
<div class="todo-filters">
<button class="filter-btn" data-filter="all">All</button>
<button class="filter-btn" data-filter="active">Active</button>
<button class="filter-btn" data-filter="completed">Completed</button>
</div>
<p class="todo-count">0 items left</p>
</div>
<script>
const createTodoApp = (selector) => {
const todoApp = new Juris({
states: {
todos: [],
newTodo: '',
filter: 'all'
},
services: {
todoManager: {
add: () => {
const newTodo = todoApp.getState('newTodo');
if (newTodo.trim()) {
const todos = todoApp.getState('todos');
todoApp.setState('todos', [
...todos,
{ id: Date.now(), text: newTodo, completed: false }
]);
todoApp.setState('newTodo', '');
}
},
remove: (id) => {
const todos = todoApp.getState('todos');
todoApp.setState('todos', todos.filter(t => t.id !== id));
},
toggle: (id) => {
const todos = todoApp.getState('todos');
todoApp.setState('todos', todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
},
getFiltered: () => {
const todos = todoApp.getState('todos');
const filter = todoApp.getState('filter');
if (filter === 'active') return todos.filter(t => !t.completed);
if (filter === 'completed') return todos.filter(t => t.completed);
return todos;
},
getActiveCount: () => {
const todos = todoApp.getState('todos');
return todos.filter(t => !t.completed).length;
}
}
}
});
// Input enhancement
todoApp.enhance(`${selector} .new-todo`, {
value: () => todoApp.getState('newTodo'),
oninput: (e) => todoApp.setState('newTodo', e.target.value),
onkeyup: (e, { todoManager }) => {
if (e.key === 'Enter') todoManager.add();
}
});
// Add button
todoApp.enhance(`${selector} .add-btn`, {
onclick: ({ todoManager }) => todoManager.add()
});
// Dynamic todo list
todoApp.enhance(`${selector} .todo-list`, ({ todoManager }) => ({
children: () => {
return todoManager.getFiltered().map(todo => ({
div: {
key: todo.id,
className: 'todo-item',
children: [
{
input: {
type: 'checkbox',
checked: todo.completed,
onchange: () => todoManager.toggle(todo.id)
}
},
{
span: {
textContent: todo.text,
className: todo.completed ? 'completed' : ''
}
},
{
button: {
textContent: '×',
onclick: () => todoManager.remove(todo.id)
}
}
]
}
}));
}
}));
// Filter buttons
todoApp.enhance(`${selector} .filter-btn`, {
onclick: (e) => {
const filter = e.target.dataset.filter;
todoApp.setState('filter', filter);
},
className: (props, { }, element) => {
const filter = todoApp.getState('filter');
const buttonFilter = element.dataset.filter;
return buttonFilter === filter ? 'active' : '';
}
});
// Count display
todoApp.enhance(`${selector} .todo-count`, ({ todoManager }) => ({
textContent: () => `${todoManager.getActiveCount()} items left`
}));
return todoApp;
};
// Create todo app instance
const myTodoApp = createTodoApp('.todo-app');
// Debugging utilities
window.todoDebug = {
getState: () => myTodoApp.stateManager.state,
getSpecificState: (path) => myTodoApp.getState(path),
addTestTodos: () => {
myTodoApp.setState('todos', [
{ id: 1, text: 'Learn Juris', completed: false },
{ id: 2, text: 'Build awesome apps', completed: true },
{ id: 3, text: 'Share with team', completed: false }
]);
}
};
</script>
What This Reveals:
- Vue provides more compact syntax for templates
- Juris offers better testability through service injection
- Vue mixes state and view logic
- Juris separates concerns more clearly
- Juris enables multiple independent instances
- Better debugging capabilities with Juris
Round 4: Performance & Bundle Size
Bundle Size Comparison
Vue 3 Full Build: ~102KB (34KB gzipped)
Petite Vue: ~9KB (6KB gzipped)
Juris: ~45KB (25KB gzipped)
Runtime Performance
The performance difference is substantial. Based on real-world benchmarks, Juris performs 4x better than Vue across key metrics:
Initial Render Time:
- Vue (Petite): ~12-16ms average
- Juris: ~3-4ms average
- 4x faster initial rendering
Memory Usage:
- Vue: Higher memory footprint due to reactive proxy system and virtual DOM
- Juris: 4x lower memory consumption through direct DOM manipulation
- Significant memory savings for complex applications
CPU Usage:
- Vue: Higher CPU usage from virtual DOM diffing and proxy reactivity
- Juris: 4x lower CPU usage through surgical updates
- Better battery life on mobile devices
Why Juris Is Faster
No Virtual DOM Overhead:
// Vue: Creates virtual DOM, diffs, then updates real DOM
Template → Virtual DOM → Diff → Real DOM Update
// Juris: Direct DOM updates only when dependencies change
State Change → Direct DOM Update (surgical)
Surgical Reactivity:
// Vue: When any state changes, entire component re-evaluates
const component = {
template: `<div>{{user.name}} {{posts.length}} {{ui.loading}}</div>`
// All expressions re-evaluate on any state change
};
// Juris: Only affected elements update
enhance('.user-name', () => ({
text: () => getState('user.name') // Only updates when user.name changes
}));
enhance('.post-count', () => ({
text: () => getState('posts').length // Only updates when posts change
}));
No Proxy Overhead:
Vue's reactivity system uses Proxy objects which add overhead to every property access. Juris uses explicit state management with direct object property access.
Performance Characteristics
Vue Excels At:
- Complex computed properties with caching
- Large lists when using virtual scrolling
- Applications with heavy template logic
Juris Excels At:
- Initial page load performance (4x faster)
- Memory-constrained environments (4x lower usage)
- Battery-sensitive applications (4x lower CPU)
- Multiple independent widgets
- Progressive enhancement scenarios
Round 5: Developer Experience
Learning Curve
Vue: Moderate
- Familiar template syntax for HTML developers
- Reactivity concepts to learn
- Component lifecycle to understand
- Build tools for complex projects
Juris: Steep initially, then smooth
- Requires solid JavaScript knowledge
- Object-first thinking needed
- But pure JavaScript debugging
- No build tools ever needed
Debugging Experience
Vue Debugging:
// Requires Vue DevTools browser extension
// Or accessing Vue internals
document.querySelector('#app').__vue__.$data
Juris Debugging:
// Standard JavaScript debugging
console.log('Full state:', myApp.stateManager.state);
console.log('Todo count:', myApp.getState('todos').length);
console.log('Current user:', myApp.getState('user.name'));
// Set breakpoints in your actual code
myApp.services.todoManager.add(); // <- Step through this normally
TypeScript Support
Vue:
- Excellent TypeScript support
- Requires build tooling
- Template type checking available
- Strong ecosystem integration
Juris:
- Good TypeScript support (it's just JavaScript functions)
- No build tools required
- Direct type annotations possible
- Simpler integration path
Round 6: Ecosystem & Long-term Considerations
Ecosystem Dependency vs JavaScript Independence
Vue: Ecosystem-Dependent
- Requires Vue-specific components and libraries
- Template syntax limits reusability outside Vue
- Component logic tied to Vue's lifecycle
- Breaking changes affect entire ecosystem
Juris: Pure JavaScript Independence
- It's just JavaScript - no special syntax or patterns
- Standard JavaScript functions work everywhere
- Can integrate with any library or framework
- Future-proof against framework churn
// Vue component - locked into Vue ecosystem
const VueComponent = {
template: `<div @click="handleClick">{{ message }}</div>`,
data() { return { message: 'Hello' }; },
methods: { handleClick() { /* Vue-specific */ } }
};
// Juris enhancement - pure JavaScript that works anywhere
const handleClick = () => console.log('Clicked!');
const getMessage = () => 'Hello';
// This is just JavaScript - reusable everywhere
myApp.enhance('.button', () => ({
textContent: getMessage(),
onclick: handleClick
}));
AI-Ready Architecture
Juris has a unique advantage: it's AI-friendly by design.
Why AI Loves Juris:
- Pure JavaScript objects - AI can generate and understand them naturally
- No special syntax - AI doesn't need to learn Vue templates
- Explicit structure - Clear, predictable patterns AI can follow
- Standard debugging - AI can help debug using normal JavaScript tools
// AI can easily generate this - it's just JavaScript
const todoComponent = new Juris({
states: { todos: [], newTodo: '' },
services: {
todoManager: {
add: () => { /* standard JavaScript */ },
remove: (id) => { /* standard JavaScript */ }
}
}
});
// vs Vue templates that AI has to learn special syntax for
// <div v-for="todo in todos" :key="todo.id" @click="removeTodo(todo.id)">
AI Integration Examples:
// AI can generate Juris enhancements on demand
const generateTodoApp = (requirements) => {
return new Juris({
states: aiGenerateStates(requirements),
services: aiGenerateServices(requirements)
});
};
// AI can debug Juris apps using standard JavaScript
const debugWithAI = (island) => {
const state = island.stateManager.state; // Just JavaScript object
const todos = island.getState('todos'); // Specific state path
return aiAnalyzeState({ fullState: state, todos }); // AI understands plain objects
};
Framework Lock-in vs Freedom
Vue: Strong Framework Lock-in
- Templates must be rewritten to migrate
- Component logic tied to Vue patterns
- Ecosystem investments lost during migration
- Team knowledge specific to Vue
Juris: Maximum Freedom
- It's just JavaScript - skills transfer everywhere
- Enhancement pattern works with any HTML
- Can coexist with React, Vue, Angular, or vanilla JS
- Zero vendor lock-in
// This Juris code works alongside ANY framework
const sharedLogic = {
validateEmail: (email) => email.includes('@'),
formatDate: (date) => new Intl.DateTimeFormat().format(date)
};
// Use with React
function ReactComponent() {
return <div className="react-enhanced">React Component</div>;
}
// Use with Vue
const vueApp = createApp({
template: '<div class="vue-enhanced">Vue Component</div>'
});
// Use with Juris - same JavaScript logic
jurisApp.enhance('.react-enhanced', () => ({ /* shared logic */ }));
jurisApp.enhance('.vue-enhanced', () => ({ /* same logic */ }));
Maintenance Burden
Vue: Framework Maintenance
- Vue version updates require code changes
- Template syntax evolution
- Build tool compatibility
- Ecosystem dependency updates
- Framework-specific knowledge required
Juris: JavaScript Maintenance
- Pure JavaScript ages well - ES5 still works today
- No framework updates to track
- No template compilation to maintain
- Standard JavaScript knowledge sufficient
- Future-proof against framework trends
Ecosystem Size vs Ecosystem Independence
Vue: Large Ecosystem, High Dependency
- Thousands of Vue components available
- Strong community and resources
- But locked into Vue's future
- Component quality varies
- Breaking changes cascade through ecosystem
Juris: Small Ecosystem, Zero Dependency
- Works with ANY JavaScript library
- Can use React components, Vue components, or vanilla JS
- npm has millions of JavaScript packages - all compatible
- No framework-specific rewrites needed
- Future-proof investment
The Verdict: Which Should You Choose?
Choose Vue's Progressive Enhancement If:
✅ Your team is already familiar with Vue
- Leverages existing knowledge
- Consistent with current stack
- Easier team onboarding
✅ You need rapid prototyping
- Template syntax is faster for simple cases
- Less code for basic interactions
- Familiar patterns
✅ You plan to evolve into a full Vue SPA
- Natural upgrade path
- Can leverage Vue ecosystem
- Consistent architecture
✅ You need extensive third-party components
- Mature ecosystem
- Pre-built solutions
- Community support
Choose Juris Enhancement If:
✅ You prioritize true progressive enhancement
- HTML works without JavaScript
- Better accessibility by default
- Server-side rendering friendly
✅ You need multiple independent widgets
- True isolation between components
- No global state interference
- Better for micro-frontends
✅ You want transparent debugging
- Standard JavaScript debugging tools
- No framework magic to learn
- Clear mental model
✅ You have strong JavaScript skills on your team
- Leverages pure JavaScript knowledge
- More flexible and powerful
- Better separation of concerns
✅ You want to avoid build tools
- Zero compilation required
- Deploy anywhere HTML/JS works
- Simpler CI/CD pipeline
✅ You value ecosystem independence
- It's just JavaScript - works with any library
- No vendor lock-in or framework dependencies
- Future-proof against framework churn
- Can integrate with React, Vue, Angular, or vanilla JS
✅ You're building for AI-assisted development
- AI can generate and understand pure JavaScript objects
- No special syntax for AI to learn
- Standard debugging AI can help with
- Clear, predictable patterns AI can follow
✅ You want maximum performance
- 4x faster initial render times
- 4x lower memory usage
- 4x more efficient CPU usage
- Better mobile and battery performance
The Honest Assessment
Vue's progressive enhancement is excellent for teams already using Vue who want to extend their approach to enhancement scenarios. It maintains consistency and leverages existing knowledge.
Juris is better for pure progressive enhancement where you truly want HTML-first development with JavaScript enhancement. It's more powerful but requires stronger JavaScript skills.
Neither is universally better - they solve different problems for different teams with different priorities.
Making Your Decision
Ask yourself these questions:
- Does your HTML need to work without JavaScript? (Accessibility, SEO, resilience)
- How important is debugging transparency to your team?
- Are you building independent widgets or a cohesive application?
- What's your team's JavaScript skill level?
- Do you plan to migrate to a full SPA eventually?
- How important is ecosystem size vs. technical flexibility?
Both tools are excellent at what they do. Vue excels at making Vue's developer experience work progressively. Juris excels at making HTML programmable with JavaScript.
The choice depends less on technical superiority and more on which approach aligns with your project goals, team skills, and long-term architecture vision.
Want to try both? Set up a simple enhancement scenario with each framework and see which feels more natural for your team and project. The best framework is the one that makes your team most productive while meeting your users' needs.