๐Ÿš€ Optimizing Third-Party Script Loading in a Rails + Vue Hybrid Architecture

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:

  1. Browser requests /dashboard
  2. Rails routes to AppController#index
  3. Rails renders static public/app.html content
  4. Rollbar gem automatically injects JavaScript into the HTML response
  5. GTM configuration adds synchronous tracking scripts
  6. Browser receives HTML with blocking third-party scripts
  7. 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

  1. Performance Gains:
  • 76% reduction in blocking JavaScript execution time
  • Improved Core Web Vitals scores
  • Better user experience with faster perceived load times
  1. Maintained Functionality:
  • Identical error tracking capabilities
  • Same analytics data collection
  • All monitoring dashboards continue working
  1. Better Architecture:
  • Modern Vue-based script management
  • Environment-specific configuration
  • TypeScript support for better maintainability
  1. 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! ๐Ÿš€

๐Ÿšฆ Classic Performance Debugging Problems in Rails Apps โ€” Part 1: Finding the Bottlenecks

Rails makes building apps fast and joyful โ€” but sooner or later, every team runs into the same dreaded complaint:

“Why is this page so slow?”

Performance debugging is tricky because Rails abstracts so much for us. Underneath every User.where(...).first or current_user.orders.includes(:products), there’s real SQL, database indexes, network calls, caching layers, and Ruby code running.

This post (Part 1) focuses on how to find the bottlenecks in a Rails app using logs and manual inspection. In Part 2, we’ll explore tools like Rack Mini Profiler and real-world fixes.


๐Ÿ”Ž Symptoms of a Slow Rails Page

Before diving into logs, it’s important to recognize what “slow” might mean:

  • Page loads take several seconds.
  • CPU usage spikes during requests.
  • The database log shows queries running longer than expected.
  • Repeated queries (e.g. the same SELECT firing 30 times).
  • Memory bloat or high GC (garbage collection) activity.

Example symptom we hit:

SELECT "flipper_features"."key" AS feature_key,
       "flipper_gates"."key",
       "flipper_gates"."value"
FROM "flipper_features"
LEFT OUTER JOIN "flipper_gates"
ON "flipper_features"."key" = "flipper_gates"."feature_key"

This query was executed 38 times when loading a product page (/product/adidas-shoe). Thatโ€™s a red flag ๐Ÿšฉ.


๐Ÿ“œ Understanding Rails Logs

Every Rails request is logged in log/development.log (or production.log). A typical request looks like:

Started GET "/products/123" for 127.0.0.1 at 2025-09-25 12:45:01 +0530
Processing by ProductsController#show as HTML
  Parameters: {"id"=>"123"}
  Product Load (1.2ms)  SELECT "products".* FROM "products" WHERE "products"."id" = $1 LIMIT $2  [["id", 123], ["LIMIT", 1]]
  Review Load (10.4ms)  SELECT "reviews".* FROM "reviews" WHERE "reviews"."product_id" = $1  [["product_id", 123]]
Completed 200 OK in 120ms (Views: 80.0ms | ActiveRecord: 20.0ms | Allocations: 3456)

Key things to notice:

  • Controller action โ†’ ProductsController#show.
  • Individual SQL timings โ†’ each query shows how long it took.
  • Overall time โ†’ Completed 200 OK in 120ms.
  • Breakdown โ†’ Views: 80.0ms | ActiveRecord: 20.0ms.

If the DB time is small but Views are big โ†’ it’s a rendering problem.
If ActiveRecord dominates โ†’ the DB queries are the bottleneck.


๐Ÿ•ต๏ธ Debugging a Slow Page Step by Step

1. Watch your logs in real time

tail -f log/development.log | grep -i "SELECT"

This shows you every SQL query as it executes.

2. Look for repeated queries (N+1)

If you see the same SELECT firing dozens of times:

SELECT "reviews".* FROM "reviews" WHERE "reviews"."product_id" = 123
SELECT "reviews".* FROM "reviews" WHERE "reviews"."product_id" = 124
SELECT "reviews".* FROM "reviews" WHERE "reviews"."product_id" = 125

That’s the classic N+1 query problem.

3. Look for expensive joins

