Why I Ditched Vue.js and Went Back to Rails Views
A contrarian take on modern web development: sometimes simple beats sophisticated
The Setup
In 2017, PropertyWebBuilder was cutting-edge:
- EmberJS admin panel
- Vue.js frontend components
- Rails API backend
- Deployed to Heroku with Webpack
The stack felt modern, professional, and aligned with industry trends. SPAs were the future. Server-rendered HTML was "legacy."
Seven years later, in 2024, I ripped it all out and went back to Rails views with Turbo.
This is why.
What We Had (The SPA Approach)
The Good Parts
Vue.js gave us:
- Reactive property search (instant filtering without page reloads)
- Smooth image galleries with transitions
- Interactive map integration
- Component reusability
Example Vue component:
<template>
<div class="property-card">
<img :src="property.featured_image" :alt="property.title">
<h3>{{ property.title }}</h3>
<p>{{ formatPrice(property.price) }}</p>
<button @click="toggleFavorite">
<i :class="favoriteIcon"></i>
</button>
</div>
</template>
<script>
export default {
props: ['property'],
data() {
return {
isFavorite: false
}
},
computed: {
favoriteIcon() {
return this.isFavorite ? 'fas fa-heart' : 'far fa-heart'
}
},
methods: {
formatPrice(price) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(price)
},
toggleFavorite() {
this.isFavorite = !this.isFavorite
// API call to save favorite
}
}
}
</script>
This felt sophisticated. We had state management, reactivity, and a clear separation of concerns.
The Pain Points
But underneath the polish, problems festered:
1. Build Complexity
Our package.json had 47 dependencies. Webpacker configuration was 300+ lines. Every deployment:
# Rails asset compilation
RAILS_ENV=production rails assets:precompile
# Plus Webpack
NODE_ENV=production webpack --config webpack.config.js
# Build time: ~4 minutes
# Failed builds: ~20% of the time (random dependency issues)
2. SEO Nightmare
Property listings are content. Google needs to index them. But our Vue SPA:
- Rendered blank HTML initially
- Required JavaScript to display properties
- Needed complex server-side rendering setup (which we never implemented)
Google Search Console:
- Pages indexed: 23 (out of 200+ properties)
- Core Web Vitals: "Needs improvement"
3. Two Codebases to Maintain
Every feature meant:
1. Rails API endpoint
2. Vue component
3. State management
4. API integration
5. Error handling (twice — frontend + backend)
Example: Adding a new property field
# 1. Rails migration
add_column :pwb_properties, :year_built, :integer
# 2. Rails serializer
def year_built
object.year_built
end
# 3. Vue API call
async fetchProperty(id) {
const response = await axios.get(`/api/properties/${id}`)
this.property = response.data
}
# 4. Vue template
<p>Built in {{ property.year_built }}</p>
# 5. Vue form
<input v-model="property.year_built" type="number">
Total files touched: 6
Lines of code: ~50
Potential bug locations: 6
4. Dependency Hell
Our yarn.lock was 14,000 lines. We had:
- Vue 2.x (Vue 3 migration would be massive)
- Webpack 4 (Webpack 5 breaking changes)
- 45 other packages with their own dependencies
Security vulnerabilities: A constant stream.
npm audit
found 23 vulnerabilities (8 moderate, 15 high)
Every npm install was a gamble.
5. Performance Overhead
Our JavaScript bundle was 487 KB (minified).
For a property listing site, users waited for:
1. HTML to load
2. JavaScript to download (487 KB)
3. JavaScript to parse and execute
4. API call to fetch properties
5. Vue to render
Time to interactive: 3.2 seconds (on 3G)
For what? Displaying 12 property cards.
The Breaking Point
In 2024, upgrading to Rails 8 meant dealing with:
- Webpacker → Importmaps migration
- Vue 2 → Vue 3 (breaking changes)
- Webpack 4 → 5 (more breaking changes)
- EmberJS admin panel (totally outdated)
I looked at the codebase and asked:
"What does this JavaScript complexity actually give us?"
The honest answer: Not enough.
The Rewrite (Rails Views + Turbo)
Decision: Embrace Server-Side Rendering
Rails 7+ includes Hotwire (Turbo + Stimulus). It promised:
- Server-rendered HTML
- SPA-like interactions
- No build step
- Simple deployment
I was skeptical. Could it really replace Vue?
Migration Strategy
Step 1: Convert Vue Components to Rails Partials
Before (Vue):
<property-card :property="property"></property-card>
After (Rails partial):
<%= render 'properties/card', property: property %>
Step 2: Replace Client-Side Routing with Turbo Frames
Before (Vue Router):
{
path: '/properties/:id',
component: PropertyShow
}
After (Turbo Frames):
<%= turbo_frame_tag "property_#{property.id}" do %>
<%= render 'properties/show', property: property %>
<% end %>
Step 3: Interactive Elements with Stimulus
For things that needed JavaScript (like favoriting), we used Stimulus (Rails' lightweight JS framework):
// app/javascript/controllers/favorite_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["icon"]
toggle() {
const propertyId = this.element.dataset.propertyId
fetch(`/favorites/${propertyId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}).then(response => {
if (response.ok) {
this.iconTarget.classList.toggle('fas')
this.iconTarget.classList.toggle('far')
}
})
}
}
HTML:
<div data-controller="favorite" data-property-id="<%= property.id %>">
<button data-action="click->favorite#toggle">
<i data-favorite-target="icon" class="far fa-heart"></i>
</button>
</div>
Lines of code: 15 (vs. 50+ for Vue version)
Dependencies: 0 (Stimulus ships with Rails)
The Results
Build Complexity: Eliminated
Before:
"dependencies": {
"vue": "^2.6.14",
"vue-router": "^3.5.3",
"vuex": "^3.6.2",
"axios": "^0.27.2",
// ... 43 more packages
}
After:
{}
No package.json. No webpack.config.js. No build step.
Deployment time: 4 minutes → 45 seconds
SEO: Fixed
Google Search Console (3 months after rewrite):
- Pages indexed: 187 (out of 200+)
- Core Web Vitals: "Good"
- Click-through rate: +34%
Why? Google sees fully-rendered HTML immediately. No JavaScript required.
Performance: Massively Improved
Bundle size:
- Before: 487 KB JavaScript
- After: 37 KB (Turbo + Stimulus)
Time to interactive:
- Before: 3.2s (3G)
- After: 0.8s (3G)
Lighthouse score:
- Before: 68/100
- After: 94/100
Development Speed: Faster
Adding a new property field:
# 1. Migration
add_column :pwb_properties, :year_built, :integer
# 2. View
<p>Built in <%= property.year_built %></p>
# 3. Form
<%= form.number_field :year_built %>
Total files: 3
Lines of code: 15
Time: 5 minutes
(Previously: 6 files, 50 lines, 20 minutes)
Maintenance: Trivial
Security vulnerabilities:
- Before: Weekly npm audit warnings
- After: Handled by bundle update
Dependency updates:
- Before: Fear-based (will Webpack break?)
- After: Routine Rails upgrades
What We Lost
Honesty time: We did lose some things.
1. Instant Search Filtering
Before: Type in search → Vue filters results instantly (client-side)
After: Type in search → Submit → Server filters → Page refresh (with Turbo, no full reload)
Impact: Slightly less "magical" feeling. But still fast (<200ms).
Trade-off worth it? Yes. Most users submit the form anyway.
2. Fancy Transitions
Before: Vue transitions between states (fade in/out, slide)
After: Basic CSS transitions
Impact: Minimal. Users didn't notice.
3. Component Reusability (Kind Of)
Vue's component system is elegant:
<PropertyCard :property="property" :show-favorite="true" />
Rails partials are... less elegant:
<%= render 'properties/card', property: property, show_favorite: true %>
But: ViewComponents (Rails gem) bridges this gap:
<%= render PropertyCardComponent.new(property: property, show_favorite: true) %>
Impact: Solved with a 50-line Ruby class.
What We Gained
1. Simplicity
One language (Ruby), one framework (Rails), one deployment target.
Team onboarding:
- Before: "Learn Rails, Vue, Webpack, state management..."
- After: "Learn Rails."
2. Reliability
Deployment success rate:
- Before: ~80% (Webpack failures, dependency conflicts)
- After: ~99%
3. Speed (Development)
Features ship faster. No context-switching between Rails and Vue.
Example: Adding multi-language support
- Before: i18n in Rails + i18n in Vue (two systems)
- After: Rails i18n (one system)
Time saved: ~40%
4. Accessibility
Server-rendered HTML is inherently accessible. Vue SPAs require extra work:
- Screen readers
- Keyboard navigation
- Progressive enhancement
With Rails views: Most accessibility is automatic.
When Vue (or React) Makes Sense
I'm not saying SPAs are always wrong. They're excellent for:
Highly Interactive Apps
- Figma (complex canvas manipulation)
- Notion (real-time collaboration)
- Google Sheets (spreadsheet interactions)
Real-Time Features
- Chat applications
- Collaborative editing
- Live dashboards
Offline-First Apps
- PWAs that work without internet
- Mobile apps (React Native)
But for content-heavy sites? (Blogs, e-commerce, real estate, etc.)
Server-side rendering + Turbo/htmx is often better.
The Controversial Take
The JavaScript community made us believe:
"You need React/Vue for a modern web app."
This is not true.
You need JavaScript for:
- Highly interactive UIs
- Real-time collaboration
- Offline functionality
You don't need it for:
- Displaying content
- Simple forms
- CRUD operations
- Most websites
Rails (and Django, Laravel, Phoenix) got sidelined in favor of JAMstack and SPAs.
The pendulum is swinging back.
Tools like:
- Hotwire (Rails)
- htmx (any backend)
- LiveView (Phoenix/Elixir)
- Inertia.js (Laravel)
...prove you can have SPA-like UX without SPA complexity.
Lessons Learned
1. Choose Boring Technology
Vue.js was exciting in 2017. But "exciting" ages poorly.
Rails views are boring. They'll work the same way in 2030.
2. Optimize for Change
The codebase that's easiest to modify wins long-term.
Vue required changing 6 files per feature.
Rails views: 2-3 files.
Over time, this compounds.
3. Question Hype
Just because everyone uses React doesn't mean you should.
Ask:
- Do I actually need client-side rendering?
- Is this complexity worth it?
- Can I achieve 90% of the UX with 10% of the code?
4. Leverage Your Framework
Rails 8 includes:
- Turbo (SPA-like navigation)
- Stimulus (lightweight JS)
- Importmaps (no bundler)
- ActionCable (WebSockets)
- ActiveStorage (file uploads)
Why add Webpack, Vue, and 47 npm packages?
Conclusion
PropertyWebBuilder is faster, simpler, and easier to maintain after ditching Vue.js.
The metrics:
- 📉 JavaScript: 487 KB → 37 KB
- ⚡ Time to interactive: 3.2s → 0.8s
- 📈 SEO: 23 pages indexed → 187 pages
- 🚀 Deploy time: 4 min → 45 sec
- 🛠️ Dependencies: 47 npm packages → 0
The lesson:
Sometimes the "legacy" approach is actually the modern one.
Server-rendered HTML isn't outdated. It's been patiently waiting for us to remember why it worked in the first place.
Further Reading
- Hotwire Documentation
- htmx: High Power Tools for HTML
- The Case for Server-Side Rendering
- Have Single-Page Apps Ruined the Web? (Rich Harris talk)
Disagree? That's fine. If Vue/React works for you, great. But if you're drowning in JavaScript complexity for a content-heavy site, consider going back to basics.
It might just be the most modern choice you make.
Follow the PropertyWebBuilder journey on GitHub or find me on Twitter [@etewiah]
PropertyWebBuilder — Open-source Rails engine for real estate websites