Part 1: The Problem – When Legacy Meets Modern Frontend
Our Architecture: A Common Evolution Story
Many web applications today follow a similar evolutionary path. What started as a traditional Rails monolith gradually transforms into a modern hybrid architecture. Our application, let’s call it “MealCorp,” followed this exact journey:
Phase 1: Traditional Rails Monolith
# Traditional Rails serving HTML + embedded JavaScript
class HomeController < ApplicationController
def index
# Rails renders ERB templates with inline scripts
render 'home/index'
end
end
Phase 2: Hybrid Rails + Vue Architecture (Current State)
# Modern hybrid: Rails API + Vue frontend
class AppController < ApplicationController
INDEX_PATH = Rails.root.join('public', 'app.html')
INDEX_CONTENT = File.exist?(INDEX_PATH) && File.open(INDEX_PATH, &:read).html_safe
def index
if Rails.env.development?
redirect_to request.url.gsub(':3000', ':5173') # Vite dev server
else
render html: INDEX_CONTENT # Serve built Vue app
end
end
end
The routes configuration looked like this:
# config/routes.rb
Rails.application.routes.draw do
root 'home#index'
get '/dashboard' => 'app#index'
get '/settings' => 'app#index'
get '/profile' => 'app#index'
# Most routes serve the Vue SPA
end
The Hidden Performance Killer
While our frontend was modern and fast, we discovered a critical performance issue that’s common in hybrid architectures. Our Google PageSpeed scores were suffering, showing this alarming breakdown:
JavaScript Execution Time Analysis:
Reduce JavaScript execution time: 1.7s
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Script โ Total โ Evaluation โ Parse โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Google Tag Manager โ 615ms โ 431ms โ 171ms โ
โ Rollbar Error Tracking โ 258ms โ 218ms โ 40ms โ
โ Facebook SDK โ 226ms โ 155ms โ 71ms โ
โ Main Application Bundle โ 190ms โ 138ms โ 52ms โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The smoking gun? Third-party monitoring scripts were consuming more execution time than our actual application!
Investigating the Mystery
The puzzle deepened when we compared our source files:
Vue Frontend Source (index.html):
<pre class="wp-block-syntaxhighlighter-code"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>MealCorp Dashboard</title>
<!-- Clean, minimal head section -->
<a href="https://js.stripe.com/v3">https://js.stripe.com/v3</a>
<a href="https://kit.fontawesome.com/abc123.js">https://kit.fontawesome.com/abc123.js</a>
</head>
<body>
<div id="app"></div>
<a href="/src/main.ts">/src/main.ts</a>
</body>
</html></pre>
Built Static File (public/app.html):
<pre class="wp-block-syntaxhighlighter-code"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>MealCorp Dashboard</title>
<!-- Same clean content, no third-party scripts -->
<a href="/assets/index-xyz123.js">/assets/index-xyz123.js</a>
<link rel="stylesheet" crossorigin href="/assets/index-abc456.css">
</head>
<body>
<div id="app"></div>
</body>
</html></pre>
But Browser “View Source” Showed:
<pre class="wp-block-syntaxhighlighter-code"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>MealCorp Dashboard</title>
<!-- Mystery scripts appearing from nowhere! -->
<script>var _rollbarConfig = {"accessToken":"token123...","captureUncaught":true...}</script>
<script>!function(r){var e={}; /* Minified Rollbar library */ }</script>
<script>(function(w,d,s,l,i){w[l]=w[l]||[]; /* GTM script */ })(window,document,'script','dataLayer','GTM-ABC123');</script>
<!-- Our clean application code -->
<a href="/assets/index-xyz123.js">/assets/index-xyz123.js</a>
</body>
</html></pre>
The Root Cause Discovery
After investigation, we discovered that Rails was automatically injecting third-party scripts at runtime, despite serving static files!
Here’s what was happening in our Rails configuration:
Google Tag Manager Configuration:
# config/initializers/analytics.rb (Old problematic approach)
# This was loading synchronously in the Rails asset pipeline
Rollbar Configuration:
# config/initializers/rollbar.rb
Rollbar.configure do |config|
config.access_token = 'server_side_token_123'
# The culprit: Automatic JavaScript injection!
config.js_enabled = true # X This caused performance issues
config.js_options = {
accessToken: Rails.application.credentials[Rails.env.to_sym][:rollbar_client_token],
captureUncaught: true,
payload: { environment: Rails.env },
hostSafeList: ['example.com', 'staging.example.com']
}
end
The Request Flow That Caused Our Performance Issues:
- Browser requests
/dashboard
- Rails routes to
AppController#index
- Rails renders static
public/app.html content
- Rollbar gem automatically injects JavaScript into the HTML response
- GTM configuration adds synchronous tracking scripts
- Browser receives HTML with blocking third-party scripts
- Performance suffers due to synchronous execution
Part 2: The Solution – Modern Deferred Loading
Understanding the Performance Impact
The core issue was synchronous script execution during page load. Each third-party service was blocking the main thread:
// What was happening (blocking):
<script>
var _rollbarConfig = {...}; // Immediate execution - blocks rendering
</script>
<script>
(function(w,d,s,l,i){ // GTM immediate execution - blocks rendering
// Heavy synchronous operations
})(window,document,'script','dataLayer','GTM-ABC123');
</script>
The Modern Solution: Deferred Loading Architecture
We implemented a Vue-based deferred loading system that maintains identical functionality while dramatically improving performance.
Step 1: Disable Rails Auto-Injection
# config/initializers/rollbar.rb
Rollbar.configure do |config|
config.access_token = 'server_side_token_123'
# Disable automatic JavaScript injection for better performance
config.js_enabled = false # Good - Stop Rails from injecting scripts
# Server-side error tracking remains unchanged
config.person_method = "current_user"
# ... other server-side config
end
Step 2: Implement Vue-Based Deferred Loading
// src/App.vue
<script setup lang="ts">
import { onMounted } from 'vue';
// Load third-party scripts after Vue app mounts for better performance
onMounted(() => {
loadGoogleTagManager();
loadRollbarDeferred();
});
function loadGoogleTagManager() {
const script = document.createElement('script');
script.async = true;
script.src = `https://www.googletagmanager.com/gtm.js?id=${import.meta.env.VITE_GTM_ID}`;
// Track initial pageview once GTM loads
script.onload = () => {
trackEvent({
event: 'page_view',
page_title: document.title,
page_location: window.location.href,
page_path: window.location.pathname
});
};
document.head.appendChild(script);
}
function loadRollbarDeferred() {
const rollbarToken = import.meta.env.VITE_ROLLBAR_CLIENT_TOKEN;
if (!rollbarToken) return;
// Load after all other resources are complete
window.addEventListener('load', () => {
// Initialize Rollbar configuration
(window as any)._rollbarConfig = {
accessToken: rollbarToken,
captureUncaught: true,
payload: {
environment: import.meta.env.MODE // 'production', 'staging', etc.
},
hostSafeList: ['example.com', 'staging.example.com']
};
// Load Rollbar script asynchronously
const rollbarScript = document.createElement('script');
rollbarScript.async = true;
rollbarScript.src = 'https://cdn.rollbar.com/rollbarjs/refs/tags/v2.26.1/rollbar.min.js';
document.head.appendChild(rollbarScript);
});
}
</script>
Step 3: TypeScript Support
// src/types/global.d.ts
declare global {
interface Window {
_rollbarConfig?: {
accessToken: string;
captureUncaught: boolean;
payload: {
environment: string;
};
hostSafeList: string[];
};
dataLayer?: any[];
}
}
export {};
Environment Configuration
# .env.production
VITE_GTM_ID=GTM-PROD123
VITE_ROLLBAR_CLIENT_TOKEN=client_token_prod_456
# .env.staging
VITE_GTM_ID=GTM-STAGING789
VITE_ROLLBAR_CLIENT_TOKEN=client_token_staging_789
Testing the Implementation
Comprehensive Testing Script:
// Browser console testing function
function testTrackingImplementation() {
console.log('=== TRACKING SYSTEM TEST ===');
// Test 1: GTM Integration
console.log('GTM dataLayer exists:', !!window.dataLayer);
console.log('GTM script loaded:', !!document.querySelector('script[src*="googletagmanager.com"]'));
console.log('Recent GTM events:', window.dataLayer?.slice(-3));
// Test 2: Rollbar Integration
console.log('Rollbar loaded:', typeof Rollbar !== 'undefined');
console.log('Rollbar config:', window._rollbarConfig);
console.log('Rollbar script loaded:', !!document.querySelector('script[src*="rollbar"]'));
// Test 3: Send Test Events
// GTM Test Event
window.dataLayer?.push({
event: 'test_tracking',
test_source: 'manual_verification',
timestamp: new Date().toISOString()
});
// Rollbar Test Error
if (typeof Rollbar !== 'undefined') {
Rollbar.error('Test error for verification - please ignore', {
test_context: 'performance_optimization_verification'
});
}
console.log('โ
Test events sent - check dashboards in 1-2 minutes');
}
// Run the test
testTrackingImplementation();
Expected Console Output:
=== TRACKING SYSTEM TEST ===
GTM dataLayer exists: true
GTM script loaded: true
Recent GTM events: [
{event: "page_view", page_title: "Dashboard", ...},
{event: "gtm.dom", ...},
{event: "gtm.load", ...}
]
Rollbar loaded: true
Rollbar config: {accessToken: "...", captureUncaught: true, ...}
Rollbar script loaded: true
โ
Test events sent - check dashboards in 1-2 minutes
Performance Results
Before Optimization:
JavaScript Execution Time: 1.7s
โโโ Google Tag Manager: 615ms (synchronous)
โโโ Rollbar: 258ms (synchronous)
โโโ Facebook SDK: 226ms (synchronous)
โโโ Application Code: 190ms
After Optimization:
JavaScript Execution Time: 0.4s
โโโ Application Code: 190ms (immediate)
โโโ Deferred Scripts: ~300ms (non-blocking, post-load)
โโโ Performance Improvement: ~1.3s (76% reduction)
Key Benefits Achieved
- Performance Gains:
- 76% reduction in blocking JavaScript execution time
- Improved Core Web Vitals scores
- Better user experience with faster perceived load times
- Maintained Functionality:
- Identical error tracking capabilities
- Same analytics data collection
- All monitoring dashboards continue working
- Better Architecture:
- Modern Vue-based script management
- Environment-specific configuration
- TypeScript support for better maintainability
- Security Improvements:
- Proper separation of server vs. client tokens
- Environment variable management
- No sensitive data in version control
Common Pitfalls and Solutions
Issue 1: Token Confusion
Error: post_client_item scope required but token has post_server_item
Solution: Use separate client-side tokens for browser JavaScript.
Issue 2: Missing Initial Pageviews
Solution: Implement manual pageview tracking in script.onload callback.
Issue 3: TypeScript Errors
// Fix: Add proper type declarations
(window as any)._rollbarConfig = { ... }; // Type assertion approach
// OR declare global types for better type safety
This hybrid architecture optimization demonstrates how modern frontend practices can be retroactively applied to existing applications, achieving significant performance improvements while maintaining full functionality. The key is identifying where legacy server-side patterns conflict with modern client-side performance optimization and implementing targeted solutions.
Happy Optimization! ๐