Queries with multiple JOINs can be slow without proper indexing. Example:

SELECT "orders"."id", "users"."email"
FROM "orders"
INNER JOIN "users" ON "users"."id" = "orders"."user_id"
WHERE "users"."status" = 'active'

If there’s no index on users.status, this can cause sequential scans.

4. Look for long-running queries

Rails logs include timings:

User Load (105.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 123

If a query consistently takes >100ms on small tables, it probably needs an index or query rewrite.


โšก Real Example: Debugging the Flipper Feature Flag Queries

In our case, the Rails logs showed:

SELECT "flipper_features"."key" AS feature_key,
       "flipper_gates"."key",
       "flipper_gates"."value"
FROM "flipper_features"
LEFT OUTER JOIN "flipper_gates"
ON "flipper_features"."key" = "flipper_gates"."feature_key"

  • It executed 38 times on one page.
  • Each execution took between 60โ€“200ms.
  • Together, that added ~6 seconds to page load time.

The query itself wasn’t huge (tables had <150 rows). The problem was repetition โ€” every feature flag check was hitting the DB fresh.

This pointed us toward caching (covered in Part 2).

๐Ÿงฉ Workflow for Performance Debugging in Rails

  1. Reproduce the slow page locally or in staging.
  2. Tail the logs and isolate the slow request.
  3. Categorize: rendering slow? DB queries slow? external API calls?
  4. Identify repeated or long queries.
  5. Ask “why“:
    • Missing index?
    • Bad join?
    • N+1 query?
    • Repeated lookups that could be cached?
  6. Confirm with SQL tools (EXPLAIN ANALYZE in Postgres).

โœ… Summary of Part 1

In this first part, we covered:

  • Recognizing symptoms of slow pages.
  • Reading Rails logs effectively.
  • Debugging step by step with queries and timings.
  • A real-world case of repeated Flipper queries slowing down a page.

In Part 2, we’ll go deeper into tools and solutions:

  • Setting up Rack Mini Profiler.
  • Capturing queries + stack traces in custom logs.
  • Applying fixes: indexes, eager loading, and caching (with Flipper as a worked example).

๐Ÿš€ Optimizing Vue 3 Page Rendering with for Async Components

When building a homepage in Vue, it’s common to split the UI into multiple components. Some of them are purely presentational, while others fetch data from APIs.

Here’s the problem: if one of those components uses an await during its setup, Vue will wait for it before rendering the parent. That means a single API call can block the entire page from appearing to the user.

That’s not what we want. A modern web app should feel snappy and responsive, even when waiting for data.

Vue 3 gives us the perfect tool for this: <Suspense>.


๐Ÿ— The Starting Point

Let’s look at a simplified index.vue homepage:

<template>
  <div>
    <Component1 :perServingPrice="data.perServingPrice" />

    <Component2 :perServingPrice="data.perServingPrice" />
    <Component3 :landingContentKey="landingContentKey" />
    <Component4 :perServingPrice="data.perServingPrice" />
    <Component5 :landingContentKey="landingContentKey" />
    <Component6 :recipes="data.recipes" />
    <Component7 :landingContentKey="landingContentKey" />
    <Component8 :recipes="data.recipes" />
  </div>
</template>

<script setup lang="ts">
import Component1 from '@/components/HomePage/Component1.vue'
import Component2 from '@/components/HomePage/Component2.vue'
import Component3 from '@/components/HomePage/Component3.vue'
import Component4 from '@/components/HomePage/Component4.vue'
import Component5 from '@/components/HomePage/Component5.vue'
import Component6 from '@/components/HomePage/Component6.vue'
import Component7 from '@/components/HomePage/Component7.vue'
import Component8 from '@/components/HomePage/Component8.vue'

const data = {
  perServingPrice: 10,
  recipes: [],
}
const landingContentKey = 'homepage'
</script>

Now imagine:

  • Component2 fetches special offers.
  • Component6 fetches recipe data.
  • Component8 fetches trending dishes.

If those API calls are written like this inside a child component:

<script setup lang="ts">
const response = await fetch('/api/recipes')
const recipes = await response.json()
</script>

โžก๏ธ Vue will not render the parent index.vue until this await is finished. That means the entire page waits, even though other components (like Component1 and Component3) donโ€™t need that data at all.


โณ Why Blocking Is a Problem

Letโ€™s simulate the render timeline without <Suspense>:

  • At t=0s: Page requested.
  • At t=0.3s: HTML + JS bundles load.
  • At t=0.4s: Component2 makes an API request.
  • At t=0.8s: Component6 makes an API request.
  • At t=1.2s: Component8 makes an API request.
  • At t=2.0s: All API responses return โ†’ finally the page renders.

The user stares at a blank page until everything resolves. ๐Ÿ˜ฉ


๐ŸŽฏ Enter <Suspense>

The <Suspense> component lets you wrap child components that might suspend (pause) while awaiting data. Instead of blocking the whole page, Vue shows:

  • The rest of the parent page (immediately).
  • A fallback placeholder for the async child until it’s ready.

๐Ÿ“ Example: Wrapping Component6

<Suspense>
  <template #default>
    <Component6 :recipes="data.recipes" />
  </template>
  <template #fallback>
    <div class="skeleton">Loading recipes...</div>
  </template>
</Suspense>

Here’s what happens now:

  • At t=0.4s: The page renders.
  • <Component6> isnโ€™t ready yet, so Vue shows the fallback (Loading recipes...).
  • At t=2.0s: Recipes arrive โ†’ Vue automatically replaces the fallback with the actual component.

Result: The page is usable instantly. โœ…


๐Ÿ”„ Applying Suspense to Multiple Components

We can selectively wrap only the async components:

<template>
  <div>
    <Component1 :perServingPrice="data.perServingPrice" />

    <Suspense>
      <template #default>
        <Component2 :perServingPrice="data.perServingPrice" />
      </template>
      <template #fallback>
        <div class="skeleton">Loading deals...</div>
      </template>
    </Suspense>

    <Component3 :landingContentKey="landingContentKey" />

    <Suspense>
      <template #default>
        <Component6 :recipes="data.recipes" />
      </template>
      <template #fallback>
        <div class="skeleton">Loading recipes...</div>
      </template>
    </Suspense>

    <Component7 :landingContentKey="landingContentKey" />

    <Suspense>
      <template #default>
        <Component8 :recipes="data.recipes" />
      </template>
      <template #fallback>
        <div class="skeleton">Loading trending dishes...</div>
      </template>
    </Suspense>
  </div>
</template>


๐Ÿ“ฆ Combining with Async Imports

Vue also allows lazy-loading the component itself, not just the data.

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

const Component6 = defineAsyncComponent(() =>
  import('@/components/HomePage/Component6.vue')
)
</script>

Now:

  • If the component file is heavy, Vue won’t even load it until needed.
  • Suspense covers both the network request and the async component loading.

๐Ÿ“Š Before vs After

Without Suspense:

  • Whole page waits until all API calls resolve.
  • User sees blank โ†’ page suddenly appears.

With Suspense:

  • Page renders instantly with placeholders.
  • Components hydrate individually as data arrives.
  • User perceives speed even if data is slow.

๐Ÿ† Best Practices

  1. Wrap only async components. Don’t spam <Suspense> everywhere.
  2. Always provide a meaningful fallback. Use skeleton loaders, not just “Loadingโ€ฆ”.
  3. Lift state when appropriate. If multiple components need the same data, fetch once in the parent and pass it down as props.
  4. Combine Suspense with code-splitting. Async imports keep your initial bundle small.
  5. Group related components. You can wrap multiple components in a single <Suspense> if they depend on the same async source.

โœ… Conclusion

With Vue 3 <Suspense>, you can make sure that your homepage never blocks while waiting for data. Each component becomes non-blocking and self-contained, showing a loader until it’s ready.

This is the same direction React and Angular have taken:

  • React โ†’ Suspense + Concurrent Rendering.
  • Angular โ†’ Route Resolvers + AsyncPipe.
  • Vue โ†’ <Suspense> + async setup.

๐Ÿ‘‰ If you want your Vue pages to feel fast and modern, adopt <Suspense> for async components.


Happy Vue Coding!